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