<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Validator;

use 
Zend\Stdlib\ErrorHandler;
use 
Zend\Stdlib\StringUtils;

/**
 * Please note there are two standalone test scripts for testing IDN characters due to problems
 * with file encoding.
 *
 * The first is tests/Zend/Validator/HostnameTestStandalone.php which is designed to be run on
 * the command line.
 *
 * The second is tests/Zend/Validator/HostnameTestForm.php which is designed to be run via HTML
 * to allow users to test entering UTF-8 characters in a form.
 */
class Hostname extends AbstractValidator
{
    const 
CANNOT_DECODE_PUNYCODE  'hostnameCannotDecodePunycode';
    const 
INVALID                 'hostnameInvalid';
    const 
INVALID_DASH            'hostnameDashCharacter';
    const 
INVALID_HOSTNAME        'hostnameInvalidHostname';
    const 
INVALID_HOSTNAME_SCHEMA 'hostnameInvalidHostnameSchema';
    const 
INVALID_LOCAL_NAME      'hostnameInvalidLocalName';
    const 
INVALID_URI             'hostnameInvalidUri';
    const 
IP_ADDRESS_NOT_ALLOWED  'hostnameIpAddressNotAllowed';
    const 
LOCAL_NAME_NOT_ALLOWED  'hostnameLocalNameNotAllowed';
    const 
UNDECIPHERABLE_TLD      'hostnameUndecipherableTld';
    const 
UNKNOWN_TLD             'hostnameUnknownTld';

    
/**
     * @var array
     */
    
protected $messageTemplates = array(
        
self::CANNOT_DECODE_PUNYCODE  => "The input appears to be a DNS hostname but the given punycode notation cannot be decoded",
        
self::INVALID                 => "Invalid type given. String expected",
        
self::INVALID_DASH            => "The input appears to be a DNS hostname but contains a dash in an invalid position",
        
self::INVALID_HOSTNAME        => "The input does not match the expected structure for a DNS hostname",
        
self::INVALID_HOSTNAME_SCHEMA => "The input appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'",
        
self::INVALID_LOCAL_NAME      => "The input does not appear to be a valid local network name",
        
self::INVALID_URI             => "The input does not appear to be a valid URI hostname",
        
self::IP_ADDRESS_NOT_ALLOWED  => "The input appears to be an IP address, but IP addresses are not allowed",
        
self::LOCAL_NAME_NOT_ALLOWED  => "The input appears to be a local network name but local network names are not allowed",
        
self::UNDECIPHERABLE_TLD      => "The input appears to be a DNS hostname but cannot extract TLD part",
        
self::UNKNOWN_TLD             => "The input appears to be a DNS hostname but cannot match TLD against known list",
    );

    
/**
     * @var array
     */
    
protected $messageVariables = array(
        
'tld' => 'tld',
    );

    const 
ALLOW_DNS   1;  // Allows Internet domain names (e.g., example.com)
    
const ALLOW_IP    2;  // Allows IP addresses
    
const ALLOW_LOCAL 4;  // Allows local network names (e.g., localhost, www.localdomain)
    
const ALLOW_URI   8;  // Allows URI hostnames
    
const ALLOW_ALL   15;  // Allows all types of hostnames

    /**
     * Array of valid top-level-domains
     *
     * @see ftp://data.iana.org/TLD/tlds-alpha-by-domain.txt  List of all TLDs by domain
     * @see http://www.iana.org/domains/root/db/ Official list of supported TLDs
     * @var array
     */
    
protected $validTlds = array(
        
'ac''ad''ae''aero''af''ag''ai''al''am''an''ao''aq''ar''arpa',
        
'as''asia''at''au''aw''ax''az''ba''bb''bd''be''bf''bg''bh''bi',
        
'biz''bj''bm''bn''bo''br''bs''bt''bv''bw''by''bz''ca''cat''cc',
        
'cd''cf''cg''ch''ci''ck''cl''cm''cn''co''com''coop''cr''cu',
        
'cv''cx''cy''cz''de''dj''dk''dm''do''dz''ec''edu''ee''eg''er',
        
'es''et''eu''fi''fj''fk''fm''fo''fr''ga''gb''gd''ge''gf''gg',
        
'gh''gi''gl''gm''gn''gov''gp''gq''gr''gs''gt''gu''gw''gy''hk',
        
'hm''hn''hr''ht''hu''id''ie''il''im''in''info''int''io''iq',
        
'ir''is''it''je''jm''jo''jobs''jp''ke''kg''kh''ki''km''kn''kp',
        
'kr''kw''ky''kz''la''lb''lc''li''lk''lr''ls''lt''lu''lv''ly',
        
'ma''mc''md''me''mg''mh''mil''mk''ml''mm''mn''mo''mobi''mp',
        
'mq''mr''ms''mt''mu''museum''mv''mw''mx''my''mz''na''name''nc',
        
'ne''net''nf''ng''ni''nl''no''np''nr''nu''nz''om''org''pa''pe',
        
'pf''pg''ph''pk''pl''pm''pn''pr''pro''ps''pt''pw''py''qa''re',
        
'ro''rs''ru''rw''sa''sb''sc''sd''se''sg''sh''si''sj''sk''sl',
        
'sm''sn''so''sr''st''su''sv''sy''sz''tc''td''tel''tf''tg''th',
        
'tj''tk''tl''tm''tn''to''tp''tr''travel''tt''tv''tw''tz''ua',
        
'ug''uk''um''us''uy''uz''va''vc''ve''vg''vi''vn''vu''wf''ws',
        
'xxx''ye''yt''yu''za''zm''zw'
    
);

    
/**
     * Array for valid Idns
     * @see http://www.iana.org/domains/idn-tables/ Official list of supported IDN Chars
     * (.AC) Ascension Island http://www.nic.ac/pdf/AC-IDN-Policy.pdf
     * (.AR) Argentina http://www.nic.ar/faqidn.html
     * (.AS) American Samoa http://www.nic.as/idn/chars.cfm
     * (.AT) Austria http://www.nic.at/en/service/technical_information/idn/charset_converter/
     * (.BIZ) International http://www.iana.org/domains/idn-tables/
     * (.BR) Brazil http://registro.br/faq/faq6.html
     * (.BV) Bouvett Island http://www.norid.no/domeneregistrering/idn/idn_nyetegn.en.html
     * (.CAT) Catalan http://www.iana.org/domains/idn-tables/tables/cat_ca_1.0.html
     * (.CH) Switzerland https://nic.switch.ch/reg/ocView.action?res=EF6GW2JBPVTG67DLNIQXU234MN6SC33JNQQGI7L6#anhang1
     * (.CL) Chile http://www.iana.org/domains/idn-tables/tables/cl_latn_1.0.html
     * (.COM) International http://www.verisign.com/information-services/naming-services/internationalized-domain-names/index.html
     * (.DE) Germany http://www.denic.de/en/domains/idns/liste.html
     * (.DK) Danmark http://www.dk-hostmaster.dk/index.php?id=151
     * (.ES) Spain https://www.nic.es/media/2008-05/1210147705287.pdf
     * (.FI) Finland http://www.ficora.fi/en/index/palvelut/fiverkkotunnukset/aakkostenkaytto.html
     * (.GR) Greece https://grweb.ics.forth.gr/CharacterTable1_en.jsp
     * (.HU) Hungary http://www.domain.hu/domain/English/szabalyzat/szabalyzat.html
     * (.INFO) International http://www.nic.info/info/idn
     * (.IO) British Indian Ocean Territory http://www.nic.io/IO-IDN-Policy.pdf
     * (.IR) Iran http://www.nic.ir/Allowable_Characters_dot-iran
     * (.IS) Iceland http://www.isnic.is/domain/rules.php
     * (.KR) Korea http://www.iana.org/domains/idn-tables/tables/kr_ko-kr_1.0.html
     * (.LI) Liechtenstein https://nic.switch.ch/reg/ocView.action?res=EF6GW2JBPVTG67DLNIQXU234MN6SC33JNQQGI7L6#anhang1
     * (.LT) Lithuania http://www.domreg.lt/static/doc/public/idn_symbols-en.pdf
     * (.MD) Moldova http://www.register.md/
     * (.MUSEUM) International http://www.iana.org/domains/idn-tables/tables/museum_latn_1.0.html
     * (.NET) International http://www.verisign.com/information-services/naming-services/internationalized-domain-names/index.html
     * (.NO) Norway http://www.norid.no/domeneregistrering/idn/idn_nyetegn.en.html
     * (.NU) Niue http://www.worldnames.net/
     * (.ORG) International http://www.pir.org/index.php?db=content/FAQs&tbl=FAQs_Registrant&id=2
     * (.PE) Peru https://www.nic.pe/nuevas_politicas_faq_2.php
     * (.PL) Poland http://www.dns.pl/IDN/allowed_character_sets.pdf
     * (.PR) Puerto Rico http://www.nic.pr/idn_rules.asp
     * (.PT) Portugal https://online.dns.pt/dns_2008/do?com=DS;8216320233;111;+PAGE(4000058)+K-CAT-CODIGO(C.125)+RCNT(100);
     * (.RU) Russia http://www.iana.org/domains/idn-tables/tables/ru_ru-ru_1.0.html
     * (.SA) Saudi Arabia http://www.iana.org/domains/idn-tables/tables/sa_ar_1.0.html
     * (.SE) Sweden http://www.iis.se/english/IDN_campaignsite.shtml?lang=en
     * (.SH) Saint Helena http://www.nic.sh/SH-IDN-Policy.pdf
     * (.SJ) Svalbard and Jan Mayen http://www.norid.no/domeneregistrering/idn/idn_nyetegn.en.html
     * (.TH) Thailand http://www.iana.org/domains/idn-tables/tables/th_th-th_1.0.html
     * (.TM) Turkmenistan http://www.nic.tm/TM-IDN-Policy.pdf
     * (.TR) Turkey https://www.nic.tr/index.php
     * (.VE) Venice http://www.iana.org/domains/idn-tables/tables/ve_es_1.0.html
     * (.VN) Vietnam http://www.vnnic.vn/english/5-6-300-2-2-04-20071115.htm#1.%20Introduction
     *
     * @var array
     */
    
protected $validIdns = array(
        
'AC'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿāăąćĉċčďđēėęěĝġģĥħīįĵķĺļľŀłńņňŋőœŕŗřśŝşšţťŧūŭůűųŵŷźżž]{1,63}$/iu'),
        
'AR'  => array(=> '/^[\x{002d}0-9a-zà-ãç-êìíñ-õü]{1,63}$/iu'),
        
'AS'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıĵķĸĺļľłńņňŋōŏőœŕŗřśŝşšţťŧũūŭůűųŵŷźż]{1,63}$/iu'),
        
'AT'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿœšž]{1,63}$/iu'),
        
'BIZ' => 'Hostname/Biz.php',
        
'BR'  => array(=> '/^[\x{002d}0-9a-zà-ãçéíó-õúü]{1,63}$/iu'),
        
'BV'  => array(=> '/^[\x{002d}0-9a-zàáä-éêñ-ôöøüčđńŋšŧž]{1,63}$/iu'),
        
'CAT' => array(=> '/^[\x{002d}0-9a-z·àç-éíïòóúü]{1,63}$/iu'),
        
'CH'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿœ]{1,63}$/iu'),
        
'CL'  => array(=> '/^[\x{002d}0-9a-záéíñóúü]{1,63}$/iu'),
        
'CN'  => 'Hostname/Cn.php',
        
'COM' => 'Hostname/Com.php',
        
'DE'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿăąāćĉčċďđĕěėęēğĝġģĥħĭĩįīıĵķĺľļłńňņŋŏőōœĸŕřŗśŝšşťţŧŭůűũųūŵŷźžż]{1,63}$/iu'),
        
'DK'  => array(=> '/^[\x{002d}0-9a-zäéöü]{1,63}$/iu'),
        
'ES'  => array(=> '/^[\x{002d}0-9a-zàáçèéíïñòóúü·]{1,63}$/iu'),
        
'EU'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıĵķĺļľŀłńņňʼnŋōŏőœŕŗřśŝšťŧũūŭůűųŵŷźżž]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zșț]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zΐάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zабвгдежзийклмнопрстуфхцчшщъыьэюя]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zἀ-ἇἐ-ἕἠ-ἧἰ-ἷὀ-ὅὐ-ὗὠ-ὧὰ-ὼώᾀ-ᾇᾐ-ᾗᾠ-ᾧᾰ-ᾴᾶᾷῂῃῄῆῇῐ-ῒΐῖῗῠ-ῧῲῳῴῶῷ]{1,63}$/iu'),
        
'FI'  => array(=> '/^[\x{002d}0-9a-zäåö]{1,63}$/iu'),
        
'GR'  => array(=> '/^[\x{002d}0-9a-zΆΈΉΊΌΎ-ΡΣ-ώἀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼῂῃῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲῳῴῶ-ῼ]{1,63}$/iu'),
        
'HK'  => 'Hostname/Cn.php',
        
'HU'  => array(=> '/^[\x{002d}0-9a-záéíóöúüőű]{1,63}$/iu'),
        
'INFO'=> array(=> '/^[\x{002d}0-9a-zäåæéöøü]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-záéíóöúüőű]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-záæéíðóöúýþ]{1,63}$/iu',
            
=> '/^[\x{AC00}-\x{D7A3}]{1,17}$/iu',
            
=> '/^[\x{002d}0-9a-zāčēģīķļņōŗšūž]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-ząčėęįšūųž]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zóąćęłńśźż]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-záéíñóúü]{1,63}$/iu'),
        
'IO'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿăąāćĉčċďđĕěėęēğĝġģĥħĭĩįīıĵķĺľļłńňņŋŏőōœĸŕřŗśŝšşťţŧŭůűũųūŵŷźžż]{1,63}$/iu'),
        
'IS'  => array(=> '/^[\x{002d}0-9a-záéýúíóþæöð]{1,63}$/iu'),
        
'IT'  => array(=> '/^[\x{002d}0-9a-zàâäèéêëìîïòôöùûüæœçÿß-]{1,63}$/iu'),
        
'JP'  => 'Hostname/Jp.php',
        
'KR'  => array(=> '/^[\x{AC00}-\x{D7A3}]{1,17}$/iu'),
        
'LI'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿœ]{1,63}$/iu'),
        
'LT'  => array(=> '/^[\x{002d}0-9ąčęėįšųūž]{1,63}$/iu'),
        
'MD'  => array(=> '/^[\x{002d}0-9ăâîşţ]{1,63}$/iu'),
        
'MUSEUM' => array(=> '/^[\x{002d}0-9a-zà-öø-ÿāăąćċčďđēėęěğġģħīįıķĺļľłńņňŋōőœŕŗřśşšţťŧūůűųŵŷźżžǎǐǒǔ\x{01E5}\x{01E7}\x{01E9}\x{01EF}ə\x{0292}ẁẃẅỳ]{1,63}$/iu'),
        
'NET' => 'Hostname/Com.php',
        
'NO'  => array(=> '/^[\x{002d}0-9a-zàáä-éêñ-ôöøüčđńŋšŧž]{1,63}$/iu'),
        
'NU'  => 'Hostname/Com.php',
        
'ORG' => array(=> '/^[\x{002d}0-9a-záéíñóúü]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zóąćęłńśźż]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-záäåæéëíðóöøúüýþ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-záéíóöúüőű]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-ząčėęįšūųž]{1,63}$/iu',
            
=> '/^[\x{AC00}-\x{D7A3}]{1,17}$/iu',
            
=> '/^[\x{002d}0-9a-zāčēģīķļņōŗšūž]{1,63}$/iu'),
        
'PE'  => array(=> '/^[\x{002d}0-9a-zñáéíóúü]{1,63}$/iu'),
        
'PL'  => array(=> '/^[\x{002d}0-9a-zāčēģīķļņōŗšūž]{1,63}$/iu',
            
=> '/^[\x{002d}а-ик-ш\x{0450}ѓѕјљњќџ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zâîăşţ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9а-яё\x{04C2}]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zàáâèéêìíîòóôùúûċġħż]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zàäåæéêòóôöøü]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zóąćęłńśźż]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zàáâãçéêíòóôõúü]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zâîăşţ]{1,63}$/iu',
            
10=> '/^[\x{002d}0-9a-záäéíóôúýčďĺľňŕšťž]{1,63}$/iu',
            
11=> '/^[\x{002d}0-9a-zçë]{1,63}$/iu',
            
12=> '/^[\x{002d}0-9а-ик-шђјљњћџ]{1,63}$/iu',
            
13=> '/^[\x{002d}0-9a-zćčđšž]{1,63}$/iu',
            
14=> '/^[\x{002d}0-9a-zâçöûüğış]{1,63}$/iu',
            
15=> '/^[\x{002d}0-9a-záéíñóúü]{1,63}$/iu',
            
16=> '/^[\x{002d}0-9a-zäõöüšž]{1,63}$/iu',
            
17=> '/^[\x{002d}0-9a-zĉĝĥĵŝŭ]{1,63}$/iu',
            
18=> '/^[\x{002d}0-9a-zâäéëîô]{1,63}$/iu',
            
19=> '/^[\x{002d}0-9a-zàáâäåæçèéêëìíîïðñòôöøùúûüýćčłńřśš]{1,63}$/iu',
            
20=> '/^[\x{002d}0-9a-zäåæõöøüšž]{1,63}$/iu',
            
21=> '/^[\x{002d}0-9a-zàáçèéìíòóùú]{1,63}$/iu',
            
22=> '/^[\x{002d}0-9a-zàáéíóöúüőű]{1,63}$/iu',
            
23=> '/^[\x{002d}0-9ΐά-ώ]{1,63}$/iu',
            
24=> '/^[\x{002d}0-9a-zàáâåæçèéêëðóôöøüþœ]{1,63}$/iu',
            
25=> '/^[\x{002d}0-9a-záäéíóöúüýčďěňřšťůž]{1,63}$/iu',
            
26=> '/^[\x{002d}0-9a-z·àçèéíïòóúü]{1,63}$/iu',
            
27=> '/^[\x{002d}0-9а-ъьюя\x{0450}\x{045D}]{1,63}$/iu',
            
28=> '/^[\x{002d}0-9а-яёіў]{1,63}$/iu',
            
29=> '/^[\x{002d}0-9a-ząčėęįšūųž]{1,63}$/iu',
            
30=> '/^[\x{002d}0-9a-záäåæéëíðóöøúüýþ]{1,63}$/iu',
            
31=> '/^[\x{002d}0-9a-zàâæçèéêëîïñôùûüÿœ]{1,63}$/iu',
            
32=> '/^[\x{002d}0-9а-щъыьэюяёєіїґ]{1,63}$/iu',
            
33=> '/^[\x{002d}0-9א-ת]{1,63}$/iu'),
        
'PR'  => array(=> '/^[\x{002d}0-9a-záéíóúñäëïüöâêîôûàèùæçœãõ]{1,63}$/iu'),
        
'PT'  => array(=> '/^[\x{002d}0-9a-záàâãçéêíóôõú]{1,63}$/iu'),
        
'RU'  => array(=> '/^[\x{002d}0-9а-яё]{1,63}$/iu'),
        
'SA'  => array(=> '/^[\x{002d}.0-9\x{0621}-\x{063A}\x{0641}-\x{064A}\x{0660}-\x{0669}]{1,63}$/iu'),
        
'SE'  => array(=> '/^[\x{002d}0-9a-zäåéöü]{1,63}$/iu'),
        
'SH'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿăąāćĉčċďđĕěėęēğĝġģĥħĭĩįīıĵķĺľļłńňņŋŏőōœĸŕřŗśŝšşťţŧŭůűũųūŵŷźžż]{1,63}$/iu'),
        
'SI'  => array(
            
=> '/^[\x{002d}0-9a-zà-öø-ÿ]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıĵķĺļľŀłńņňʼnŋōŏőœŕŗřśŝšťŧũūŭůűųŵŷźżž]{1,63}$/iu',
            
=> '/^[\x{002d}0-9a-zșț]{1,63}$/iu'),
        
'SJ'  => array(=> '/^[\x{002d}0-9a-zàáä-éêñ-ôöøüčđńŋšŧž]{1,63}$/iu'),
        
'TH'  => array(=> '/^[\x{002d}0-9a-z\x{0E01}-\x{0E3A}\x{0E40}-\x{0E4D}\x{0E50}-\x{0E59}]{1,63}$/iu'),
        
'TM'  => array(=> '/^[\x{002d}0-9a-zà-öø-ÿāăąćĉċčďđēėęěĝġģĥħīįĵķĺļľŀłńņňŋőœŕŗřśŝşšţťŧūŭůűųŵŷźżž]{1,63}$/iu'),
        
'TW'  => 'Hostname/Cn.php',
        
'TR'  => array(=> '/^[\x{002d}0-9a-zğıüşöç]{1,63}$/iu'),
        
'VE'  => array(=> '/^[\x{002d}0-9a-záéíóúüñ]{1,63}$/iu'),
        
'VN'  => array(=> '/^[ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚÝàáâãèéêìíòóôõùúýĂăĐđĨĩŨũƠơƯư\x{1EA0}-\x{1EF9}]{1,63}$/iu'),
        
'中国' => 'Hostname/Cn.php',
        
'中國' => 'Hostname/Cn.php',
        
'ලංකා' => array(=> '/^[\x{0d80}-\x{0dff}]{1,63}$/iu'),
        
'香港' => 'Hostname/Cn.php',
        
'台湾' => 'Hostname/Cn.php',
        
'台灣' => 'Hostname/Cn.php',
        
'امارات'   => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
        
'الاردن'    => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
        
'السعودية' => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
        
'ไทย' => array(=> '/^[\x{002d}0-9a-z\x{0E01}-\x{0E3A}\x{0E40}-\x{0E4D}\x{0E50}-\x{0E59}]{1,63}$/iu'),
        
'рф' => array(=> '/^[\x{002d}0-9а-яё]{1,63}$/iu'),
        
'تونس' => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
        
'مصر' => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
        
'இலங்கை' => array(=> '/^[\x{0b80}-\x{0bff}]{1,63}$/iu'),
        
'فلسطين' => array(=> '/^[\x{0621}-\x{0624}\x{0626}-\x{063A}\x{0641}\x{0642}\x{0644}-\x{0648}\x{067E}\x{0686}\x{0698}\x{06A9}\x{06AF}\x{06CC}\x{06F0}-\x{06F9}]{1,30}$/iu'),
    );

    protected 
$idnLength = array(
        
'BIZ' => array(=> 1711 => 1512 => 20),
        
'CN'  => array(=> 20),
        
'COM' => array(=> 17=> 20),
        
'HK'  => array(=> 15),
        
'INFO'=> array(=> 17),
        
'KR'  => array(=> 17),
        
'NET' => array(=> 17=> 20),
        
'ORG' => array(=> 17),
        
'TW'  => array(=> 20),
        
'امارات' => array(=> 30),
        
'الاردن' => array(=> 30),
        
'السعودية' => array(=> 30),
        
'تونس' => array(=> 30),
        
'مصر' => array(=> 30),
        
'فلسطين' => array(=> 30),
        
'中国' => array(=> 20),
        
'中國' => array(=> 20),
        
'香港' => array(=> 20),
        
'台湾' => array(=> 20),
        
'台灣' => array(=> 20),
    );

    protected 
$tld;

    
/**
     * Options for the hostname validator
     *
     * @var array
     */
    
protected $options = array(
        
'allow'       => self::ALLOW_DNS// Allow these hostnames
        
'useIdnCheck' => true,  // Check IDN domains
        
'useTldCheck' => true,  // Check TLD elements
        
'ipValidator' => null,  // IP validator to use
    
);

    
/**
     * Sets validator options
     *
     * @param integer $allow       OPTIONAL Set what types of hostname to allow (default ALLOW_DNS)
     * @param  bool $validateIdn OPTIONAL Set whether IDN domains are validated (default true)
     * @param  bool $validateTld OPTIONAL Set whether the TLD element of a hostname is validated (default true)
     * @param Ip      $ipValidator OPTIONAL
     * @see http://www.iana.org/cctld/specifications-policies-cctlds-01apr02.htm  Technical Specifications for ccTLDs
     */
    
public function __construct($options = array())
    {
        if (!
is_array($options)) {
            
$options func_get_args();
            
$temp['allow'] = array_shift($options);
            if (!empty(
$options)) {
                
$temp['useIdnCheck'] = array_shift($options);
            }

            if (!empty(
$options)) {
                
$temp['useTldCheck'] = array_shift($options);
            }

            if (!empty(
$options)) {
                
$temp['ipValidator'] = array_shift($options);
            }

            
$options $temp;
        }

        if (!
array_key_exists('ipValidator'$options)) {
            
$options['ipValidator'] = null;
        }

        
parent::__construct($options);
    }

    
/**
     * Returns the set ip validator
     *
     * @return Ip
     */
    
public function getIpValidator()
    {
        return 
$this->options['ipValidator'];
    }

    
/**
     * @param Ip $ipValidator OPTIONAL
     * @return Hostname;
     */
    
public function setIpValidator(Ip $ipValidator null)
    {
        if (
$ipValidator === null) {
            
$ipValidator = new Ip();
        }

        
$this->options['ipValidator'] = $ipValidator;
        return 
$this;
    }

    
/**
     * Returns the allow option
     *
     * @return integer
     */
    
public function getAllow()
    {
        return 
$this->options['allow'];
    }

    
/**
     * Sets the allow option
     *
     * @param  integer $allow
     * @return Hostname Provides a fluent interface
     */
    
public function setAllow($allow)
    {
        
$this->options['allow'] = $allow;
        return 
$this;
    }

    
/**
     * Returns the set idn option
     *
     * @return bool
     */
    
public function getIdnCheck()
    {
        return 
$this->options['useIdnCheck'];
    }

    
/**
     * Set whether IDN domains are validated
     *
     * This only applies when DNS hostnames are validated
     *
     * @param  bool $useIdnCheck Set to true to validate IDN domains
     * @return Hostname
     */
    
public function useIdnCheck ($useIdnCheck)
    {
        
$this->options['useIdnCheck'] = (bool) $useIdnCheck;
        return 
$this;
    }

    
/**
     * Returns the set tld option
     *
     * @return bool
     */
    
public function getTldCheck()
    {
        return 
$this->options['useTldCheck'];
    }

    
/**
     * Set whether the TLD element of a hostname is validated
     *
     * This only applies when DNS hostnames are validated
     *
     * @param  bool $useTldCheck Set to true to validate TLD elements
     * @return Hostname
     */
    
public function useTldCheck ($useTldCheck)
    {
        
$this->options['useTldCheck'] = (bool) $useTldCheck;
        return 
$this;
    }

    
/**
     * Defined by Interface
     *
     * Returns true if and only if the $value is a valid hostname with respect to the current allow option
     *
     * @param  string $value
     * @return bool
     */
    
public function isValid($value)
    {
        if (!
is_string($value)) {
            
$this->error(self::INVALID);
            return 
false;
        }

        
$this->setValue($value);
        
// Check input against IP address schema
        
if (preg_match('/^[0-9a-f:.]*$/i'$value) &&
            
$this->getIpValidator()->setTranslator($this->getTranslator())->isValid($value)) {
            if (!(
$this->getAllow() & self::ALLOW_IP)) {
                
$this->error(self::IP_ADDRESS_NOT_ALLOWED);
                return 
false;
            } else {
                return 
true;
            }
        }

        
// Local hostnames are allowed to be partial (ending '.')
        
if ($this->getAllow() & self::ALLOW_LOCAL) {
            if (
substr($value, -1) === '.') {
                
$value substr($value0, -1);
                if (
substr($value, -1) === '.') {
                    
// Empty hostnames (ending '..') are not allowed
                    
$this->error(self::INVALID_LOCAL_NAME);
                    return 
false;
                }
            }
        }

        
$domainParts explode('.'$value);

        
// Prevent partial IP V4 addresses (ending '.')
        
if ((count($domainParts) == 4) && preg_match('/^[0-9.a-e:.]*$/i'$value) &&
            
$this->getIpValidator()->setTranslator($this->getTranslator())->isValid($value)) {
            
$this->error(self::INVALID_LOCAL_NAME);
        }

        
// Check input against DNS hostname schema
        
if ((count($domainParts) > 1) && (strlen($value) >= 4) && (strlen($value) <= 254)) {
            
$utf8StrWrapper StringUtils::getWrapper('UTF-8');
            
$status         false;

            do {
                
// First check TLD
                
$matches = array();
                if (
preg_match('/([^.]{2,10})$/i'end($domainParts), $matches) ||
                    (
array_key_exists(end($domainParts), $this->validIdns))) {
                    
reset($domainParts);

                    
// Hostname characters are: *(label dot)(label dot label); max 254 chars
                    // label: id-prefix [*ldh{61} id-prefix]; max 63 chars
                    // id-prefix: alpha / digit
                    // ldh: alpha / digit / dash

                    // Match TLD against known list
                    
$this->tld strtolower($matches[1]);
                    if (
$this->getTldCheck()) {
                        if (!
in_array($this->tld$this->validTlds)) {
                            
$this->error(self::UNKNOWN_TLD);
                            
$status false;
                            break;
                        }
                    }

                    
/**
                     * Match against IDN hostnames
                     * Note: Keep label regex short to avoid issues with long patterns when matching IDN hostnames
                     * @see Hostname\Interface
                     */
                    
$regexChars = array(=> '/^[a-z0-9\x2d]{1,63}$/i');
                    if (
$this->getIdnCheck() &&  isset($this->validIdns[strtoupper($this->tld)])) {
                        if (
is_string($this->validIdns[strtoupper($this->tld)])) {
                            
$regexChars += include __DIR__ .'/'$this->validIdns[strtoupper($this->tld)];
                        } else {
                            
$regexChars += $this->validIdns[strtoupper($this->tld)];
                        }
                    }

                    
// Check each hostname part
                    
$check 0;
                    foreach (
$domainParts as $domainPart) {
                        
// Decode Punycode domain names to IDN
                        
if (strpos($domainPart'xn--') === 0) {
                            
$domainPart $this->decodePunycode(substr($domainPart4));
                            if (
$domainPart === false) {
                                return 
false;
                            }
                        }

                        
// Check dash (-) does not start, end or appear in 3rd and 4th positions
                        
if ((strpos($domainPart'-') === 0)
                            || ((
strlen($domainPart) > 2) && (strpos($domainPart'-'2) == 2) && (strpos($domainPart'-'3) == 3))
                            || (
strpos($domainPart'-') === (strlen($domainPart) - 1))) {
                            
$this->error(self::INVALID_DASH);
                            
$status false;
                            break 
2;
                        }

                        
// Check each domain part
                        
$checked false;
                        foreach (
$regexChars as $regexKey => $regexChar) {
                            
ErrorHandler::start();
                            
$status preg_match($regexChar$domainPart);
                            
ErrorHandler::stop();
                            if (
$status 0) {
                                
$length 63;
                                if (
array_key_exists(strtoupper($this->tld), $this->idnLength)
                                    && (
array_key_exists($regexKey$this->idnLength[strtoupper($this->tld)]))) {
                                    
$length $this->idnLength[strtoupper($this->tld)];
                                }

                                if (
$utf8StrWrapper->strlen($domainPart) > $length) {
                                    
$this->error(self::INVALID_HOSTNAME);
                                } else {
                                    
$checked true;
                                    break;
                                }
                            }
                        }

                        if (
$checked) {
                            ++
$check;
                        }
                    }

                    
// If one of the labels doesn't match, the hostname is invalid
                    
if ($check !== count($domainParts)) {
                        
$this->error(self::INVALID_HOSTNAME_SCHEMA);
                        
$status false;
                    }
                } else {
                    
// Hostname not long enough
                    
$this->error(self::UNDECIPHERABLE_TLD);
                    
$status false;
                }
            } while (
false);

            
// If the input passes as an Internet domain name, and domain names are allowed, then the hostname
            // passes validation
            
if ($status && ($this->getAllow() & self::ALLOW_DNS)) {
                return 
true;
            }
        } elseif (
$this->getAllow() & self::ALLOW_DNS) {
            
$this->error(self::INVALID_HOSTNAME);
        }

        
// Check for URI Syntax (RFC3986)
        
if ($this->getAllow() & self::ALLOW_URI) {
            if (
preg_match("/^([a-zA-Z0-9-._~!$&\'()*+,;=]|%[[:xdigit:]]{2}){1,254}$/i"$value)) {
                return 
true;
            } else {
                
$this->error(self::INVALID_URI);
            }
        }

        
// Check input against local network name schema; last chance to pass validation
        
ErrorHandler::start();
        
$regexLocal '/^(([a-zA-Z0-9\x2d]{1,63}\x2e)*[a-zA-Z0-9\x2d]{1,63}[\x2e]{0,1}){1,254}$/';
        
$status preg_match($regexLocal$value);
        
ErrorHandler::stop();

        
// If the input passes as a local network name, and local network names are allowed, then the
        // hostname passes validation
        
$allowLocal $this->getAllow() & self::ALLOW_LOCAL;
        if (
$status && $allowLocal) {
            return 
true;
        }

        
// If the input does not pass as a local network name, add a message
        
if (!$status) {
            
$this->error(self::INVALID_LOCAL_NAME);
        }

        
// If local network names are not allowed, add a message
        
if ($status && !$allowLocal) {
            
$this->error(self::LOCAL_NAME_NOT_ALLOWED);
        }

        return 
false;
    }

    
/**
     * Decodes a punycode encoded string to it's original utf8 string
     * In case of a decoding failure the original string is returned
     *
     * @param  string $encoded Punycode encoded string to decode
     * @return string
     */
    
protected function decodePunycode($encoded)
    {
        
$found preg_match('/([^a-z0-9\x2d]{1,10})$/i'$encoded);
        if (empty(
$encoded) || ($found 0)) {
            
// no punycode encoded string, return as is
            
$this->error(self::CANNOT_DECODE_PUNYCODE);
            return 
false;
        }

        
$separator strrpos($encoded'-');
        if (
$separator 0) {
            for (
$x 0$x $separator; ++$x) {
                
// prepare decoding matrix
                
$decoded[] = ord($encoded[$x]);
            }
        } else {
            
$this->error(self::CANNOT_DECODE_PUNYCODE);
            return 
false;
        }

        
$lengthd count($decoded);
        
$lengthe strlen($encoded);

        
// decoding
        
$init  true;
        
$base  72;
        
$index 0;
        
$char  0x80;

        for (
$indexe = ($separator) ? ($separator 1) : 0$indexe $lengthe; ++$lengthd) {
            for (
$oldIndex $index$pos 1$key 361$key += 36) {
                
$hex   ord($encoded[$indexe++]);
                
$digit = ($hex 48 10) ? $hex 22
                       
: (($hex 65 26) ? $hex 65
                       
: (($hex 97 26) ? $hex 97
                       
36));

                
$index += $digit $pos;
                
$tag    = ($key <= $base) ? : (($key >= $base 26) ? 26 : ($key $base));
                if (
$digit $tag) {
                    break;
                }

                
$pos = (int) ($pos * (36 $tag));
            }

            
$delta   intval($init ? (($index $oldIndex) / 700) : (($index $oldIndex) / 2));
            
$delta  += intval($delta / ($lengthd 1));
            for (
$key 0$delta 910 2$key += 36) {
                
$delta intval($delta 35);
            }

            
$base   intval($key 36 $delta / ($delta 38));
            
$init   false;
            
$char  += (int) ($index / ($lengthd 1));
            
$index %= ($lengthd 1);
            if (
$lengthd 0) {
                for (
$i $lengthd$i $index$i--) {
                    
$decoded[$i] = $decoded[($i 1)];
                }
            }

            
$decoded[$index++] = $char;
        }

        
// convert decoded ucs4 to utf8 string
        
foreach ($decoded as $key => $value) {
            if (
$value 128) {
                
$decoded[$key] = chr($value);
            } elseif (
$value < (<< 11)) {
                
$decoded[$key]  = chr(192 + ($value >> 6));
                
$decoded[$key] .= chr(128 + ($value 63));
            } elseif (
$value < (<< 16)) {
                
$decoded[$key]  = chr(224 + ($value >> 12));
                
$decoded[$key] .= chr(128 + (($value >> 6) & 63));
                
$decoded[$key] .= chr(128 + ($value 63));
            } elseif (
$value < (<< 21)) {
                
$decoded[$key]  = chr(240 + ($value >> 18));
                
$decoded[$key] .= chr(128 + (($value >> 12) & 63));
                
$decoded[$key] .= chr(128 + (($value >> 6) & 63));
                
$decoded[$key] .= chr(128 + ($value 63));
            } else {
                
$this->error(self::CANNOT_DECODE_PUNYCODE);
                return 
false;
            }
        }

        return 
implode($decoded);
    }
}