web/lib/Zend/Validate/EmailAddress.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     1 <?php
       
     2 /**
       
     3  * Zend Framework
       
     4  *
       
     5  * LICENSE
       
     6  *
       
     7  * This source file is subject to the new BSD license that is bundled
       
     8  * with this package in the file LICENSE.txt.
       
     9  * It is also available through the world-wide-web at this URL:
       
    10  * http://framework.zend.com/license/new-bsd
       
    11  * If you did not receive a copy of the license and are unable to
       
    12  * obtain it through the world-wide-web, please send an email
       
    13  * to license@zend.com so we can send you a copy immediately.
       
    14  *
       
    15  * @category   Zend
       
    16  * @package    Zend_Validate
       
    17  * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
       
    18  * @license    http://framework.zend.com/license/new-bsd     New BSD License
       
    19  * @version    $Id: EmailAddress.php 22668 2010-07-25 14:50:46Z thomas $
       
    20  */
       
    21 
       
    22 /**
       
    23  * @see Zend_Validate_Abstract
       
    24  */
       
    25 require_once 'Zend/Validate/Abstract.php';
       
    26 
       
    27 /**
       
    28  * @see Zend_Validate_Hostname
       
    29  */
       
    30 require_once 'Zend/Validate/Hostname.php';
       
    31 
       
    32 /**
       
    33  * @category   Zend
       
    34  * @package    Zend_Validate
       
    35  * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
       
    36  * @license    http://framework.zend.com/license/new-bsd     New BSD License
       
    37  */
       
    38 class Zend_Validate_EmailAddress extends Zend_Validate_Abstract
       
    39 {
       
    40     const INVALID            = 'emailAddressInvalid';
       
    41     const INVALID_FORMAT     = 'emailAddressInvalidFormat';
       
    42     const INVALID_HOSTNAME   = 'emailAddressInvalidHostname';
       
    43     const INVALID_MX_RECORD  = 'emailAddressInvalidMxRecord';
       
    44     const INVALID_SEGMENT    = 'emailAddressInvalidSegment';
       
    45     const DOT_ATOM           = 'emailAddressDotAtom';
       
    46     const QUOTED_STRING      = 'emailAddressQuotedString';
       
    47     const INVALID_LOCAL_PART = 'emailAddressInvalidLocalPart';
       
    48     const LENGTH_EXCEEDED    = 'emailAddressLengthExceeded';
       
    49 
       
    50     /**
       
    51      * @var array
       
    52      */
       
    53     protected $_messageTemplates = array(
       
    54         self::INVALID            => "Invalid type given. String expected",
       
    55         self::INVALID_FORMAT     => "'%value%' is no valid email address in the basic format local-part@hostname",
       
    56         self::INVALID_HOSTNAME   => "'%hostname%' is no valid hostname for email address '%value%'",
       
    57         self::INVALID_MX_RECORD  => "'%hostname%' does not appear to have a valid MX record for the email address '%value%'",
       
    58         self::INVALID_SEGMENT    => "'%hostname%' is not in a routable network segment. The email address '%value%' should not be resolved from public network",
       
    59         self::DOT_ATOM           => "'%localPart%' can not be matched against dot-atom format",
       
    60         self::QUOTED_STRING      => "'%localPart%' can not be matched against quoted-string format",
       
    61         self::INVALID_LOCAL_PART => "'%localPart%' is no valid local part for email address '%value%'",
       
    62         self::LENGTH_EXCEEDED    => "'%value%' exceeds the allowed length",
       
    63     );
       
    64 
       
    65     /**
       
    66      * @see http://en.wikipedia.org/wiki/IPv4
       
    67      * @var array
       
    68      */
       
    69     protected $_invalidIp = array(
       
    70         '0'   => '0.0.0.0/8',
       
    71         '10'  => '10.0.0.0/8',
       
    72         '127' => '127.0.0.0/8',
       
    73         '128' => '128.0.0.0/16',
       
    74         '169' => '169.254.0.0/16',
       
    75         '172' => '172.16.0.0/12',
       
    76         '191' => '191.255.0.0/16',
       
    77         '192' => array(
       
    78             '192.0.0.0/24',
       
    79             '192.0.2.0/24',
       
    80             '192.88.99.0/24',
       
    81             '192.168.0.0/16'
       
    82         ),
       
    83         '198' => '198.18.0.0/15',
       
    84         '223' => '223.255.255.0/24',
       
    85         '224' => '224.0.0.0/4',
       
    86         '240' => '240.0.0.0/4'
       
    87     );
       
    88 
       
    89     /**
       
    90      * @var array
       
    91      */
       
    92     protected $_messageVariables = array(
       
    93         'hostname'  => '_hostname',
       
    94         'localPart' => '_localPart'
       
    95     );
       
    96 
       
    97     /**
       
    98      * @var string
       
    99      */
       
   100     protected $_hostname;
       
   101 
       
   102     /**
       
   103      * @var string
       
   104      */
       
   105     protected $_localPart;
       
   106 
       
   107     /**
       
   108      * Internal options array
       
   109      */
       
   110     protected $_options = array(
       
   111         'mx'       => false,
       
   112         'deep'     => false,
       
   113         'domain'   => true,
       
   114         'allow'    => Zend_Validate_Hostname::ALLOW_DNS,
       
   115         'hostname' => null
       
   116     );
       
   117 
       
   118     /**
       
   119      * Instantiates hostname validator for local use
       
   120      *
       
   121      * The following option keys are supported:
       
   122      * 'hostname' => A hostname validator, see Zend_Validate_Hostname
       
   123      * 'allow'    => Options for the hostname validator, see Zend_Validate_Hostname::ALLOW_*
       
   124      * 'mx'       => If MX check should be enabled, boolean
       
   125      * 'deep'     => If a deep MX check should be done, boolean
       
   126      *
       
   127      * @param array|Zend_Config $options OPTIONAL
       
   128      * @return void
       
   129      */
       
   130     public function __construct($options = array())
       
   131     {
       
   132         if ($options instanceof Zend_Config) {
       
   133             $options = $options->toArray();
       
   134         } else if (!is_array($options)) {
       
   135             $options = func_get_args();
       
   136             $temp['allow'] = array_shift($options);
       
   137             if (!empty($options)) {
       
   138                 $temp['mx'] = array_shift($options);
       
   139             }
       
   140 
       
   141             if (!empty($options)) {
       
   142                 $temp['hostname'] = array_shift($options);
       
   143             }
       
   144 
       
   145             $options = $temp;
       
   146         }
       
   147 
       
   148         $options += $this->_options;
       
   149         $this->setOptions($options);
       
   150     }
       
   151 
       
   152     /**
       
   153      * Returns all set Options
       
   154      *
       
   155      * @return array
       
   156      */
       
   157     public function getOptions()
       
   158     {
       
   159         return $this->_options;
       
   160     }
       
   161 
       
   162     /**
       
   163      * Set options for the email validator
       
   164      *
       
   165      * @param array $options
       
   166      * @return Zend_Validate_EmailAddress fluid interface
       
   167      */
       
   168     public function setOptions(array $options = array())
       
   169     {
       
   170         if (array_key_exists('messages', $options)) {
       
   171             $this->setMessages($options['messages']);
       
   172         }
       
   173 
       
   174         if (array_key_exists('hostname', $options)) {
       
   175             if (array_key_exists('allow', $options)) {
       
   176                 $this->setHostnameValidator($options['hostname'], $options['allow']);
       
   177             } else {
       
   178                 $this->setHostnameValidator($options['hostname']);
       
   179             }
       
   180         }
       
   181 
       
   182         if (array_key_exists('mx', $options)) {
       
   183             $this->setValidateMx($options['mx']);
       
   184         }
       
   185 
       
   186         if (array_key_exists('deep', $options)) {
       
   187             $this->setDeepMxCheck($options['deep']);
       
   188         }
       
   189 
       
   190         if (array_key_exists('domain', $options)) {
       
   191             $this->setDomainCheck($options['domain']);
       
   192         }
       
   193 
       
   194         return $this;
       
   195     }
       
   196 
       
   197     /**
       
   198      * Sets the validation failure message template for a particular key
       
   199      * Adds the ability to set messages to the attached hostname validator
       
   200      *
       
   201      * @param  string $messageString
       
   202      * @param  string $messageKey     OPTIONAL
       
   203      * @return Zend_Validate_Abstract Provides a fluent interface
       
   204      * @throws Zend_Validate_Exception
       
   205      */
       
   206     public function setMessage($messageString, $messageKey = null)
       
   207     {
       
   208         $messageKeys = $messageKey;
       
   209         if ($messageKey === null) {
       
   210             $keys = array_keys($this->_messageTemplates);
       
   211             $messageKeys = current($keys);
       
   212         }
       
   213 
       
   214         if (!isset($this->_messageTemplates[$messageKeys])) {
       
   215             $this->_options['hostname']->setMessage($messageString, $messageKey);
       
   216         }
       
   217 
       
   218         $this->_messageTemplates[$messageKeys] = $messageString;
       
   219         return $this;
       
   220     }
       
   221 
       
   222     /**
       
   223      * Returns the set hostname validator
       
   224      *
       
   225      * @return Zend_Validate_Hostname
       
   226      */
       
   227     public function getHostnameValidator()
       
   228     {
       
   229         return $this->_options['hostname'];
       
   230     }
       
   231 
       
   232     /**
       
   233      * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL
       
   234      * @param int                    $allow             OPTIONAL
       
   235      * @return void
       
   236      */
       
   237     public function setHostnameValidator(Zend_Validate_Hostname $hostnameValidator = null, $allow = Zend_Validate_Hostname::ALLOW_DNS)
       
   238     {
       
   239         if (!$hostnameValidator) {
       
   240             $hostnameValidator = new Zend_Validate_Hostname($allow);
       
   241         }
       
   242 
       
   243         $this->_options['hostname'] = $hostnameValidator;
       
   244         $this->_options['allow']    = $allow;
       
   245         return $this;
       
   246     }
       
   247 
       
   248     /**
       
   249      * Whether MX checking via getmxrr is supported or not
       
   250      *
       
   251      * This currently only works on UNIX systems
       
   252      *
       
   253      * @return boolean
       
   254      */
       
   255     public function validateMxSupported()
       
   256     {
       
   257         return function_exists('getmxrr');
       
   258     }
       
   259 
       
   260     /**
       
   261      * Returns the set validateMx option
       
   262      *
       
   263      * @return boolean
       
   264      */
       
   265     public function getValidateMx()
       
   266     {
       
   267         return $this->_options['mx'];
       
   268     }
       
   269 
       
   270     /**
       
   271      * Set whether we check for a valid MX record via DNS
       
   272      *
       
   273      * This only applies when DNS hostnames are validated
       
   274      *
       
   275      * @param boolean $mx Set allowed to true to validate for MX records, and false to not validate them
       
   276      * @return Zend_Validate_EmailAddress Fluid Interface
       
   277      */
       
   278     public function setValidateMx($mx)
       
   279     {
       
   280         if ((bool) $mx && !$this->validateMxSupported()) {
       
   281             require_once 'Zend/Validate/Exception.php';
       
   282             throw new Zend_Validate_Exception('MX checking not available on this system');
       
   283         }
       
   284 
       
   285         $this->_options['mx'] = (bool) $mx;
       
   286         return $this;
       
   287     }
       
   288 
       
   289     /**
       
   290      * Returns the set deepMxCheck option
       
   291      *
       
   292      * @return boolean
       
   293      */
       
   294     public function getDeepMxCheck()
       
   295     {
       
   296         return $this->_options['deep'];
       
   297     }
       
   298 
       
   299     /**
       
   300      * Set whether we check MX record should be a deep validation
       
   301      *
       
   302      * @param boolean $deep Set deep to true to perform a deep validation process for MX records
       
   303      * @return Zend_Validate_EmailAddress Fluid Interface
       
   304      */
       
   305     public function setDeepMxCheck($deep)
       
   306     {
       
   307         $this->_options['deep'] = (bool) $deep;
       
   308         return $this;
       
   309     }
       
   310 
       
   311     /**
       
   312      * Returns the set domainCheck option
       
   313      *
       
   314      * @return unknown
       
   315      */
       
   316     public function getDomainCheck()
       
   317     {
       
   318         return $this->_options['domain'];
       
   319     }
       
   320 
       
   321     /**
       
   322      * Sets if the domain should also be checked
       
   323      * or only the local part of the email address
       
   324      *
       
   325      * @param boolean $domain
       
   326      * @return Zend_Validate_EmailAddress Fluid Interface
       
   327      */
       
   328     public function setDomainCheck($domain = true)
       
   329     {
       
   330         $this->_options['domain'] = (boolean) $domain;
       
   331         return $this;
       
   332     }
       
   333 
       
   334     /**
       
   335      * Returns if the given host is reserved
       
   336      *
       
   337      * @param string $host
       
   338      * @return boolean
       
   339      */
       
   340     private function _isReserved($host){
       
   341         if (!preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $host)) {
       
   342             $host = gethostbyname($host);
       
   343         }
       
   344 
       
   345         $octet = explode('.',$host);
       
   346         if ((int)$octet[0] >= 224) {
       
   347             return true;
       
   348         } else if (array_key_exists($octet[0], $this->_invalidIp)) {
       
   349             foreach ((array)$this->_invalidIp[$octet[0]] as $subnetData) {
       
   350                 // we skip the first loop as we already know that octet matches
       
   351                 for ($i = 1; $i < 4; $i++) {
       
   352                     if (strpos($subnetData, $octet[$i]) !== $i * 4) {
       
   353                         break;
       
   354                     }
       
   355                 }
       
   356 
       
   357                 $host       = explode("/", $subnetData);
       
   358                 $binaryHost = "";
       
   359                 $tmp        = explode(".", $host[0]);
       
   360                 for ($i = 0; $i < 4 ; $i++) {
       
   361                     $binaryHost .= str_pad(decbin($tmp[$i]), 8, "0", STR_PAD_LEFT);
       
   362                 }
       
   363 
       
   364                 $segmentData = array(
       
   365                     'network'   => (int)$this->_toIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 0)),
       
   366                     'broadcast' => (int)$this->_toIp(str_pad(substr($binaryHost, 0, $host[1]), 32, 1))
       
   367                 );
       
   368 
       
   369                 for ($j = $i; $j < 4; $j++) {
       
   370                     if ((int)$octet[$j] < $segmentData['network'][$j] ||
       
   371                         (int)$octet[$j] > $segmentData['broadcast'][$j]) {
       
   372                         return false;
       
   373                     }
       
   374                 }
       
   375             }
       
   376 
       
   377             return true;
       
   378         } else {
       
   379             return false;
       
   380         }
       
   381     }
       
   382 
       
   383     /**
       
   384      * Converts a binary string to an IP address
       
   385      *
       
   386      * @param string $binary
       
   387      * @return mixed
       
   388      */
       
   389     private function _toIp($binary)
       
   390     {
       
   391         $ip  = array();
       
   392         $tmp = explode(".", chunk_split($binary, 8, "."));
       
   393         for ($i = 0; $i < 4 ; $i++) {
       
   394             $ip[$i] = bindec($tmp[$i]);
       
   395         }
       
   396 
       
   397         return $ip;
       
   398     }
       
   399 
       
   400     /**
       
   401      * Internal method to validate the local part of the email address
       
   402      *
       
   403      * @return boolean
       
   404      */
       
   405     private function _validateLocalPart()
       
   406     {
       
   407         // First try to match the local part on the common dot-atom format
       
   408         $result = false;
       
   409 
       
   410         // Dot-atom characters are: 1*atext *("." 1*atext)
       
   411         // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*",
       
   412         //        "+", "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~"
       
   413         $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d\x7e';
       
   414         if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->_localPart)) {
       
   415             $result = true;
       
   416         } else {
       
   417             // Try quoted string format
       
   418 
       
   419             // Quoted-string characters are: DQUOTE *([FWS] qtext/quoted-pair) [FWS] DQUOTE
       
   420             // qtext: Non white space controls, and the rest of the US-ASCII characters not
       
   421             //   including "\" or the quote character
       
   422             $noWsCtl = '\x01-\x08\x0b\x0c\x0e-\x1f\x7f';
       
   423             $qtext   = $noWsCtl . '\x21\x23-\x5b\x5d-\x7e';
       
   424             $ws      = '\x20\x09';
       
   425             if (preg_match('/^\x22([' . $ws . $qtext . '])*[$ws]?\x22$/', $this->_localPart)) {
       
   426                 $result = true;
       
   427             } else {
       
   428                 $this->_error(self::DOT_ATOM);
       
   429                 $this->_error(self::QUOTED_STRING);
       
   430                 $this->_error(self::INVALID_LOCAL_PART);
       
   431             }
       
   432         }
       
   433 
       
   434         return $result;
       
   435     }
       
   436 
       
   437     /**
       
   438      * Internal method to validate the servers MX records
       
   439      *
       
   440      * @return boolean
       
   441      */
       
   442     private function _validateMXRecords()
       
   443     {
       
   444         $mxHosts = array();
       
   445         $result = getmxrr($this->_hostname, $mxHosts);
       
   446         if (!$result) {
       
   447             $this->_error(self::INVALID_MX_RECORD);
       
   448         } else if ($this->_options['deep'] && function_exists('checkdnsrr')) {
       
   449             $validAddress = false;
       
   450             $reserved     = true;
       
   451             foreach ($mxHosts as $hostname) {
       
   452                 $res = $this->_isReserved($hostname);
       
   453                 if (!$res) {
       
   454                     $reserved = false;
       
   455                 }
       
   456 
       
   457                 if (!$res
       
   458                     && (checkdnsrr($hostname, "A")
       
   459                     || checkdnsrr($hostname, "AAAA")
       
   460                     || checkdnsrr($hostname, "A6"))) {
       
   461                     $validAddress = true;
       
   462                     break;
       
   463                 }
       
   464             }
       
   465 
       
   466             if (!$validAddress) {
       
   467                 $result = false;
       
   468                 if ($reserved) {
       
   469                     $this->_error(self::INVALID_SEGMENT);
       
   470                 } else {
       
   471                     $this->_error(self::INVALID_MX_RECORD);
       
   472                 }
       
   473             }
       
   474         }
       
   475 
       
   476         return $result;
       
   477     }
       
   478 
       
   479     /**
       
   480      * Internal method to validate the hostname part of the email address
       
   481      *
       
   482      * @return boolean
       
   483      */
       
   484     private function _validateHostnamePart()
       
   485     {
       
   486         $hostname = $this->_options['hostname']->setTranslator($this->getTranslator())
       
   487                          ->isValid($this->_hostname);
       
   488         if (!$hostname) {
       
   489             $this->_error(self::INVALID_HOSTNAME);
       
   490 
       
   491             // Get messages and errors from hostnameValidator
       
   492             foreach ($this->_options['hostname']->getMessages() as $code => $message) {
       
   493                 $this->_messages[$code] = $message;
       
   494             }
       
   495 
       
   496             foreach ($this->_options['hostname']->getErrors() as $error) {
       
   497                 $this->_errors[] = $error;
       
   498             }
       
   499         } else if ($this->_options['mx']) {
       
   500             // MX check on hostname
       
   501             $hostname = $this->_validateMXRecords();
       
   502         }
       
   503 
       
   504         return $hostname;
       
   505     }
       
   506 
       
   507     /**
       
   508      * Defined by Zend_Validate_Interface
       
   509      *
       
   510      * Returns true if and only if $value is a valid email address
       
   511      * according to RFC2822
       
   512      *
       
   513      * @link   http://www.ietf.org/rfc/rfc2822.txt RFC2822
       
   514      * @link   http://www.columbia.edu/kermit/ascii.html US-ASCII characters
       
   515      * @param  string $value
       
   516      * @return boolean
       
   517      */
       
   518     public function isValid($value)
       
   519     {
       
   520         if (!is_string($value)) {
       
   521             $this->_error(self::INVALID);
       
   522             return false;
       
   523         }
       
   524 
       
   525         $matches = array();
       
   526         $length  = true;
       
   527         $this->_setValue($value);
       
   528 
       
   529         // Split email address up and disallow '..'
       
   530         if ((strpos($value, '..') !== false) or
       
   531             (!preg_match('/^(.+)@([^@]+)$/', $value, $matches))) {
       
   532             $this->_error(self::INVALID_FORMAT);
       
   533             return false;
       
   534         }
       
   535 
       
   536         $this->_localPart = $matches[1];
       
   537         $this->_hostname  = $matches[2];
       
   538 
       
   539         if ((strlen($this->_localPart) > 64) || (strlen($this->_hostname) > 255)) {
       
   540             $length = false;
       
   541             $this->_error(self::LENGTH_EXCEEDED);
       
   542         }
       
   543 
       
   544         // Match hostname part
       
   545         if ($this->_options['domain']) {
       
   546             $hostname = $this->_validateHostnamePart();
       
   547         }
       
   548 
       
   549         $local = $this->_validateLocalPart();
       
   550 
       
   551         // If both parts valid, return true
       
   552         if ($local && $length) {
       
   553             if (($this->_options['domain'] && $hostname) || !$this->_options['domain']) {
       
   554                 return true;
       
   555             }
       
   556         }
       
   557 
       
   558         return false;
       
   559     }
       
   560 }