source: branches/1.1dev/lib/FormValidator.inc.php @ 568

Last change on this file since 568 was 568, checked in by anonymous, 8 years ago

Backported improvements of email domain validation.

File size: 19.4 KB
Line 
1<?php
2/**
3 * FormValidator.inc.php
4 * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
5 */
6
7//     Examples of use:
8//
9//     require_once CODE_BASE . '/lib/FormValidator.inc.php';
10//     $fv = new FormValidator();
11//
12//     $fv->isEmpty('location_name', _("<strong>Location name</strong> cannot be blank."));
13//     $fv->checkRegex('cc_exp', '/^\d{4}$|^$/', true, _("CC exp date must be in MMYY format."));
14//     $fv->isInteger('client_id', _("<strong>Client id</strong> must be an integer."));
15//     $fv->numericRange('client_id', -32768, 32767, _("<strong>Client id</strong> must be a number between -32768 and 32767."));
16//     $fv->stringLength('zip', 0, 255, _("<strong>Zip</strong> must contain less than 256 characters."));
17//     $fv->validateEmail('invoice_email');
18//     $fv->validatePhone('phone1');
19
20
21/**
22 * The FormValidator:: class provides a method for validating input from
23 * http requests and displaying errors.
24 *
25 * @requires  This class requires App.inc.php
26 * @requires  This class requires Utilities.inc.php
27 * @author    Quinn Comendant <quinn@strangecode.com>
28 * @version   1.5
29 */
30
31require_once dirname(__FILE__) . '/App.inc.php';
32require_once dirname(__FILE__) . '/Utilities.inc.php';
33require_once dirname(__FILE__) . '/Prefs.inc.php';
34
35class FormValidator
36{
37
38    /**
39     * Array filling with errors. The key will be the name of the form where
40     * the data came from. The value will be a message to print to the user.
41     */
42    var $errors = array();
43
44    /**
45     * Return the current list of errors.
46     *
47     * @return array    an array of errors in the following arrangement:
48     *                  keys: the name of the variable with an error
49     *                  vals: the message to display for that error
50     */
51    function getErrorList()
52    {
53        return $this->errors;
54    }
55
56    /**
57     * Add an error to the errors stack.
58     *
59     * @param   string $form_name   The name of the incoming form variable.
60     * @param   string $msg         The error message for that form.
61     * @param   int    $type        The type of message: MSG_NOTICE,
62     *                              MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
63     * @param   string $file        __FILE__.
64     * @param   string $line        __LINE__.
65     */
66    function addError($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
67    {
68        $this->errors[] = array(
69            'name' => $form_name,
70            'message' => $msg,
71            'type' => $type,
72            'file' => $file,
73            'line' => $line
74        );
75    }
76
77    /**
78     * Check whether any errors have been triggered.
79     *
80     * @param  string $form_name the name of the incoming form variable
81     *
82     * @return bool   true if any errors were found, or if found for
83     *                a variable of $form_name, false otherwise
84     */
85    function anyErrors($form_name=null)
86    {
87        if (isset($form_name)) {
88            foreach ($this->errors as $err) {
89                if ($err['name'] == $form_name) {
90                    return true;
91                }
92            }
93            return false;
94        }
95        return (sizeof($this->errors) > 0);
96    }
97
98    /**
99     * Reset the error list.
100     */
101    function resetErrorList()
102    {
103        $this->errors = array();
104    }
105
106    /**
107     * If this form has an error, print an error marker like "<<".
108     *
109     * @param  string $form_name the name of the incoming form variable
110     * @param  string $marker    A string to print if there is an error. if
111     *                           not provided, use default.
112     */
113    function err($form_name, $marker=null)
114    {
115        global $CFG;
116        if ($this->anyErrors($form_name)) {
117            if (isset($marker)) {
118                echo $marker;
119            } else {
120                echo '<span style="color:red; font-size:2em;"><strong>&laquo;</strong></span>';
121            }
122        }
123    }
124
125    /**
126     * Check whether input has a value. To be used when a value must be empty
127     * under certain circumstances.
128     *
129     * @param  string $form_name the name of the incoming form variable
130     * @param  string $msg       the message to display on error
131     *
132     * @return bool   true if form is not empty, false otherwise.
133     */
134    function notEmpty($form_name, $msg='')
135    {
136
137        $val = getFormData($form_name);
138        if (is_array($val)) {
139            if (!empty($val)) {
140                $this->addError($form_name, $msg);
141                return true;
142            } else {
143                return false;
144            }
145        } else {
146            if (trim($val) != '') {
147                $this->addError($form_name, $msg);
148                return true;
149            } else {
150                return false;
151            }
152        }
153    }
154
155    /**
156     * Check whether input is blank.
157     *
158     * @param  string $form_name the name of the incoming form variable
159     * @param  string $msg       the message to display on error
160     *
161     * @return bool   true if form is empty, false otherwise.
162     */
163    function isEmpty($form_name, $msg='')
164    {
165        $val = getFormData($form_name);
166        if (is_array($val)) {
167            if (empty($val)) {
168                $this->addError($form_name, $msg);
169                return true;
170            } else {
171                return false;
172            }
173        } else {
174            if (trim($val) == '') {
175                $this->addError($form_name, $msg);
176                return true;
177            } else {
178                return false;
179            }
180        }
181    }
182
183    /**
184     * Check whether input is a string.
185     *
186     * @param  string $form_name the name of the incoming form variable
187     * @param  string $msg       the message to display on error
188     *
189     * @return bool   true if form is a string, false otherwise.
190     */
191    function isString($form_name, $msg='')
192    {
193        $val = getFormData($form_name);
194        if (!is_string($val) && $val != '') {
195            $this->addError($form_name, $msg);
196            return false;
197        } else {
198            return true;
199        }
200    }
201
202    /**
203     * Check whether input is a number. Allows negative numbers.
204     *
205     * @param  string $form_name the name of the incoming form variable
206     * @param  string $msg       the message to display on error
207     *
208     * @return bool   true if no errors found, false otherwise
209     */
210    function isNumber($form_name, $msg='')
211    {
212        $val = getFormData($form_name);
213        if (!is_numeric($val) && $val != '') {
214            $this->addError($form_name, $msg);
215            return false;
216        } else {
217            return true;
218        }
219    }
220
221    /**
222     * addError if input is NOT an integer. Don't just use is_int() because the
223     * data coming from the user is *really* a string.
224     *
225     * @param  string $form_name the name of the incoming form variable
226     * @param  string $msg       the message to display on error
227     *
228     * @return bool   true if value is an integer
229     */
230    function isInteger($form_name, $msg='', $negative_ok=false)
231    {
232        $val = getFormData($form_name);
233        $pattern = $negative_ok ? '/^-?[[:digit:]]+$/' : '/^[[:digit:]]+$/';
234        if ((!is_numeric($val) || !preg_match($pattern, $val)) && $val != '') {
235            $this->addError($form_name, $msg);
236            return false;
237        } else {
238            return true;
239        }
240    }
241
242    /**
243     * Check whether input is a float. Don't just use is_float() because the
244     * data coming from the user is *really* a string. Integers will also
245     * pass this test.
246     *
247     * @param  string $form_name the name of the incoming form variable
248     * @param  string $msg       the message to display on error
249     *
250     * @return bool   true if value is a float
251     */
252    function isFloat($form_name, $msg='', $negative_ok=false)
253    {
254        $val = getFormData($form_name);
255        $pattern = $negative_ok ? '/^-?[[:digit:]]*(?:\.?[[:digit:]]+)$/' : '/^[[:digit:]]*(?:\.?[[:digit:]]+)$/';
256        if ((!is_numeric($val) || !preg_match($pattern, $val)) && $val != '') {
257            $this->addError($form_name, $msg);
258            return false;
259        } else {
260            return true;
261        }
262    }
263
264    /**
265     * Check whether input is an array.
266     *
267     * @param  string $form_name the name of the incoming form variable
268     * @param  string $msg       the message to display on error
269     *
270     * @return bool   true if value is a float
271     */
272    function isArray($form_name, $msg='')
273    {
274        $val = getFormData($form_name);
275        if (!is_array($val) && !empty($val)) {
276            $this->addError($form_name, $msg);
277            return false;
278        } else {
279            return true;
280        }
281    }
282
283    /**
284     * Check whether input matches the specified perl regular expression
285     * pattern.
286     *
287     * @param  string $form_name the name of the incoming form variable
288     * @param  int    $regex     perl regex that the string must match
289     * @param  bool   $not       set to false to be valid if match, or true
290     *                           to be valid on no match
291     * @param  string $msg       the message to display on error
292     *
293     * @return bool   true if value passes regex test
294     */
295    function checkRegex($form_name, $regex, $not, $msg='')
296    {
297        $val = getFormData($form_name);
298        if ($not) {
299            if (!preg_match($regex, $val))  {
300                $this->addError($form_name, $msg);
301                return false;
302            } else {
303                return true;
304            }
305        } else {
306            if (preg_match($regex, $val))  {
307                $this->addError($form_name, $msg);
308                return false;
309            } else {
310                return true;
311            }
312        }
313    }
314
315    /**
316     * Tests if the string length is between specified values. Whitespace excluded for min.
317     *
318     * @param  string $form_name the name of the incoming form variable
319     * @param  int    $min       minimum length of string, inclusive
320     * @param  int    $max       maximum length of string, inclusive
321     * @param  string $msg       the message to display on error
322     *
323     * @return bool   true if string length is within given boundaries
324     */
325    function stringLength($form_name, $min, $max, $msg='')
326    {
327        $val = getFormData($form_name);
328
329        if (strlen(trim($val)) < $min || strlen($val) > $max) {
330            $this->addError($form_name, $msg);
331            return false;
332        } else {
333            return true;
334        }
335    }
336
337    /**
338     * Check whether input is within a valid numeric range.
339     *
340     * @param  string $form_name the name of the incoming form variable
341     * @param  int    $min       minimum value of number, inclusive
342     * @param  int    $max       maximum value of number, inclusive
343     * @param  string $msg       the message to display on error
344     *
345     * @return bool   true if no errors found, false otherwise
346     */
347    function numericRange($form_name, $min, $max, $msg='')
348    {
349        $val = getFormData($form_name);
350        if ($val != '' && is_numeric($val)) {
351            if ($val < $min || $val > $max) {
352                $this->addError($form_name, $msg);
353                return false;
354            }
355            return true;
356        } else {
357            // Not a number!
358            return false;
359        }
360    }
361
362    /**
363     * Validates email address length, domain name existence, format.
364     *
365     * @param  string  $form_name       The name of the incoming form variable
366     * @param   bool   $strict Run strict tests (check if the domain exists and has an MX record assigned)
367     * @return bool    true if no errors found, false otherwise
368     */
369    function validateEmail($form_name, $strict=false)
370    {
371        $email = getFormData($form_name);
372        if ('' == trim($email)) {
373            return false;
374        }
375
376        $regex = '/^(?:[^,@]*\s+|[^,@]*(<)|)'                           // Display name
377        . '((?:[^.<>\s@\",\[\]]+[^<>\s@\",\[\]])*[^.<>\s@\",\[\]]+)'    // Local-part
378        . '@'                                                           // @
379        . '((?:(\[)|[A-Z0-9]?)'                                         // Domain, first char
380        . '(?(4)'                                                       // Domain conditional for if first domain char is [
381        . '(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]'                             // TRUE, matches IP address
382        . '|'
383        . '[.-]?(?:[A-Z0-9]+[-.])*(?:[A-Z0-9]+\.)+[A-Z]{2,6}))'         // FALSE, matches domain name
384        . '(?(1)'                                                       // Comment conditional for if initial < exists
385        . '(?:>\s*|>\s+\([^,@]+\)\s*)'                                  // TRUE, ensure ending >
386        . '|'
387        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i';
388
389        // Test email address format.
390        if (!preg_match($regex, getFormData($form_name), $e_parts)) {
391            $this->addError($form_name, sprintf(_("The email address <strong>%s</strong> is formatted incorrectly."), oTxt(getFormData($form_name))), MSG_ERR, __FILE__, __LINE__);
392            logMsg(sprintf('The email address %s is not valid.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
393            return false;
394        }
395
396        // We have a match! Here are the captured subpatterns, on which further tests are run.
397        // The part before the @.
398        $local = $e_parts[2];
399
400        // The part after the @.
401        // If domain is an IP [XXX.XXX.XXX.XXX] strip off the brackets.
402        $domain = $e_parts[3]{0} == '[' ? mb_substr($e_parts[3], 1, -1) : $e_parts[3];
403
404        // Test length.
405        if (mb_strlen($local) > 64 || mb_strlen($domain) > 191) {
406            $this->addError($form_name, sprintf(_("The email address <strong>%s</strong> is too long."), oTxt(getFormData($form_name))), MSG_ERR, __FILE__, __LINE__);
407            logMsg(sprintf('The email address %s is too long.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
408            return false;
409        }
410
411        if ($strict) {
412            // Strict tests.
413            if (ip2long($domain) === false && function_exists('checkdnsrr') && !checkdnsrr($domain . '.', 'MX') && gethostbyname($domain) == $domain) {
414                // Check domain exists: It's a domain if ip2long fails; checkdnsrr ensures a MX record exists; gethostbyname() ensures the domain exists.
415                $this->addError($form_name, sprintf(_("The email address <em>%s</em> does not have a valid domain name."), oTxt(getFormData($form_name))), MSG_ERR, __FILE__, __LINE__);
416                $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)));
417                return false;
418            }
419        }
420
421        return true;
422    }
423
424    /**
425     * Check whether input is a valid phone number. Notice: it is now set
426     * to allow characters like - or () or + so people can type in a phone
427     * number that looks like: +1 (530) 624-4410
428     *
429     * @param  string  $form_name the name of the incoming form variable
430     *
431     * @return bool    true if no errors found, false otherwise
432     */
433    function validatePhone($form_name)
434    {
435        $phone = getFormData($form_name);
436
437        $this->checkRegex($form_name, '/^[0-9 +().-]*$/', true, sprintf(_("The phone number <strong>%s</strong> is not valid."), $phone));
438        $this->stringLength($form_name, 0, 25, sprintf(_("The phone number <strong>%s</strong> is too long"), $phone));
439    }
440
441    /**
442     * Verifies that date can be processed by the strtotime function.
443     *
444     * @param  string  $form_name the name of the incoming form variable
445     * @param  string  $msg       the message to display on error
446     *
447     * @return bool    true if no errors found, false otherwise
448     */
449    function validateStrDate($form_name, $msg='')
450    {
451        if (($timestamp = strtotime(getFormData($form_name))) === -1) {
452            $this->addError($form_name, $msg);
453            logMsg(sprintf('The string date %s is not valid.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
454            return false;
455        } else {
456            return true;
457        }
458    }
459
460
461    /**
462     * Verifies credit card number.
463     *
464     * @param  string  $form_name   The name of the incoming form variable.
465     * @param  string  $cc_num      Card number to verify.
466     * @param  string  $cc_type     Optional, card type to do specific checks.
467     *
468     * @return bool    true if no errors found, false otherwise
469     */
470    function validateCCNumber($form_name, $cc_num=null, $cc_type=null)
471    {
472        if (!isset($cc_num)) {
473            $cc_num = getFormData($form_name);
474        }
475
476        if ('' == $cc_num) {
477            return false;
478        }
479
480        // Innocent until proven guilty
481        $card_is_valid = true;
482
483        // Get rid of any non-digits
484        $cc_num = preg_replace('/[^\d]/', '', $cc_num);
485
486        // Perform card-specific checks, if applicable
487        switch (strtolower($cc_type)) {
488            case 'visa' :
489                $card_is_valid = preg_match('/^4\d{15}$|^4\d{12}$/', $cc_num);
490                break;
491            case 'mastercard' :
492            case 'mc' :
493                $card_is_valid = preg_match('/^5[1-5]\d{14}$/', $cc_num);
494                break;
495            case 'american_express' :
496            case 'american_ex' :
497            case 'americanexpress' :
498            case 'americanex' :
499            case 'am_ex' :
500            case 'amex' :
501            case 'ae' :
502                $card_is_valid = preg_match('/^3[47]\d{13}$/', $cc_num);
503                break;
504            case 'discover' :
505                $card_is_valid = preg_match('/^6011\d{12}$/', $cc_num);
506                break;
507            case 'diners_club' :
508            case 'dinersclub' :
509            case 'diners' :
510            case 'diner' :
511            case 'dc' :
512                $card_is_valid = preg_match('/^30[0-5]\d{11}$|^3[68]\d{12}$/', $cc_num);
513                break;
514            case 'jcb' :
515                $card_is_valid = preg_match('/^3\d{15}$|^2131|1800\d{11}$/', $cc_num);
516                break;
517        }
518
519        // The Luhn formula works right to left, so reverse the number.
520        $cc_num = strrev($cc_num);
521
522        $luhn_total = 0;
523
524        $num = strlen($cc_num);
525        for ($i=0; $i<$num; $i++) {
526            // Get each digit.
527            $digit = substr($cc_num,$i,1);
528
529            //  If it's an odd digit, double it.
530            if ($i / 2 != floor($i / 2)) {
531                $digit *= 2;
532            }
533
534            //  If the result is two digits, add them.
535            if (strlen($digit) == 2) {
536                $digit = substr($digit,0,1) + substr($digit,1,1);
537            }
538
539            //  Add the current digit to the $luhn_total.
540            $luhn_total += $digit;
541        }
542
543        // If it passed (or bypassed) the card-specific check and the Total is evenly divisible by 10, it's cool!
544        if ($card_is_valid && $luhn_total % 10 == 0) {
545            return true;
546        } else {
547            $this->addError($form_name, _("The <strong>credit card number</strong> you entered is not valid."));
548            return false;
549        }
550    }
551
552    /**
553     * Check whether uploaded file is valid.
554     *
555     * @param  string $form_name the name of the incoming form variable
556     * @param  string $msg       the message to display on error
557     *
558     * @return bool   true if no errors found, false otherwise
559     */
560    function validateFile($form_name, $msg='')
561    {
562        if (!isset($_FILES[$form_name]['tmp_name']) || '' == trim($_FILES[$form_name]['tmp_name'])) {
563            $this->addError($form_name, $msg);
564            return false;
565        } else {
566            return true;
567        }
568    }
569
570} // THE END
571
572?>
Note: See TracBrowser for help on using the repository browser.