fix image text
[pear] / Validate.php
1 <?php
2 /**
3  * Validation class
4  *
5  * Copyright (c) 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox, Amir Saied
6  *
7  * This source file is subject to the New BSD license, That is bundled
8  * with this package in the file LICENSE, and is available through
9  * the world-wide-web at
10  * http://www.opensource.org/licenses/bsd-license.php
11  * If you did not receive a copy of the new BSDlicense and are unable
12  * to obtain it through the world-wide-web, please send a note to
13  * pajoye@php.net so we can mail you a copy immediately.
14  *
15  * Author: Tomas V.V.Cox  <cox@idecnet.com>
16  *         Pierre-Alain Joye <pajoye@php.net>
17  *         Amir Mohammad Saied <amir@php.net>
18  *
19  *
20  * Package to validate various datas. It includes :
21  *   - numbers (min/max, decimal or not)
22  *   - email (syntax, domain check)
23  *   - string (predifined type alpha upper and/or lowercase, numeric,...)
24  *   - date (min, max, rfc822 compliant)
25  *   - uri (RFC2396)
26  *   - possibility valid multiple data with a single method call (::multiple)
27  *
28  * @category   Validate
29  * @package    Validate
30  * @author     Tomas V.V.Cox <cox@idecnet.com>
31  * @author     Pierre-Alain Joye <pajoye@php.net>
32  * @author     Amir Mohammad Saied <amir@php.net>
33  * @copyright  1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
34  * @license    http://www.opensource.org/licenses/bsd-license.php  New BSD License
35  * @version    CVS: $Id: Validate.php 302518 2010-08-20 01:58:15Z clockwerx $
36  * @link       http://pear.php.net/package/Validate
37  */
38
39 // {{{ Constants
40 /**
41  * Methods for common data validations
42  */
43 define('VALIDATE_NUM',          '0-9');
44 define('VALIDATE_SPACE',        '\s');
45 define('VALIDATE_ALPHA_LOWER',  'a-z');
46 define('VALIDATE_ALPHA_UPPER',  'A-Z');
47 define('VALIDATE_ALPHA',        VALIDATE_ALPHA_LOWER . VALIDATE_ALPHA_UPPER);
48 define('VALIDATE_EALPHA_LOWER', VALIDATE_ALPHA_LOWER . 'áéíóúýàèìòùäëïöüÿâêîôûãñõ¨åæç½ðøþß');
49 define('VALIDATE_EALPHA_UPPER', VALIDATE_ALPHA_UPPER . 'ÁÉÍÓÚÝÀÈÌÒÙÄËÏÖܾÂÊÎÔÛÃÑÕ¦ÅÆǼÐØÞ');
50 define('VALIDATE_EALPHA',       VALIDATE_EALPHA_LOWER . VALIDATE_EALPHA_UPPER);
51 define('VALIDATE_PUNCTUATION',  VALIDATE_SPACE . '\.,;\:&"\'\?\!\(\)');
52 define('VALIDATE_NAME',         VALIDATE_EALPHA . VALIDATE_SPACE . "'" . '\-');
53 define('VALIDATE_STREET',       VALIDATE_NUM . VALIDATE_NAME . "/\\ºª\.");
54
55 define('VALIDATE_ITLD_EMAILS',  1);
56 define('VALIDATE_GTLD_EMAILS',  2);
57 define('VALIDATE_CCTLD_EMAILS', 4);
58 define('VALIDATE_ALL_EMAILS',   8);
59 // }}}
60
61 /**
62  * Validation class
63  *
64  * Package to validate various datas. It includes :
65  *   - numbers (min/max, decimal or not)
66  *   - email (syntax, domain check)
67  *   - string (predifined type alpha upper and/or lowercase, numeric,...)
68  *   - date (min, max)
69  *   - uri (RFC2396)
70  *   - possibility valid multiple data with a single method call (::multiple)
71  *
72  * @category  Validate
73  * @package   Validate
74  * @author    Tomas V.V.Cox <cox@idecnet.com>
75  * @author    Pierre-Alain Joye <pajoye@php.net>
76  * @author    Amir Mohammad Saied <amir@php.net>
77  * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
78  * @license   http://www.opensource.org/licenses/bsd-license.php  New BSD License
79  * @version   Release: @package_version@
80  * @link      http://pear.php.net/package/Validate
81  */
82 class Validate
83 {
84     // {{{ International, Generic and Country code TLDs
85     /**
86      * International Top-Level Domain
87      *
88      * This is an array of the known international
89      * top-level domain names.
90      *
91      * @access protected
92      * @var    array     $_iTld (International top-level domains)
93      */
94     var $_itld = array(
95         'arpa',
96         'root',
97     );
98
99     /**
100      * Generic top-level domain
101      *
102      * This is an array of the official
103      * generic top-level domains.
104      *
105      * @access protected
106      * @var    array     $_gTld (Generic top-level domains)
107      */
108     var $_gtld = array(
109         'aero',
110         'biz',
111         'cat',
112         'com',
113         'coop',
114         'edu',
115         'gov',
116         'info',
117         'int',
118         'jobs',
119         'mil',
120         'mobi',
121         'museum',
122         'name',
123         'net',
124         'org',
125         'pro',
126         'travel',
127         'asia',
128         'post',
129         'tel',
130         'geo',
131     );
132
133     /**
134      * Country code top-level domains
135      *
136      * This is an array of the official country
137      * codes top-level domains
138      *
139      * @access protected
140      * @var    array     $_ccTld (Country Code Top-Level Domain)
141      */
142     var $_cctld = array(
143         'ac',
144         'ad','ae','af','ag',
145         'ai','al','am','an',
146         'ao','aq','ar','as',
147         'at','au','aw','ax',
148         'az','ba','bb','bd',
149         'be','bf','bg','bh',
150         'bi','bj','bm','bn',
151         'bo','br','bs','bt',
152         'bu','bv','bw','by',
153         'bz','ca','cc','cd',
154         'cf','cg','ch','ci',
155         'ck','cl','cm','cn',
156         'co','cr','cs','cu',
157         'cv','cx','cy','cz',
158         'de','dj','dk','dm',
159         'do','dz','ec','ee',
160         'eg','eh','er','es',
161         'et','eu','fi','fj',
162         'fk','fm','fo','fr',
163         'ga','gb','gd','ge',
164         'gf','gg','gh','gi',
165         'gl','gm','gn','gp',
166         'gq','gr','gs','gt',
167         'gu','gw','gy','hk',
168         'hm','hn','hr','ht',
169         'hu','id','ie','il',
170         'im','in','io','iq',
171         'ir','is','it','je',
172         'jm','jo','jp','ke',
173         'kg','kh','ki','km',
174         'kn','kp','kr','kw',
175         'ky','kz','la','lb',
176         'lc','li','lk','lr',
177         'ls','lt','lu','lv',
178         'ly','ma','mc','md',
179         'me','mg','mh','mk',
180         'ml','mm','mn','mo',
181         'mp','mq','mr','ms',
182         'mt','mu','mv','mw',
183         'mx','my','mz','na',
184         'nc','ne','nf','ng',
185         'ni','nl','no','np',
186         'nr','nu','nz','om',
187         'pa','pe','pf','pg',
188         'ph','pk','pl','pm',
189         'pn','pr','ps','pt',
190         'pw','py','qa','re',
191         'ro','rs','ru','rw',
192         'sa','sb','sc','sd',
193         'se','sg','sh','si',
194         'sj','sk','sl','sm',
195         'sn','so','sr','st',
196         'su','sv','sy','sz',
197         'tc','td','tf','tg',
198         'th','tj','tk','tl',
199         'tm','tn','to','tp',
200         'tr','tt','tv','tw',
201         'tz','ua','ug','uk',
202         'us','uy','uz','va',
203         'vc','ve','vg','vi',
204         'vn','vu','wf','ws',
205         'ye','yt','yu','za',
206         'zm','zw',
207     );
208     // }}}
209
210     /**
211      * Validate a tag URI (RFC4151)
212      *
213      * @param string $uri tag URI to validate
214      *
215      * @return boolean true if valid tag URI, false if not
216      *
217      * @access private
218      */
219     static function __uriRFC4151($uri)
220     {
221         $datevalid = false;
222         if (preg_match(
223             '/^tag:(?<name>.*),(?<date>\d{4}-?\d{0,2}-?\d{0,2}):(?<specific>.*)(.*:)*$/', $uri, $matches)) {
224             $date  = $matches['date'];
225             $date6 = strtotime($date);
226             if ((strlen($date) == 4) && $date <= date('Y')) {
227                 $datevalid = true;
228             } elseif ((strlen($date) == 7) && ($date6 < strtotime("now"))) {
229                 $datevalid = true;
230             } elseif ((strlen($date) == 10) && ($date6 < strtotime("now"))) {
231                 $datevalid = true;
232             }
233             if (self::email($matches['name'])) {
234                 $namevalid = true;
235             } else {
236                 $namevalid = self::email('info@' . $matches['name']);
237             }
238             return $datevalid && $namevalid;
239         } else {
240             return false;
241         }
242     }
243
244     /**
245      * Validate a number
246      *
247      * @param string $number  Number to validate
248      * @param array  $options array where:
249      *                          'decimal'  is the decimal char or false when decimal
250      *                                     not allowed.
251      *                                     i.e. ',.' to allow both ',' and '.'
252      *                          'dec_prec' Number of allowed decimals
253      *                          'min'      minimum value
254      *                          'max'      maximum value
255      *
256      * @return boolean true if valid number, false if not
257      *
258      * @access public
259      */
260     static function number($number, $options = array())
261     {
262         $decimal = $dec_prec = $min = $max = null;
263         if (is_array($options)) {
264             extract($options);
265         }
266
267         $dec_prec  = $dec_prec ? "{1,$dec_prec}" : '+';
268         $dec_regex = $decimal  ? "[$decimal][0-9]$dec_prec" : '';
269
270         if (!preg_match("|^[-+]?\s*[0-9]+($dec_regex)?\$|", $number)) {
271             return false;
272         }
273
274         if ($decimal != '.') {
275             $number = strtr($number, $decimal, '.');
276         }
277
278         $number = (float)str_replace(' ', '', $number);
279         if ($min !== null && $min > $number) {
280             return false;
281         }
282
283         if ($max !== null && $max < $number) {
284             return false;
285         }
286         return true;
287     }
288
289     /**
290      * Converting a string to UTF-7 (RFC 2152)
291      *
292      * @param string $string string to be converted
293      *
294      * @return  string  converted string
295      *
296      * @access  private
297      */
298     static function __stringToUtf7($string)
299     {
300         $return = '';
301         $utf7   = array(
302                         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
303                         'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
304                         'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
305                         'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
306                         's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
307                         '3', '4', '5', '6', '7', '8', '9', '+', ','
308                     );
309
310
311         $state = 0;
312
313         if (!empty($string)) {
314             $i = 0;
315             while ($i <= strlen($string)) {
316                 $char = substr($string, $i, 1);
317                 if ($state == 0) {
318                     if ((ord($char) >= 0x7F) || (ord($char) <= 0x1F)) {
319                         if ($char) {
320                             $return .= '&';
321                         }
322                         $state = 1;
323                     } elseif ($char == '&') {
324                         $return .= '&-';
325                     } else {
326                         $return .= $char;
327                     }
328                 } elseif (($i == strlen($string) ||
329                             !((ord($char) >= 0x7F)) || (ord($char) <= 0x1F))) {
330                     if ($state != 1) {
331                         if (ord($char) > 64) {
332                             $return .= '';
333                         } else {
334                             $return .= $utf7[ord($char)];
335                         }
336                     }
337                     $return .= '-';
338                     $state   = 0;
339                 } else {
340                     switch($state) {
341                     case 1:
342                         $return .= $utf7[ord($char) >> 2];
343                         $residue = (ord($char) & 0x03) << 4;
344                         $state   = 2;
345                         break;
346                     case 2:
347                         $return .= $utf7[$residue | (ord($char) >> 4)];
348                         $residue = (ord($char) & 0x0F) << 2;
349                         $state   = 3;
350                         break;
351                     case 3:
352                         $return .= $utf7[$residue | (ord($char) >> 6)];
353                         $return .= $utf7[ord($char) & 0x3F];
354                         $state   = 1;
355                         break;
356                     }
357                 }
358                 $i++;
359             }
360             return $return;
361         }
362         return '';
363     }
364
365     /**
366      * Validate an email according to full RFC822 (inclusive human readable part)
367      *
368      * @param string $email   email to validate,
369      *                        will return the address for optional dns validation
370      * @param array  $options email() options
371      *
372      * @return boolean true if valid email, false if not
373      *
374      * @access private
375      */
376     static function __emailRFC822(&$email, &$options)
377     {
378         static $address   = null;
379         static $uncomment = null;
380         if (!$address) {
381             // atom        =  1*<any CHAR except specials, SPACE and CTLs>
382             $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*';
383             // qtext       =  <any CHAR excepting <">,     ; => may be folded
384             //         "\" & CR, and including linear-white-space>
385             $qtext = '[^"\\\\\r]';
386             // quoted-pair =  "\" CHAR                     ; may quote any char
387             $quoted_pair = '\\\\.';
388             // quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
389             //                                             ;   quoted chars.
390             $quoted_string = '"(?:' . $qtext . '|' . $quoted_pair . ')*"\s*';
391             // word        =  atom / quoted-string
392             $word = '(?:' . $atom . '|' . $quoted_string . ')';
393             // local-part  =  word *("." word)             ; uninterpreted
394             //                                             ; case-preserved
395             $local_part = $word . '(?:\.\s*' . $word . ')*';
396             // dtext       =  <any CHAR excluding "[",     ; => may be folded
397             //         "]", "\" & CR, & including linear-white-space>
398             $dtext = '[^][\\\\\r]';
399             // domain-literal =  "[" *(dtext / quoted-pair) "]"
400             $domain_literal = '\[(?:' . $dtext . '|' . $quoted_pair . ')*\]\s*';
401             // sub-domain  =  domain-ref / domain-literal
402             // domain-ref  =  atom                         ; symbolic reference
403             $sub_domain = '(?:' . $atom . '|' . $domain_literal . ')';
404             // domain      =  sub-domain *("." sub-domain)
405             $domain = $sub_domain . '(?:\.\s*' . $sub_domain . ')*';
406             // addr-spec   =  local-part "@" domain        ; global address
407             $addr_spec = $local_part . '@\s*' . $domain;
408             // route       =  1#("@" domain) ":"           ; path-relative
409             $route = '@' . $domain . '(?:,@\s*' . $domain . ')*:\s*';
410             // route-addr  =  "<" [route] addr-spec ">"
411             $route_addr = '<\s*(?:' . $route . ')?' . $addr_spec . '>\s*';
412             // phrase      =  1*word                       ; Sequence of words
413             $phrase = $word  . '+';
414             // mailbox     =  addr-spec                    ; simple address
415             //             /  phrase route-addr            ; name & addr-spec
416             $mailbox = '(?:' . $addr_spec . '|' . $phrase . $route_addr . ')';
417             // group       =  phrase ":" [#mailbox] ";"
418             $group = $phrase . ':\s*(?:' . $mailbox . '(?:,\s*' . $mailbox . ')*)?;\s*';
419             //     address     =  mailbox                      ; one addressee
420             //                 /  group                        ; named list
421             $address = '/^\s*(?:' . $mailbox . '|' . $group . ')$/';
422
423             $uncomment =
424             '/((?:(?:\\\\"|[^("])*(?:' . $quoted_string .
425                                              ')?)*)((?<!\\\\)\((?:(?2)|.)*?(?<!\\\\)\))/';
426         }
427         // strip comments
428         $email = preg_replace($uncomment, '$1 ', $email);
429         return preg_match($address, $email);
430     }
431
432     /**
433      * Full TLD Validation function
434      *
435      * This function is used to make a much more proficient validation
436      * against all types of official domain names.
437      *
438      * @param string $email   The email address to check.
439      * @param array  $options The options for validation
440      *
441      * @access protected
442      *
443      * @return bool True if validating succeeds
444      */
445     static function _fullTLDValidation($email, $options)
446     {
447         $validate = array();
448         if(!empty($options["VALIDATE_ITLD_EMAILS"])) array_push($validate, 'itld');
449         if(!empty($options["VALIDATE_GTLD_EMAILS"])) array_push($validate, 'gtld');
450         if(!empty($options["VALIDATE_CCTLD_EMAILS"])) array_push($validate, 'cctld');
451
452         $self = new Validate;
453
454         $toValidate = array();
455
456         foreach ($validate as $valid) {
457             $tmpVar = '_' . (string)$valid;
458
459             $toValidate[$valid] = $self->{$tmpVar};
460         }
461
462         $e = $self->executeFullEmailValidation($email, $toValidate);
463
464         return $e;
465     }
466
467     /**
468      * Execute the validation
469      *
470      * This function will execute the full email vs tld
471      * validation using an array of tlds passed to it.
472      *
473      * @param string $email       The email to validate.
474      * @param array  $arrayOfTLDs The array of the TLDs to validate
475      *
476      * @access public
477      *
478      * @return true or false (Depending on if it validates or if it does not)
479      */
480     static function executeFullEmailValidation($email, $arrayOfTLDs)
481     {
482         $emailEnding = explode('.', $email);
483         $emailEnding = $emailEnding[count($emailEnding)-1];
484         foreach ($arrayOfTLDs as $validator => $keys) {
485             if (in_array($emailEnding, $keys)) {
486                 return true;
487             }
488         }
489         return false;
490     }
491
492     /**
493      * Validate an email
494      *
495      * @param string $email  email to validate
496      * @param mixed  boolean (BC) $check_domain Check or not if the domain exists
497      *              array $options associative array of options
498      *              'check_domain' boolean Check or not if the domain exists
499      *              'use_rfc822' boolean Apply the full RFC822 grammar
500      *
501      * Ex.
502      *  $options = array(
503      *      'check_domain' => 'true',
504      *      'fullTLDValidation' => 'true',
505      *      'use_rfc822' => 'true',
506      *      'VALIDATE_GTLD_EMAILS' => 'true',
507      *      'VALIDATE_CCTLD_EMAILS' => 'true',
508      *      'VALIDATE_ITLD_EMAILS' => 'true',
509      *      );
510      *
511      * @return boolean true if valid email, false if not
512      *
513      * @access public
514      */
515     static  function email($email, $options = null)
516     {
517         static $dom_cache = array();
518         $check_domain = false;
519         $use_rfc822   = false;
520         if (is_bool($options)) {
521             $check_domain = $options;
522         } elseif (is_array($options)) {
523             extract($options);
524         }
525
526         /**
527          * Check for IDN usage so we can encode the domain as Punycode
528          * before continuing.
529          */
530         $hasIDNA = false;
531
532         if (Validate::_includePathFileExists('Net/IDNA.php')) {
533             include_once('Net/IDNA.php');
534             $hasIDNA = true;
535         }
536
537         if ($hasIDNA === true) {
538             if (strpos($email, '@') !== false) {
539                 $tmpEmail = explode('@', $email);
540                 $domain = array_pop($tmpEmail);
541
542                 // Check if the domain contains characters > 127 which means
543                 // it's an idn domain name.
544                 $chars = count_chars($domain, 1);
545                 if (!empty($chars) && max(array_keys($chars)) > 127) {
546                     $idna   =& Net_IDNA::singleton();
547                     $domain = $idna->encode($domain);
548                 }
549
550                 array_push($tmpEmail, $domain);
551                 $email = implode('@', $tmpEmail);
552             }
553         }
554
555         /**
556          * @todo Fix bug here.. even if it passes this, it won't be passing
557          *       The regular expression below
558          */
559         if (isset($fullTLDValidation)) {
560             //$valid = Validate::_fullTLDValidation($email, $fullTLDValidation);
561             $valid = Validate::_fullTLDValidation($email, $options);
562
563             if (!$valid) {
564                 return false;
565             }
566         }
567
568         // the base regexp for address
569         $regex = '&^(?:                                               # recipient:
570          ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                          #1 quoted name
571          ([-\w!\#\$%\&\'*+~/^`|{}]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}]+)*)) #2 OR dot-atom
572          @(((\[)?                     #3 domain, 4 as IPv4, 5 optionally bracketed
573          (?:(?:(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))\.){3}
574                (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))))(?(5)\])|
575          ((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)  #6 domain as hostname
576          \.((?:([^- ])[-a-z]*[-a-z]))) #7 TLD
577          $&xi';
578
579         //checks if exists the domain (MX or A)
580         if ($use_rfc822? Validate::__emailRFC822($email, $options) :
581                 preg_match($regex, $email)) {
582             if ($check_domain && function_exists('checkdnsrr')) {
583                 $em = explode('@', $email);
584                 $domain = preg_replace('/[^-a-z.0-9]/i', '', array_pop($em));
585                 
586                 if (isset($dom_cache[$domain])) {
587                     return $dom_cache[$domain];
588                 }
589                 
590                 if (checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A')) {
591                     $dom_cache[$domain] = true;
592                     return true;
593                 }
594                 $dom_cache[$domain] = false;
595                 return false;
596             }
597             return true;
598         }
599         return false;
600     }
601
602     /**
603      * Validate a string using the given format 'format'
604      *
605      * @param string $string  String to validate
606      * @param array  $options Options array where:
607      *                          'format' is the format of the string
608      *                              Ex:VALIDATE_NUM . VALIDATE_ALPHA (see constants)
609      *                          'min_length' minimum length
610      *                          'max_length' maximum length
611      *
612      * @return boolean true if valid string, false if not
613      *
614      * @access public
615      */
616     static  function string($string, $options)
617     {
618         $format     = null;
619         $min_length = 0;
620         $max_length = 0;
621
622         if (is_array($options)) {
623             extract($options);
624         }
625
626         if ($format && !preg_match("|^[$format]*\$|s", $string)) {
627             return false;
628         }
629
630         if ($min_length && strlen($string) < $min_length) {
631             return false;
632         }
633
634         if ($max_length && strlen($string) > $max_length) {
635             return false;
636         }
637
638         return true;
639     }
640
641     /**
642      * Validate an URI (RFC2396)
643      * This function will validate 'foobarstring' by default, to get it to validate
644      * only http, https, ftp and such you have to pass it in the allowed_schemes
645      * option, like this:
646      * <code>
647      * $options = array('allowed_schemes' => array('http', 'https', 'ftp'))
648      * var_dump(Validate::uri('http://www.example.org', $options));
649      * </code>
650      *
651      * NOTE 1: The rfc2396 normally allows middle '-' in the top domain
652      *         e.g. http://example.co-m should be valid
653      *         However, as '-' is not used in any known TLD, it is invalid
654      * NOTE 2: As double shlashes // are allowed in the path part, only full URIs
655      *         including an authority can be valid, no relative URIs
656      *         the // are mandatory (optionally preceeded by the 'sheme:' )
657      * NOTE 3: the full complience to rfc2396 is not achieved by default
658      *         the characters ';/?:@$,' will not be accepted in the query part
659      *         if not urlencoded, refer to the option "strict'"
660      *
661      * @param string $url     URI to validate
662      * @param array  $options Options used by the validation method.
663      *                          key => type
664      *                          'domain_check' => boolean
665      *                              Whether to check the DNS entry or not
666      *                          'allowed_schemes' => array, list of protocols
667      *                              List of allowed schemes ('http',
668      *                              'ssh+svn', 'mms')
669      *                          'strict' => string the refused chars
670      *                              in query and fragment parts
671      *                              default: ';/?:@$,'
672      *                              empty: accept all rfc2396 foreseen chars
673      *
674      * @return boolean true if valid uri, false if not
675      *
676      * @access public
677      */
678     static function uri($url, $options = null)
679     {
680         $strict = ';/?:@$,';
681         $domain_check = false;
682         $allowed_schemes = null;
683         if (is_array($options)) {
684             extract($options);
685         }
686         if (is_array($allowed_schemes) &&
687             in_array("tag", $allowed_schemes)
688         ) {
689             if (strpos($url, "tag:") === 0) {
690                 return self::__uriRFC4151($url);
691             }
692         }
693
694         if (preg_match(
695              '&^(?:([a-z][-+.a-z0-9]*):)?                             # 1. scheme
696               (?://                                                   # authority start
697               (?:((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();:\&=+$,])*)@)?    # 2. authority-userinfo
698               (?:((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[a-z0-9]+)?\.?)  # 3. authority-hostname OR
699               |([0-9]{1,3}(?:\.[0-9]{1,3}){3}))                       # 4. authority-ipv4
700               (?::([0-9]*))?)                                        # 5. authority-port
701               ((?:/(?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'():@\&=+$,;])*)*/?)? # 6. path
702               (?:\?([^#]*))?                                          # 7. query
703               (?:\#((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();/?:@\&=+$,])*))? # 8. fragment
704               $&xi', $url, $matches)) {
705             $scheme = isset($matches[1]) ? $matches[1] : '';
706             $authority = isset($matches[3]) ? $matches[3] : '' ;
707             if (is_array($allowed_schemes) &&
708                 !in_array($scheme, $allowed_schemes)
709             ) {
710                 return false;
711             }
712             if (!empty($matches[4])) {
713                 $parts = explode('.', $matches[4]);
714                 foreach ($parts as $part) {
715                     if ($part > 255) {
716                         return false;
717                     }
718                 }
719             } elseif ($domain_check && function_exists('checkdnsrr')) {
720                 if (!checkdnsrr($authority, 'A')) {
721                     return false;
722                 }
723             }
724             if ($strict) {
725                 $strict = '#[' . preg_quote($strict, '#') . ']#';
726                 if ((!empty($matches[7]) && preg_match($strict, $matches[7]))
727                  || (!empty($matches[8]) && preg_match($strict, $matches[8]))) {
728                     return false;
729                 }
730             }
731             return true;
732         }
733         return false;
734     }
735
736     /**
737      * Validate date and times. Note that this method need the Date_Calc class
738      *
739      * @param string $date    Date to validate
740      * @param array  $options array options where :
741      *                          'format' The format of the date (%d-%m-%Y)
742      *                                   or rfc822_compliant
743      *                          'min'    The date has to be greater
744      *                                   than this array($day, $month, $year)
745      *                                   or PEAR::Date object
746      *                          'max'    The date has to be smaller than
747      *                                   this array($day, $month, $year)
748      *                                   or PEAR::Date object
749      *
750      * @return boolean true if valid date/time, false if not
751      *
752      * @access public
753      */
754     public static function date($date, $options)
755     {
756         $max    = false;
757         $min    = false;
758         $format = '';
759
760         if (is_array($options)) {
761             extract($options);
762         }
763
764         if (strtolower($format) == 'rfc822_compliant') {
765             $preg = '&^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),) \s+
766                     (?:(\d{2})?) \s+
767                     (?:(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?) \s+
768                     (?:(\d{2}(\d{2})?)?) \s+
769                     (?:(\d{2}?)):(?:(\d{2}?))(:(?:(\d{2}?)))? \s+
770                     (?:[+-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])$&xi';
771
772             if (!preg_match($preg, $date, $matches)) {
773                 return false;
774             }
775
776             $year    = (int)$matches[4];
777             $months  = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
778                              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
779             $month   = array_keys($months, $matches[3]);
780             $month   = (int)$month[0]+1;
781             $day     = (int)$matches[2];
782             $weekday = $matches[1];
783             $hour    = (int)$matches[6];
784             $minute  = (int)$matches[7];
785             isset($matches[9]) ? $second = (int)$matches[9] : $second = 0;
786
787             if ((strlen($year) != 4)        ||
788                 ($day    > 31   || $day < 1)||
789                 ($hour   > 23)  ||
790                 ($minute > 59)  ||
791                 ($second > 59)) {
792                     return false;
793             }
794         } else {
795             $date_len = strlen($format);
796             for ($i = 0; $i < $date_len; $i++) {
797                 $c = $format[$i];
798                 if ($c == '%') {
799                     $next = $format[$i + 1];
800                     switch ($next) {
801                     case 'j':
802                     case 'd':
803                         if ($next == 'j') {
804                             $day = (int)Validate::_substr($date, 1, 2);
805                         } else {
806                             $day = (int)Validate::_substr($date, 0, 2);
807                         }
808                         if ($day < 1 || $day > 31) {
809                             return false;
810                         }
811                         break;
812                     case 'm':
813                     case 'n':
814                         if ($next == 'm') {
815                             $month = (int)Validate::_substr($date, 0, 2);
816                         } else {
817                             $month = (int)Validate::_substr($date, 1, 2);
818                         }
819                         if ($month < 1 || $month > 12) {
820                             return false;
821                         }
822                         break;
823                     case 'Y':
824                     case 'y':
825                         if ($next == 'Y') {
826                             $year = Validate::_substr($date, 4);
827                             $year = (int)$year?$year:'';
828                         } else {
829                             $year = (int)(substr(date('Y'), 0, 2) .
830                                               Validate::_substr($date, 2));
831                         }
832                         if (strlen($year) != 4 || $year < 0 || $year > 9999) {
833                             return false;
834                         }
835                         break;
836                     case 'g':
837                     case 'h':
838                         if ($next == 'g') {
839                             $hour = Validate::_substr($date, 1, 2);
840                         } else {
841                             $hour = Validate::_substr($date, 2);
842                         }
843                         if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 12) {
844                             return false;
845                         }
846                         break;
847                     case 'G':
848                     case 'H':
849                         if ($next == 'G') {
850                             $hour = Validate::_substr($date, 1, 2);
851                         } else {
852                             $hour = Validate::_substr($date, 2);
853                         }
854                         if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 24) {
855                             return false;
856                         }
857                         break;
858                     case 's':
859                     case 'i':
860                         $t = Validate::_substr($date, 2);
861                         if (!preg_match('/^\d+$/', $t) || $t < 0 || $t > 59) {
862                             return false;
863                         }
864                         break;
865                     default:
866                         trigger_error("Not supported char `$next' after % in offset " . ($i+2), E_USER_WARNING);
867                     }
868                     $i++;
869                 } else {
870                     //literal
871                     if (Validate::_substr($date, 1) != $c) {
872                         return false;
873                     }
874                 }
875             }
876         }
877         // there is remaing data, we don't want it
878         if (strlen($date) && (strtolower($format) != 'rfc822_compliant')) {
879             return false;
880         }
881
882         if (isset($day) && isset($month) && isset($year)) {
883             if (!checkdate($month, $day, $year)) {
884                 return false;
885             }
886
887             if (strtolower($format) == 'rfc822_compliant') {
888                 if ($weekday != date("D", mktime(0, 0, 0, $month, $day, $year))) {
889                     return false;
890                 }
891             }
892
893             if ($min) {
894                 include_once 'Date/Calc.php';
895                 if (is_a($min, 'Date') &&
896                     (Date_Calc::compareDates($day, $month, $year,
897                         $min->getDay(), $min->getMonth(), $min->getYear()) < 0)
898                 ) {
899                     return false;
900                 } elseif (is_array($min) &&
901                         (Date_Calc::compareDates($day, $month, $year,
902                             $min[0], $min[1], $min[2]) < 0)
903                 ) {
904                     return false;
905                 }
906             }
907
908             if ($max) {
909                 include_once 'Date/Calc.php';
910                 if (is_a($max, 'Date') &&
911                     (Date_Calc::compareDates($day, $month, $year,
912                         $max->getDay(), $max->getMonth(), $max->getYear()) > 0)
913                 ) {
914                     return false;
915                 } elseif (is_array($max) &&
916                         (Date_Calc::compareDates($day, $month, $year,
917                             $max[0], $max[1], $max[2]) > 0)
918                 ) {
919                     return false;
920                 }
921             }
922         }
923
924         return true;
925     }
926
927     /**
928      * Substr
929      *
930      * @param string &$date Date
931      * @param string $num   Length
932      * @param string $opt   Unknown
933      *
934      * @access private
935      * @return string
936      */
937     static function _substr(&$date, $num, $opt = false)
938     {
939         if ($opt && strlen($date) >= $opt && preg_match('/^[0-9]{'.$opt.'}/', $date, $m)) {
940             $ret = $m[0];
941         } else {
942             $ret = substr($date, 0, $num);
943         }
944         $date = substr($date, strlen($ret));
945         return $ret;
946     }
947
948     static function _modf($val, $div)
949     {
950         if (function_exists('bcmod')) {
951             return bcmod($val, $div);
952         } elseif (function_exists('fmod')) {
953             return fmod($val, $div);
954         }
955         $r = $val / $div;
956         $i = intval($r);
957         return intval($val - $i * $div + .1);
958     }
959
960     /**
961      * Calculates sum of product of number digits with weights
962      *
963      * @param string $number  number string
964      * @param array  $weights reference to array of weights
965      *
966      * @access protected
967      *
968      * @return int returns product of number digits with weights
969      */
970     static function _multWeights($number, &$weights)
971     {
972         if (!is_array($weights)) {
973             return -1;
974         }
975         $sum = 0;
976
977         $count = min(count($weights), strlen($number));
978         if ($count == 0) { // empty string or weights array
979             return -1;
980         }
981         for ($i = 0; $i < $count; ++$i) {
982             $sum += intval(substr($number, $i, 1)) * $weights[$i];
983         }
984
985         return $sum;
986     }
987
988     /**
989      * Calculates control digit for a given number
990      *
991      * @param string $number     number string
992      * @param array  $weights    reference to array of weights
993      * @param int    $modulo     (optionsl) number
994      * @param int    $subtract   (optional) number
995      * @param bool   $allow_high (optional) true if function can return number higher than 10
996      *
997      * @access protected
998      *
999      * @return  int -1 calculated control number is returned
1000      */
1001     static function _getControlNumber($number, &$weights, $modulo = 10, $subtract = 0, $allow_high = false)
1002     {
1003         // calc sum
1004         $sum = Validate::_multWeights($number, $weights);
1005         if ($sum == -1) {
1006             return -1;
1007         }
1008         $mod = Validate::_modf($sum, $modulo);  // calculate control digit
1009
1010         if ($subtract > $mod && $mod > 0) {
1011             $mod = $subtract - $mod;
1012         }
1013         if ($allow_high === false) {
1014             $mod %= 10;           // change 10 to zero
1015         }
1016         return $mod;
1017     }
1018
1019     /**
1020      * Validates a number
1021      *
1022      * @param string $number   number to validate
1023      * @param array  $weights  reference to array of weights
1024      * @param int    $modulo   (optional) number
1025      * @param int    $subtract (optional) number
1026      *
1027      * @access protected
1028      *
1029      * @return  bool true if valid, false if not
1030      */
1031     static function _checkControlNumber($number, &$weights, $modulo = 10, $subtract = 0)
1032     {
1033         if (strlen($number) < count($weights)) {
1034             return false;
1035         }
1036         $target_digit  = substr($number, count($weights), 1);
1037         $control_digit = Validate::_getControlNumber($number, $weights, $modulo, $subtract, $modulo > 10);
1038
1039         if ($control_digit == -1) {
1040             return false;
1041         }
1042         if ($target_digit === 'X' && $control_digit == 10) {
1043             return true;
1044         }
1045         if ($control_digit != $target_digit) {
1046             return false;
1047         }
1048         return true;
1049     }
1050
1051     /**
1052      * Bulk data validation for data introduced in the form of an
1053      * assoc array in the form $var_name => $value.
1054      * Can be used on any of Validate subpackages
1055      *
1056      * @param array   $data     Ex: array('name' => 'toto', 'email' => 'toto@thing.info');
1057      * @param array   $val_type Contains the validation type and all parameters used in.
1058      *                          'val_type' is not optional
1059      *                          others validations properties must have the same name as the function
1060      *                          parameters.
1061      *                          Ex: array('toto'=>array('type'=>'string','format'='toto@thing.info','min_length'=>5));
1062      * @param boolean $remove   if set, the elements not listed in data will be removed
1063      *
1064      * @return array   value name => true|false    the value name comes from the data key
1065      *
1066      * @access public
1067      */
1068     static function multiple(&$data, &$val_type, $remove = false)
1069     {
1070         $keys  = array_keys($data);
1071         $valid = array();
1072
1073         foreach ($keys as $var_name) {
1074             if (!isset($val_type[$var_name])) {
1075                 if ($remove) {
1076                     unset($data[$var_name]);
1077                 }
1078                 continue;
1079             }
1080             $opt       = $val_type[$var_name];
1081             $methods   = get_class_methods('Validate');
1082             $val2check = $data[$var_name];
1083             // core validation method
1084             if (in_array(strtolower($opt['type']), $methods)) {
1085                 //$opt[$opt['type']] = $data[$var_name];
1086                 $method = $opt['type'];
1087                 unset($opt['type']);
1088
1089                 if (sizeof($opt) == 1 && is_array(reset($opt))) {
1090                     $opt = array_pop($opt);
1091                 }
1092                 $valid[$var_name] = call_user_func(array('Validate', $method), $val2check, $opt);
1093
1094                 /**
1095                  * external validation method in the form:
1096                  * "<class name><underscore><method name>"
1097                  * Ex: us_ssn will include class Validate/US.php and call method ssn()
1098                  */
1099             } elseif (strpos($opt['type'], '_') !== false) {
1100                 $validateType = explode('_', $opt['type']);
1101                 $method       = array_pop($validateType);
1102                 $class        = implode('_', $validateType);
1103                 $classPath    = str_replace('_', DIRECTORY_SEPARATOR, $class);
1104                 $class        = 'Validate_' . $class;
1105                 if (!Validate::_includePathFileExists("Validate/$classPath.php")) {
1106                     trigger_error("$class isn't installed or you may have some permission issues", E_USER_ERROR);
1107                 }
1108
1109                 $ce = substr(phpversion(), 0, 1) > 4 ?
1110                     class_exists($class, false) : class_exists($class);
1111                 if (!$ce ||
1112                     !in_array($method, get_class_methods($class))
1113                 ) {
1114                     trigger_error("Invalid validation type $class::$method",
1115                         E_USER_WARNING);
1116                     continue;
1117                 }
1118                 unset($opt['type']);
1119                 if (sizeof($opt) == 1) {
1120                     $opt = array_pop($opt);
1121                 }
1122                 $valid[$var_name] = call_user_func(array($class, $method),
1123                     $data[$var_name], $opt);
1124             } else {
1125                 trigger_error("Invalid validation type {$opt['type']}",
1126                     E_USER_WARNING);
1127             }
1128         }
1129         return $valid;
1130     }
1131
1132     /**
1133      * Determine whether specified file exists along the include path.
1134      *
1135      * @param string $filename file to search for
1136      *
1137      * @access private
1138      *
1139      * @return bool true if file exists
1140      */
1141     static function _includePathFileExists($filename)
1142     {
1143         $paths = explode(":", ini_get("include_path"));
1144         $result = false;
1145         foreach($paths as $key => $val) {
1146             $result = file_exists($val . "/" . $filename);
1147             if ($result) {
1148                 return $return;
1149             }
1150         }
1151         return false;
1152     }
1153 }
1154