root/fValidation.php

Revision 670, 12.7 kB (checked in by wbond, 1 year ago)

Fixed a typo in the fValidation API docs

LineHide Line Numbers
1 <?php
2 /**
3  * Provides validation routines for standalone forms, such as contact forms
4  *
5  * @copyright  Copyright (c) 2007-2009 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fValidation
11  *
12  * @version    1.0.0b4
13  * @changes    1.0.0b4  Changed date checking from `strtotime()` to fTimestamp for better localization support [wb, 2009-06-01]
14  * @changes    1.0.0b3  Updated for new fCore API [wb, 2009-02-16]
15  * @changes    1.0.0b2  Added support for validating date and URL fields [wb, 2009-01-23]
16  * @changes    1.0.0b   The initial implementation [wb, 2007-06-14]
17  */
18 class fValidation
19 {
20     /**
21     * Composes text using fText if loaded
22     *
23     * @param  string  $message    The message to compose
24     * @param  mixed   $component  A string or number to insert into the message
25     * @param  mixed   ...
26     * @return string  The composed and possible translated message
27     */
28     static protected function compose($message)
29     {
30         $args = array_slice(func_get_args(), 1);
31        
32         if (class_exists('fText', FALSE)) {
33             return call_user_func_array(
34                 array('fText', 'compose'),
35                 array($message, $args)
36             );
37         } else {
38             return vsprintf($message, $args);
39         }
40     }
41    
42    
43     /**
44     * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
45     *
46     * @param  mixed $value  The value to check
47     * @return boolean  If the value is string-like
48     */
49     static protected function stringlike($value)
50     {
51         if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
52             return FALSE;   
53         }
54        
55         return TRUE;
56     }
57    
58    
59     /**
60     * Fields that should be valid dates
61     *
62     * @var array
63     */
64     private $date_fields = array();
65    
66     /**
67     * Fields that should be formatted as email addresses
68     *
69     * @var array
70     */
71     private $email_fields = array();
72    
73     /**
74     * Fields that will be included in email headers and should be checked for email injection
75     *
76     * @var array
77     */
78     private $email_header_fields = array();
79    
80     /**
81     * The fields to be required
82     *
83     * @var array
84     */
85     private $required_fields = array();
86    
87     /**
88     * Fields that should be formatted as URLs
89     *
90     * @var array
91     */
92     private $url_fields = array();
93    
94    
95     /**
96     * All requests that hit this method should be requests for callbacks
97     *
98     * @internal
99      *
100     * @param  string $method  The method to create a callback for
101     * @return callback  The callback for the method requested
102     */
103     public function __get($method)
104     {
105         return array($this, $method);       
106     }
107    
108    
109     /**
110     * Adds form fields to the list of fields to be blank or a valid date
111     *
112     * Use ::addRequiredFields() disallow blank values.
113     *
114     * @param  string $field  Any number of fields that should contain a valid date
115     * @param  string ...
116     * @return void
117     */
118     public function addDateFields()
119     {
120         $args = func_get_args();
121         foreach ($args as $arg) {
122             if (!self::stringlike($arg)) {
123                 throw new fProgrammerException(
124                     'The field specified, %s, does not appear to be a valid field name',
125                     $arg
126                 );
127             }
128         }
129         $this->date_fields = array_merge($this->date_fields, $args);
130     }
131    
132    
133     /**
134     * Adds form fields to the list of fields to be blank or a valid email address
135     *
136     * Use ::addRequiredFields() disallow blank values.
137     *
138     * @param  string $field  Any number of fields that should contain a valid email address
139     * @param  string ...
140     * @return void
141     */
142     public function addEmailFields()
143     {
144         $args = func_get_args();
145         foreach ($args as $arg) {
146             if (!self::stringlike($arg)) {
147                 throw new fProgrammerException(
148                     'The field specified, %s, does not appear to be a valid field name',
149                     $arg
150                 );
151             }
152         }
153         $this->email_fields = array_merge($this->email_fields, $args);
154     }
155    
156    
157     /**
158     * Adds form fields to be checked for email injection
159     *
160     * Every field that is included in email headers should be passed to this
161     * method.
162     *
163     * @param  string $field  Any number of fields to be checked for email injection
164     * @param  string ...
165     * @return void
166     */
167     public function addEmailHeaderFields()
168     {
169         $args = func_get_args();
170         foreach ($args as $arg) {
171             if (!self::stringlike($arg)) {
172                 throw new fProgrammerException(
173                     'The field specified, %s, does not appear to be a valid field name',
174                     $arg
175                 );
176             }
177         }
178         $this->email_header_fields = array_merge($this->email_header_fields, $args);
179     }
180    
181    
182     /**
183     * Adds form fields to be required, taking each parameter as a field name
184     *
185     * To require one of multiple fields, pass an array of fields as the parameter.
186     *
187     * To conditionally require fields, pass an associative array of with the
188     * key being the field that will trigger the other fields to be required:
189     *
190     * {{{
191     * #!php
192     * array(
193     *     'trigger_field' => array(
194     *         'conditionally_required_field',
195     *         'second_conditionally_required_field'
196     *     )
197     * );
198     * }}}
199     *
200     * @param  mixed $field  Any number of fields to check
201     * @param  mixed ...
202     * @return void
203     */
204     public function addRequiredFields()
205     {
206         $args       = func_get_args();
207         $fixed_args = array();
208        
209         foreach ($args as $arg) {
210             // This handles normal field validation
211             if (self::stringlike($arg)) {
212                 $fixed_args[] = $arg;
213            
214             // This allows for 'or' validation
215             } elseif (is_array($arg) && sizeof($arg) > 1) {
216                 $fixed_args[] = $arg;
217            
218             // This handles conditional validation
219             } elseif (is_array($arg) && sizeof($arg) == 1 && self::stringlike(key($arg)) && is_array(reset($arg))) {
220                 $fixed_args[key($arg)] = reset($arg);
221                
222             } else {
223                 throw new fProgrammerException(
224                     'The field specified, %s, does not appear to be a valid required field definition',
225                     $arg
226                 );
227             }
228         }
229        
230         $this->required_fields = array_merge($this->required_fields, $fixed_args);
231     }
232    
233    
234     /**
235     * Adds form fields to the list of fields to be blank or a valid URL
236     *
237     * Use ::addRequiredFields() disallow blank values.
238     *
239     * @param  string $field  Any number of fields that should contain a valid URL
240     * @param  string ...
241     * @return void
242     */
243     public function addURLFields()
244     {
245         $args = func_get_args();
246         foreach ($args as $arg) {
247             if (!self::stringlike($arg)) {
248                 throw new fProgrammerException(
249                     'The field specified, %s, does not appear to be a valid field name',
250                     $arg
251                 );
252             }
253         }
254         $this->url_fields = array_merge($this->url_fields, $args);
255     }
256    
257    
258     /**
259     * Validates the date fields, requiring that any date fields that have a value that can be interpreted as a date
260     *
261     * @param  array &$messages  The messages to display to the user
262     * @return void
263     */
264     private function checkDateFields(&$messages)
265     {
266         foreach ($this->date_fields as $date_field) {
267             $value = trim(fRequest::get($date_field));
268             if (self::stringlike($value)) {
269                 try {
270                     new fTimestamp($value);   
271                 } catch (fValidationException $e) {
272                     $messages[] = self::compose(
273                         '%sPlease enter a date',
274                         fValidationException::formatField(fGrammar::humanize($date_field))
275                     );
276                 }
277             }
278         }
279     }
280    
281    
282     /**
283     * Validates the email fields, requiring that any email fields that have a value are formatted like an email address
284     *
285     * @param  array &$messages  The messages to display to the user
286     * @return void
287     */
288     private function checkEmailFields(&$messages)
289     {
290         foreach ($this->email_fields as $email_field) {
291             $value = trim(fRequest::get($email_field));
292             if (self::stringlike($value) && !preg_match(fEmail::EMAIL_REGEX, $value)) {
293                 $messages[] = self::compose(
294                     '%sPlease enter an email address in the form name@example.com',
295                     fValidationException::formatField(fGrammar::humanize($email_field))
296                 );
297             }
298         }
299     }
300    
301    
302     /**
303     * Validates email header fields to ensure they don't have newline characters (which allow for email header injection)
304     *
305     * @param  array &$messages  The messages to display to the user
306     * @return void
307     */
308     private function checkEmailHeaderFields(&$messages)
309     {
310         foreach ($this->email_header_fields as $email_header_field) {
311             if (preg_match('#\r|\n#', fRequest::get($email_header_field))) {
312                 $messages[] = self::compose(
313                     '%sLine breaks are not allowed',
314                     fValidationException::formatField(fGrammar::humanize($email_header_field))
315                 );
316             }
317         }
318     }
319    
320    
321     /**
322     * Validates the required fields, adding any missing fields to the messages array
323     *
324     * @param  array &$messages  The messages to display to the user
325     * @return void
326     */
327     private function checkRequiredFields(&$messages)
328     {
329         foreach ($this->required_fields as $key => $required_field) {
330             // Handle single fields
331             if (is_numeric($key) && is_string($required_field)) {
332                 if (!self::hasValue($required_field)) {
333                     $messages[] = self::compose(
334                         '%sPlease enter a value',
335                         fValidationException::formatField(fGrammar::humanize($required_field))
336                     );
337                 }
338                
339             // Handle one of multiple fields
340             } elseif (is_numeric($key) && is_array($required_field)) {
341                 $found = FALSE;
342                 foreach ($required_field as $individual_field) {
343                     if (self::hasValue($individual_field)) {
344                         $found = TRUE;
345                     }
346                 }
347                
348                 if (!$found) {
349                     $required_field = array_map(array('fGrammar', 'humanize'), $required_field);
350                     $messages[] = self::compose(
351                         '%sPlease enter at least one',
352                         fValidationException::formatField(join(', ', $required_field))
353                     );
354                 }
355                
356             // Handle conditional fields
357             } else {
358                 if (!self::hasValue($key)) {
359                     continue;
360                 }
361                 foreach ($required_field as $individual_field) {
362                     if (!self::hasValue($individual_field)) {
363                         $messages[] = self::compose(
364                             '%sPlease enter a value',
365                             fValidationException::formatField(fGrammar::humanize($individual_field))
366                         );
367                     }
368                 }
369             }
370         }
371     }
372    
373    
374     /**
375     * Validates the URL fields, requiring that any URL fields that have a value are valid URLs
376     *
377     * @param  array &$messages  The messages to display to the user
378     * @return void
379     */
380     private function checkURLFields(&$messages)
381     {
382         foreach ($this->url_fields as $url_field) {
383             $value = trim(fRequest::get($url_field));
384             if (self::stringlike($value) && !preg_match('#^https?://[^ ]+$#iD', $value)) {
385                 $messages[] = self::compose(
386                     '%sPlease enter a URL in the form http://www.example.com/page',
387                     fValidationException::formatField(fGrammar::humanize($url_field))
388                 );
389             }
390         }
391     }
392    
393    
394     /**
395     * Check if a field has a value
396     *
397     * @param  string $key  The key to check for a value
398     * @return boolean  If the key has a value
399     */
400     static private function hasValue($key)
401     {
402         $value = fRequest::get($key);
403         if (self::stringlike($value)) {
404             return TRUE;
405         }   
406         if (is_array($value)) {
407             foreach ($value as $individual_value) {
408                 if (self::stringlike($individual_value)) {
409                     return TRUE;   
410                 }
411             }   
412         }
413         return FALSE;
414     }
415    
416    
417     /**
418     * Checks for required fields, email field formatting and email header injection using values previously set
419     *
420     * @throws fValidationException  When one of the options set for the object is violated
421     *
422     * @return void
423     */
424     public function validate()
425     {
426         if (!$this->email_header_fields &&
427               !$this->required_fields &&
428               !$this->email_fields &&
429               !$this->date_fields &&
430               !$this->url_fields) {
431             throw new fProgrammerException(
432                 'No fields have been set to be validated'
433             );
434         }
435        
436         $messages = array();
437        
438         $this->checkRequiredFields($messages);
439         $this->checkDateFields($messages);
440         $this->checkEmailFields($messages);
441         $this->checkEmailHeaderFields($messages);
442         $this->checkURLFields($messages);
443        
444         if ($messages) {
445             throw new fValidationException(
446                 sprintf(
447                     "<p>%1\$s</p>\n<ul>\n<li>%2\$s</li>\n</ul>",
448                     self::compose("The following problems were found:"),
449                     join("</li>\n<li>", $messages)
450                 )
451             );
452         }
453     }
454 }
455  
456  
457  
458 /**
459  * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
460  *
461  * Permission is hereby granted, free of charge, to any person obtaining a copy
462  * of this software and associated documentation files (the "Software"), to deal
463  * in the Software without restriction, including without limitation the rights
464  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
465  * copies of the Software, and to permit persons to whom the Software is
466  * furnished to do so, subject to the following conditions:
467  *
468  * The above copyright notice and this permission notice shall be included in
469  * all copies or substantial portions of the Software.
470  *
471  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
472  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
473  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
474  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
475  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
476  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
477  * THE SOFTWARE.
478  */