root/trunk/system/libraries/Validation.php

Revision 2637, 16.8 kB (checked in by Shadowhand, 4 days ago)

Properly handle key strings in ORM::safe_array()

  • Property svn:eol-style set to LF
  • Property copyright set to Copyright (c) 2007 Kohana Team
  • Property svn:keywords set to Id
Line 
1 <?php defined('SYSPATH') or die('No direct script access.');
2 /**
3  * Validation library.
4  *
5  * $Id$
6  *
7  * @package    Validation
8  * @author     Kohana Team
9  * @copyright  (c) 2007-2008 Kohana Team
10  * @license    http://kohanaphp.com/license.html
11  */
12 class Validation_Core extends ArrayObject {
13
14     // Unique "any field" key
15     protected $any_field;
16
17     // Array fields
18     protected $array_fields = array();
19
20     // Filters
21     protected $pre_filters = array();
22     protected $post_filters = array();
23
24     // Rules and callbacks
25     protected $rules = array();
26     protected $callbacks = array();
27
28     // Rules that are allowed to run on empty files
29     protected $empty_rules = array('required', 'matches');
30
31     // Errors
32     protected $errors = array();
33     protected $messages = array();
34
35     // Checks if there is data to validate.
36     protected $submitted;
37
38     /**
39      * Creates a new Validation instance.
40      *
41      * @param   array   array to use for validation
42      * @return  object
43      */
44     public static function factory($array = NULL)
45     {
46         return new Validation( ! is_array($array) ? $_POST : $array);
47     }
48
49     /**
50      * Sets the unique "any field" key and creates an ArrayObject from the
51      * passed array.
52      *
53      * @param   array   array to validate
54      * @return  void
55      */
56     public function __construct(array $array)
57     {
58         // Set a dynamic, unique "any field" key
59         $this->any_field = uniqid(NULL, TRUE);
60
61         // Test if there is any actual data
62         $this->submitted = (count($array) > 0);
63
64         parent::__construct($array, ArrayObject::ARRAY_AS_PROPS | ArrayObject::STD_PROP_LIST);
65     }
66
67     /**
68      * Test if the data has been submitted.
69      *
70      * @return  boolean
71      */
72     public function submitted($value = NULL)
73     {
74         if (is_bool($value))
75         {
76             $this->submitted = $value;
77         }
78
79         return $this->submitted;
80     }
81
82     /**
83      * Returns the ArrayObject values.
84      *
85      * @return  array
86      */
87     public function as_array()
88     {
89         return $this->getArrayCopy();
90     }
91
92     /**
93      * Returns the ArrayObject values, removing all inputs without rules.
94      *
95      * @return  array
96      */
97     public function safe_array()
98     {
99         // All the fields that are being validated
100         $all_fields = array_unique(array_merge
101         (
102             array_keys($this->pre_filters),
103             array_keys($this->rules),
104             array_keys($this->callbacks),
105             array_keys($this->post_filters)
106         ));
107
108         $safe = array();
109         foreach ($all_fields as $i => $field)
110         {
111             // Ignore "any field" key
112             if ($field === $this->any_field) continue;
113
114             if (isset($this->array_fields[$field]))
115             {
116                 // Use the key field
117                 $field = $this->array_fields[$field];
118             }
119
120             // Make sure all fields are defined
121             $safe[$field] = isset($this[$field]) ? $this[$field] : NULL;
122         }
123
124         return $safe;
125     }
126
127     /**
128      * Add additional rules that will forced, even for empty fields. All arguments
129      * passed will be appended to the list.
130      *
131      * @chainable
132      * @param   string   rule name
133      * @return  object
134      */
135     public function allow_empty_rules($rules)
136     {
137         // Any number of args are supported
138         $rules = func_get_args();
139
140         // Merge the allowed rules
141         $this->empty_rules = array_merge($this->empty_rules, $rules);
142
143         return $this;
144     }
145
146     /**
147      * Add a pre-filter to one or more inputs.
148      *
149      * @chainable
150      * @param   callback  filter
151      * @param   string    fields to apply filter to, use TRUE for all fields
152      * @return  object
153      */
154     public function pre_filter($filter, $field = TRUE)
155     {
156         if ( ! is_callable($filter))
157             throw new Kohana_Exception('validation.filter_not_callable');
158
159         $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter;
160
161         if ($field === TRUE)
162         {
163             // Handle "any field" filters
164             $fields = array($this->any_field);
165         }
166         else
167         {
168             // Add the filter to specific inputs
169             $fields = func_get_args();
170             $fields = array_slice($fields, 1);
171         }
172
173         foreach ($fields as $field)
174         {
175             if (strpos($field, '.') > 0)
176             {
177                 // Field keys
178                 $keys = explode('.', $field);
179
180                 // Add to array fields
181                 $this->array_fields[$field] = $keys[0];
182             }
183
184             // Add the filter to specified field
185             $this->pre_filters[$field][] = $filter;
186         }
187
188         return $this;
189     }
190
191     /**
192      * Add a post-filter to one or more inputs.
193      *
194      * @chainable
195      * @param   callback  filter
196      * @param   string    fields to apply filter to, use TRUE for all fields
197      * @return  object
198      */
199     public function post_filter($filter, $field = TRUE)
200     {
201         if ( ! is_callable($filter, TRUE))
202             throw new Kohana_Exception('validation.filter_not_callable');
203
204         $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter;
205
206         if ($field === TRUE)
207         {
208             // Handle "any field" filters
209             $fields = array($this->any_field);
210         }
211         else
212         {
213             // Add the filter to specific inputs
214             $fields = func_get_args();
215             $fields = array_slice($fields, 1);
216         }
217
218         foreach ($fields as $field)
219         {
220             if (strpos($field, '.') > 0)
221             {
222                 // Field keys
223                 $keys = explode('.', $field);
224
225                 // Add to array fields
226                 $this->array_fields[$field] = $keys[0];
227             }
228
229             // Add the filter to specified field
230             $this->post_filters[$field][] = $filter;
231         }
232
233         return $this;
234     }
235
236     /**
237      * Add rules to a field. Rules are callbacks or validation methods. Rules can
238      * only return TRUE or FALSE.
239      *
240      * @chainable
241      * @param   string    field name
242      * @param   callback  rules (unlimited number)
243      * @return  object
244      */
245     public function add_rules($field, $rules)
246     {
247         // Handle "any field" filters
248         ($field === TRUE) and $field = $this->any_field;
249
250         // Get the rules
251         $rules = func_get_args();
252         $rules = array_slice($rules, 1);
253
254         foreach ($rules as $rule)
255         {
256             // Rule arguments
257             $args = NULL;
258
259             if (is_string($rule))
260             {
261                 if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches))
262                 {
263                     // Split the rule into the function and args
264                     $rule = $matches[1];
265                     $args = preg_split('/(?<!\\\\),\s*/', $matches[2]);
266
267                     // Replace escaped comma with comma
268                     $args = str_replace('\,', ',', $args);
269                 }
270
271                 if (method_exists($this, $rule))
272                 {
273                     // Make the rule a valid callback
274                     $rule = array($this, $rule);
275                 }
276                 elseif (method_exists('valid', $rule))
277                 {
278                     // Make the rule a callback for the valid:: helper
279                     $rule = array('valid', $rule);
280                 }
281             }
282
283             if ( ! is_callable($rule, TRUE))
284                 throw new Kohana_Exception('validation.rule_not_callable');
285
286             $rule = (is_string($rule) AND strpos($rule, '::') !== FALSE) ? explode('::', $rule) : $rule;
287
288             if (strpos($field, '.') > 0)
289             {
290                 // Field keys
291                 $keys = explode('.', $field);
292
293                 // Add to array fields
294                 $this->array_fields[$field] = $keys[0];
295             }
296
297             // Add the rule to specified field
298             $this->rules[$field][] = array($rule, $args);
299         }
300
301         return $this;
302     }
303
304     /**
305      * Add callbacks to a field. Callbacks must accept the Validation object
306      * and the input name. Callback returns are not processed.
307      *
308      * @chainable
309      * @param   string     field name
310      * @param   callbacks  callbacks (unlimited number)
311      * @return  object
312      */
313     public function add_callbacks($field, $callbacks)
314     {
315         // Handle "any field" filters
316         ($field === TRUE) and $field = $this->any_field;
317
318         if (func_get_args() > 2)
319         {
320             // Multiple callback
321             $callbacks = array_slice(func_get_args(), 1);
322         }
323         else
324         {
325             // Only one callback
326             $callbacks = array($callbacks);
327         }
328
329         foreach ($callbacks as $callback)
330         {
331             if ( ! is_callable($callback, TRUE))
332                 throw new Kohana_Exception('validation.callback_not_callable');
333
334             $callback = (is_string($callback) AND strpos($callback, '::') !== FALSE) ? explode('::', $callback) : $callback;
335
336             if (strpos($field, '.') > 0)
337             {
338                 // Field keys
339                 $keys = explode('.', $field);
340
341                 // Add to array fields
342                 $this->array_fields[$field] = $keys[0];
343             }
344
345             // Add the callback to specified field
346             $this->callbacks[$field][] = $callback;
347         }
348
349         return $this;
350     }
351
352     /**
353      * Validate by processing pre-filters, rules, callbacks, and post-filters.
354      * All fields that have filters, rules, or callbacks will be initialized if
355      * they are undefined. Validation will only be run if there is data already
356      * in the array.
357      *
358      * @return bool
359      */
360     public function validate()
361     {
362         // All the fields that are being validated
363         $all_fields = array_unique(array_merge
364         (
365             array_keys($this->pre_filters),
366             array_keys($this->rules),
367             array_keys($this->callbacks),
368             array_keys($this->post_filters)
369         ));
370
371         // Copy the array from the object, to optimize multiple sets
372         $object_array = $this->getArrayCopy();
373
374         foreach ($all_fields as $i => $field)
375         {
376             if ($field === $this->any_field)
377             {
378                 // Remove "any field" from the list of fields
379                 unset($all_fields[$i]);
380                 continue;
381             }
382
383             if (substr($field, -2) === '.*')
384             {
385                 // Set the key to be an array
386                 Kohana::key_string_set($object_array, substr($field, 0, -2), array());
387             }
388             else
389             {
390                 // Set the key to be NULL
391                 Kohana::key_string_set($object_array, $field, NULL);
392             }
393         }
394
395         // Swap the array back into the object
396         $this->exchangeArray($object_array);
397
398         // Reset all fields to ALL defined fields
399         $all_fields = array_keys($this->getArrayCopy());
400
401         foreach ($this->pre_filters as $field => $calls)
402         {
403             foreach ($calls as $func)
404             {
405                 if ($field === $this->any_field)
406                 {
407                     foreach ($all_fields as $f)
408                     {
409                         // Process each filter
410                         $this[$f] = is_array($this[$f]) ? arr::map_recursive($func, $this[$f]) : call_user_func($func, $this[$f]);
411                     }
412                 }
413                 else
414                 {
415                     // Process each filter
416                     $this[$field] = is_array($this[$field]) ? arr::map_recursive($func, $this[$field]) : call_user_func($func, $this[$field]);
417                 }
418             }
419         }
420
421         if ($this->submitted === FALSE)
422             return FALSE;
423
424         foreach ($this->rules as $field => $calls)
425         {
426             foreach ($calls as $call)
427             {
428                 // Split the rule into function and args
429                 list($func, $args) = $call;
430
431                 if ($field === $this->any_field)
432                 {
433                     foreach ($all_fields as $f)
434                     {
435                         if (isset($this->array_fields[$f]))
436                         {
437                             // Use the field key
438                             $f_key = $this->array_fields[$f];
439
440                             // Prevent other rules from running when this field already has errors
441                             if ( ! empty($this->errors[$f_key])) break;
442
443                             // Don't process rules on empty fields
444                             if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f_key] == NULL)
445                                 continue;
446
447                             foreach ($this[$f_key] as $k => $v)
448                             {
449                                 if ( ! call_user_func($func, $this[$f_key][$k], $args))
450                                 {
451                                     // Run each rule
452                                     $this->errors[$f_key] = is_array($func) ? $func[1] : $func;
453                                 }
454                             }
455                         }
456                         else
457                         {
458                             // Prevent other rules from running when this field already has errors
459                             if ( ! empty($this->errors[$f])) break;
460
461                             // Don't process rules on empty fields
462                             if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f] == NULL)
463                                 continue;
464
465                             if ( ! call_user_func($func, $this[$f], $args))
466                             {
467                                 // Run each rule
468                                 $this->errors[$f] = is_array($func) ? $func[1] : $func;
469                             }
470                         }
471                     }
472                 }
473                 else
474                 {
475                     if (isset($this->array_fields[$field]))
476                     {
477                         // Use the field key
478                         $field_key = $this->array_fields[$field];
479
480                         // Prevent other rules from running when this field already has errors
481                         if ( ! empty($this->errors[$field_key])) break;
482
483                         // Don't process rules on empty fields
484                         if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field_key] == NULL)
485                             continue;
486
487                         foreach ($this[$field_key] as $k => $val)
488                         {
489                             if ( ! call_user_func($func, $this[$field_key][$k], $args))
490                             {
491                                 // Run each rule
492                                 $this->errors[$field_key] = is_array($func) ? $func[1] : $func;
493
494                                 // Stop after an error is found
495                                 break 2;
496                             }
497                         }
498                     }
499                     else
500                     {
501                         // Prevent other rules from running when this field already has errors
502                         if ( ! empty($this->errors[$field])) break;
503
504                         // Don't process rules on empty fields
505                         if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field] == NULL)
506                             continue;
507
508                         if ( ! call_user_func($func, $this[$field], $args))
509                         {
510                             // Run each rule
511                             $this->errors[$field] = is_array($func) ? $func[1] : $func;
512
513                             // Stop after an error is found
514                             break;
515                         }
516                     }
517                 }
518             }
519         }
520
521         foreach ($this->callbacks as $field => $calls)
522         {
523             foreach ($calls as $func)
524             {
525                 if ($field === $this->any_field)
526                 {
527                     foreach ($all_fields as $f)
528                     {
529                         // Execute the callback
530                         call_user_func($func, $this, $f);
531
532                         // Stop after an error is found