Development

/branches/1.2/lib/form/sfForm.class.php

You must first sign up to be able to contribute.

root/branches/1.2/lib/form/sfForm.class.php

Revision 20298, 30.3 kB (checked in by fabien, 5 years ago)

[1.2, 1.3] fixed sfForm::setDefaults() ignores individual CSRF protection (closes #6864)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * sfForm represents a form.
13  *
14  * A forms is composed of a validator schema and a widget form schema.
15  *
16  * sfForm also takes care of CSRF protection by default.
17  *
18  * @package    symfony
19  * @subpackage form
20  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
21  * @version    SVN: $Id$
22  */
23 class sfForm implements ArrayAccess, Iterator, Countable
24 {
25   protected static
26     $CSRFProtection    = false,
27     $CSRFSecret        = null,
28     $CSRFFieldName     = '_csrf_token',
29     $toStringException = null;
30
31   protected
32     $widgetSchema    = null,
33     $validatorSchema = null,
34     $errorSchema     = null,
35     $formFieldSchema = null,
36     $formFields      = array(),
37     $isBound         = false,
38     $taintedValues   = array(),
39     $taintedFiles    = array(),
40     $values          = null,
41     $defaults        = array(),
42     $fieldNames      = array(),
43     $options         = array(),
44     $count           = 0,
45     $embeddedForms   = array();
46
47   /**
48    * Constructor.
49    *
50    * @param array  $defaults   An array of field default values
51    * @param array  $options    An array of options
52    * @param string $CSRFSecret A CSRF secret (false to disable CSRF protection, null to use the global CSRF secret)
53    */
54   public function __construct($defaults = array(), $options = array(), $CSRFSecret = null)
55   {
56     $this->setDefaults($defaults);
57     $this->options = $options;
58
59     $this->validatorSchema = new sfValidatorSchema();
60     $this->widgetSchema    = new sfWidgetFormSchema();
61     $this->errorSchema     = new sfValidatorErrorSchema($this->validatorSchema);
62
63     $this->setup();
64     $this->configure();
65
66     $this->addCSRFProtection($CSRFSecret);
67     $this->resetFormFields();
68   }
69
70   /**
71    * Returns a string representation of the form.
72    *
73    * @return string A string representation of the form
74    *
75    * @see render()
76    */
77   public function __toString()
78   {
79     try
80     {
81       return $this->render();
82     }
83     catch (Exception $e)
84     {
85       self::setToStringException($e);
86
87       // we return a simple Exception message in case the form framework is used out of symfony.
88       return 'Exception: '.$e->getMessage();
89     }
90   }
91
92   /**
93    * Configures the current form.
94    */
95   public function configure()
96   {
97   }
98
99   /**
100    * Setups the current form.
101    *
102    * This method is overridden by generator.
103    *
104    * If you want to do something at initialization, you have to override the configure() method.
105    *
106    * @see configure()
107    */
108   public function setup()
109   {
110   }
111
112   /**
113    * Renders the widget schema associated with this form.
114    *
115    * @param array $attributes An array of HTML attributes
116    *
117    * @return string The rendered widget schema
118    */
119   public function render($attributes = array())
120   {
121     return $this->getFormFieldSchema()->render($attributes);
122   }
123
124   /**
125    * Renders the widget schema using a specific form formatter
126    *
127    * @param string $formatterName The form formatter name
128    * @param array  $attributes    An array of HTML attributes
129    *
130    * @return string The rendered widget schema
131    */
132   public function renderUsing($formatterName, $attributes = array())
133   {
134     $currentFormatterName = $this->widgetSchema->getFormFormatterName();
135
136     $this->widgetSchema->setFormFormatterName($formatterName);
137
138     $output = $this->render($attributes);
139
140     $this->widgetSchema->setFormFormatterName($currentFormatterName);
141
142     return $output;
143   }
144
145   /**
146    * Renders hidden form fields.
147    *
148    * @return string
149    */
150   public function renderHiddenFields()
151   {
152     $output = '';
153     foreach ($this->getFormFieldSchema() as $name => $field)
154     {
155       if ($field->isHidden())
156       {
157         $output .= $field->render();
158       }
159     }
160
161     return $output;
162   }
163
164   /**
165    * Renders global errors associated with this form.
166    *
167    * @return string The rendered global errors
168    */
169   public function renderGlobalErrors()
170   {
171     return $this->widgetSchema->getFormFormatter()->formatErrorsForRow($this->getGlobalErrors());
172   }
173
174   /**
175    * Returns true if the form has some global errors.
176    *
177    * @return Boolean true if the form has some global errors, false otherwise
178    */
179   public function hasGlobalErrors()
180   {
181     return (Boolean) count($this->getGlobalErrors());
182   }
183
184   /**
185    * Gets the global errors associated with the form.
186    *
187    * @return array An array of global errors
188    */
189   public function getGlobalErrors()
190   {
191     return $this->widgetSchema->getGlobalErrors($this->getErrorSchema());
192   }
193
194   /**
195    * Binds the form with input values.
196    *
197    * It triggers the validator schema validation.
198    *
199    * @param array $taintedValues An array of input values
200    * @param array $taintedFiles  An array of uploaded files (in the $_FILES or $_GET format)
201    */
202   public function bind(array $taintedValues = null, array $taintedFiles = null)
203   {
204     $this->taintedValues = $taintedValues;
205     $this->taintedFiles  = $taintedFiles;
206     $this->isBound = true;
207     $this->resetFormFields();
208
209     if (is_null($this->taintedValues))
210     {
211       $this->taintedValues = array();
212     }
213
214     if (is_null($this->taintedFiles))
215     {
216       if ($this->isMultipart())
217       {
218         throw new InvalidArgumentException('This form is multipart, which means you need to supply a files array as the bind() method second argument.');
219       }
220
221       $this->taintedFiles = array();
222     }
223
224     try
225     {
226       $this->values = $this->validatorSchema->clean(self::deepArrayUnion($this->taintedValues, self::convertFileInformation($this->taintedFiles)));
227       $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);
228
229       // remove CSRF token
230       unset($this->values[self::$CSRFFieldName]);
231     }
232     catch (sfValidatorErrorSchema $e)
233     {
234       $this->values = array();
235       $this->errorSchema = $e;
236     }
237   }
238
239   /**
240    * Returns true if the form is bound to input values.
241    *
242    * @return Boolean true if the form is bound to input values, false otherwise
243    */
244   public function isBound()
245   {
246     return $this->isBound;
247   }
248
249   /**
250    * Returns the submitted tainted values.
251    *
252    * @return array An array of tainted values
253    */
254   public function getTaintedValues()
255   {
256     if (!$this->isBound)
257     {
258       return array();
259     }
260
261     return $this->taintedValues;
262   }
263
264   /**
265    * Returns true if the form is valid.
266    *
267    * It returns false if the form is not bound.
268    *
269    * @return Boolean true if the form is valid, false otherwise
270    */
271   public function isValid()
272   {
273     if (!$this->isBound)
274     {
275       return false;
276     }
277
278     return 0 == count($this->errorSchema);
279   }
280
281   /**
282    * Returns true if the form has some errors.
283    *
284    * It returns false if the form is not bound.
285    *
286    * @return Boolean true if the form has no errors, false otherwise
287    */
288   public function hasErrors()
289   {
290     if (!$this->isBound)
291     {
292       return false;
293     }
294
295     return count($this->errorSchema) > 0;
296   }
297
298   /**
299    * Returns the array of cleaned values.
300    *
301    * If the form is not bound, it returns an empty array.
302    *
303    * @return array An array of cleaned values
304    */
305   public function getValues()
306   {
307     return $this->isBound ? $this->values : array();
308   }
309
310   /**
311    * Returns a cleaned value by field name.
312    *
313    * If the form is not bound, it will return null.
314    *
315    * @param string $field The name of the value required
316    * @return string The cleaned value
317    */
318   public function getValue($field)
319   {
320     return ($this->isBound && isset($this->values[$field])) ? $this->values[$field] : null;
321   }
322
323   /**
324    * Returns the array name under which user data can retrieved.
325    *
326    * If the user data is not stored under an array, it returns null.
327    *
328    * @return string The name
329    */
330   public function getName()
331   {
332     if ('%s' == $nameFormat = $this->widgetSchema->getNameFormat())
333     {
334       return false;
335     }
336
337     return str_replace('[%s]', '', $nameFormat);
338   }
339
340   /**
341    * Gets the error schema associated with the form.
342    *
343    * @return sfValidatorErrorSchema A sfValidatorErrorSchema instance
344    */
345   public function getErrorSchema()
346   {
347     return $this->errorSchema;
348   }
349
350   /**
351    * Embeds a sfForm into the current form.
352    *
353    * @param string $name      The field name
354    * @param sfForm $form      A sfForm instance
355    * @param string $decorator A HTML decorator for the embedded form
356    */
357   public function embedForm($name, sfForm $form, $decorator = null)
358   {
359     $name = (string) $name;
360     if (true === $this->isBound() || true === $form->isBound())
361     {
362       throw new LogicException('A bound form cannot be embedded');
363     }
364
365     $this->embeddedForms[$name] = $form;
366
367     $form = clone $form;
368     unset($form[self::$CSRFFieldName]);
369
370     $widgetSchema = $form->getWidgetSchema();
371
372     $this->setDefault($name, $form->getDefaults());
373
374     $decorator = is_null($decorator) ? $widgetSchema->getFormFormatter()->getDecoratorFormat() : $decorator;
375
376     $this->widgetSchema[$name] = new sfWidgetFormSchemaDecorator($widgetSchema, $decorator);
377     $this->validatorSchema[$name] = $form->getValidatorSchema();
378
379     $this->resetFormFields();
380   }
381
382   /**
383    * Embeds a sfForm into the current form n times.
384    *
385    * @param string  $name           The field name
386    * @param sfForm  $form           A sfForm instance
387    * @param integer $n              The number of times to embed the form
388    * @param string  $decorator      A HTML decorator for the main form around embedded forms
389    * @param string  $innerDecorator A HTML decorator for each embedded form
390    * @param array   $options        Options for schema
391    * @param array   $attributes     Attributes for schema
392    * @param array   $labels         Labels for schema
393    */
394   public function embedFormForEach($name, sfForm $form, $n, $decorator = null, $innerDecorator = null, $options = array(), $attributes = array(), $labels = array())
395   {
396     if (true === $this->isBound() || true === $form->isBound())
397     {
398       throw new LogicException('A bound form cannot be embedded');
399     }
400
401     $this->embeddedForms[$name] = new sfForm();
402
403     $form = clone $form;
404     unset($form[self::$CSRFFieldName]);
405
406     $widgetSchema = $form->getWidgetSchema();
407
408     // generate default values
409     $defaults = array();
410     for ($i = 0; $i < $n; $i++)
411     {
412       $defaults[$i] = $form->getDefaults();
413
414       $this->embeddedForms[$name]->embedForm($i, $form);
415     }
416
417     $this->setDefault($name, $defaults);
418
419     $decorator = is_null($decorator) ? $widgetSchema->getFormFormatter()->getDecoratorFormat() : $decorator;
420     $innerDecorator = is_null($innerDecorator) ? $widgetSchema->getFormFormatter()->getDecoratorFormat() : $innerDecorator;
421
422     $this->widgetSchema[$name] = new sfWidgetFormSchemaDecorator(new sfWidgetFormSchemaForEach(new sfWidgetFormSchemaDecorator($widgetSchema, $innerDecorator), $n, $options, $attributes), $decorator);
423     $this->validatorSchema[$name] = new sfValidatorSchemaForEach($form->getValidatorSchema(), $n);
424
425     // generate labels
426     for ($i = 0; $i < $n; $i++)
427     {
428       if (!isset($labels[$i]))
429       {
430         $labels[$i] = sprintf('%s (%s)', $this->widgetSchema->getFormFormatter()->generateLabelName($name), $i);
431       }
432     }
433
434     $this->widgetSchema[$name]->setLabels($labels);
435
436     $this->resetFormFields();
437   }
438
439   /**
440    * Gets the list of embedded forms.
441    *
442    * @return array An array of embedded forms
443    */
444   public function getEmbeddedForms()
445   {
446     return $this->embeddedForms;
447   }
448
449   /**
450    * Merges current form widget and validator schemas with the ones from the
451    * sfForm object passed as parameter. Please note it also merge defaults.
452    *
453    * @param sfForm $form The sfForm instance to merge with current form
454    *
455    * @throws LogicException If one of the form has already been bound
456    */
457   public function mergeForm(sfForm $form)
458   {
459     if (true === $this->isBound() || true === $form->isBound())
460     {
461       throw new LogicException('A bound form cannot be merged');
462     }
463
464     $form = clone $form;
465     unset($form[self::$CSRFFieldName]);
466
467     $this->defaults = array_merge($this->defaults, $form->getDefaults());
468
469     foreach ($form->getWidgetSchema()->getPositions() as $field)
470     {
471       $this->widgetSchema[$field] = $form->getWidget($field);
472     }
473
474     foreach ($form->getValidatorSchema()->getFields() as $field => $validator)
475     {
476       $this->validatorSchema[$field] = $validator;
477     }
478
479     $this->getWidgetSchema()->setLabels(array_merge($this->getWidgetSchema()->getLabels(), $form->getWidgetSchema()->getLabels()));
480     $this->getWidgetSchema()->setHelps(array_merge($this->getWidgetSchema()->getHelps(), $form->getWidgetSchema()->getHelps()));
481
482     $this->mergePreValidator($form->getValidatorSchema()->getPreValidator());
483     $this->mergePostValidator($form->getValidatorSchema()->getPostValidator());
484
485     $this->resetFormFields();
486   }
487
488   /**
489    * Merges a validator with the current pre validators.
490    *
491    * @param sfValidatorBase $validator A validator to be merged
492    */
493   public function mergePreValidator(sfValidatorBase $validator = null)
494   {
495     if (is_null($validator))
496     {
497       return;
498     }
499
500     if (is_null($this->validatorSchema->getPreValidator()))
501     {
502       $this->validatorSchema->setPreValidator($validator);
503     }
504     else
505     {
506       $this->validatorSchema->setPreValidator(new sfValidatorAnd(array(
507         $this->validatorSchema->getPreValidator(),
508         $validator,
509       )));
510     }
511   }
512
513   /**
514    * Merges a validator with the current post validators.
515    *
516    * @param sfValidatorBase $validator A validator to be merged
517    */
518   public function mergePostValidator(sfValidatorBase $validator = null)
519   {
520     if (is_null($validator))
521     {
522       return;
523     }
524
525     if (is_null($this->validatorSchema->getPostValidator()))
526     {
527       $this->validatorSchema->setPostValidator($validator);
528     }
529     else
530     {
531       $this->validatorSchema->setPostValidator(new sfValidatorAnd(array(
532         $this->validatorSchema->getPostValidator(),
533         $validator,
534       )));
535     }
536   }
537
538   /**
539    * Sets the validators associated with this form.
540    *
541    * @param array $validators An array of named validators
542    */
543   public function setValidators(array $validators)
544   {
545     $this->setValidatorSchema(new sfValidatorSchema($validators));
546   }
547
548   /**
549    * Set a validator for the given field name.
550    *
551    * @param string      $name      The field name
552    * @param sfValidator $validator The validator
553    */
554   public function setValidator($name, sfValidatorBase $validator)
555   {
556     $this->validatorSchema[$name] = $validator;
557
558     $this->resetFormFields();
559   }
560
561   /**
562    * Gets a validator for the given field name.
563    *
564    * @param string $name The field name
565    *
566    * @return sfValidator $validator The validator
567    */
568   public function getValidator($name)
569   {
570     if (!isset($this->validatorSchema[$name]))
571     {
572       throw new InvalidArgumentException(sprintf('The validator "%s" does not exist.', $name));
573     }
574
575     return $this->validatorSchema[$name];
576   }
577
578   /**
579    * Sets the validator schema associated with this form.
580    *
581    * @param sfValidatorSchema $validatorSchema A sfValidatorSchema instance
582    */
583   public function setValidatorSchema(sfValidatorSchema $validatorSchema)
584   {
585     $this->validatorSchema = $validatorSchema;
586
587     $this->resetFormFields();
588   }
589
590   /**
591    * Gets the validator schema associated with this form.
592    *
593    * @return sfValidatorSchema A sfValidatorSchema instance
594    */
595   public function getValidatorSchema()
596   {
597     return $this->validatorSchema;
598   }
599
600   /**
601    * Sets the widgets associated with this form.
602    *
603    * @param array $widgets An array of named widgets
604    */
605   public function setWidgets(array $widgets)
606   {
607     $this->setWidgetSchema(new sfWidgetFormSchema($widgets));
608   }
609
610   /**
611    * Set a widget for the given field name.
612    *
613    * @param string       $name   The field name
614    * @param sfWidgetForm $widget The widget
615    */
616   public function setWidget($name, sfWidgetForm $widget)
617   {
618     $this->widgetSchema[$name] = $widget;
619
620     $this->resetFormFields();
621   }
622
623   /**
624    * Gets a widget for the given field name.
625    *
626    * @param string $name The field name
627    *
628    * @return sfWidgetForm $widget The widget
629    */
630   public function getWidget($name)
631   {
632     if (!isset($this->widgetSchema[$name]))
633     {
634       throw new InvalidArgumentException(sprintf('The widget "%s" does not exist.', $name));
635     }
636
637     return $this->widgetSchema[$name];
638   }
639
640   /**
641    * Sets the widget schema associated with this form.
642    *
643    * @param sfWidgetFormSchema $widgetSchema A sfWidgetFormSchema instance
644    */
645   public function setWidgetSchema(sfWidgetFormSchema $widgetSchema)
646   {
647     $this->widgetSchema = $widgetSchema;
648
649     $this->resetFormFields();
650   }
651
652   /**
653    * Gets the widget schema associated with this form.
654    *
655    * @return sfWidgetFormSchema A sfWidgetFormSchema instance
656    */
657   public function getWidgetSchema()
658   {
659     return $this->widgetSchema;
660   }
661
662   /**
663    * Gets the stylesheet paths associated with the form.
664    *
665    * @return array An array of stylesheet paths
666    */
667   public function getStylesheets()
668   {
669     return $this->widgetSchema->getStylesheets();
670   }
671
672   /**
673    * Gets the JavaScript paths associated with the form.
674    *
675    * @return array An array of JavaScript paths
676    */
677   public function getJavaScripts()
678   {
679     return $this->widgetSchema->getJavaScripts();
680   }
681
682   /**
683    * Sets an option value.
684    *
685    * @param string $name  The option name
686    * @param mixed  $value The default value
687    */
688   public function setOption($name, $value)
689   {
690     $this->options[$name] = $value;
691   }
692
693   /**
694    * Gets an option value.
695    *
696    * @param string $name    The option name
697    * @param mixed  $default The default value (null by default)
698    *
699    * @param mixed  The default value
700    */
701   public function getOption($name, $default = null)
702   {
703     return isset($this->options[$name]) ? $this->options[$name] : $default;
704   }
705
706   /**
707    * Sets a default value for a form field.
708    *
709    * @param string $name    The field name
710    * @param mixed  $default The default value
711    */
712   public function setDefault($name, $default)
713   {
714     $this->defaults[$name] = $default;
715
716     $this->resetFormFields();
717   }
718
719   /**
720    * Gets a default value for a form field.
721    *
722    * @param string $name The field name
723    *
724    * @param mixed  The default value
725    */
726   public function getDefault($name)
727   {
728     return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
729   }
730
731   /**
732    * Returns true if the form has a default value for a form field.
733    *
734    * @param string $name The field name
735    *
736    * @param Boolean true if the form has a default value for this field, false otherwise
737    */
738   public function hasDefault($name)
739   {
740     return array_key_exists($name, $this->defaults);
741   }
742
743   /**
744    * Sets the default values for the form.
745    *
746    * The default values are only used if the form is not bound.
747    *
748    * @param array $defaults An array of default values
749    */
750   public function setDefaults($defaults)
751   {
752     $this->defaults = is_null($defaults) ? array() : $defaults;
753
754     if ($this->isCSRFProtected())
755     {
756       $this->setDefault(self::$CSRFFieldName, $this->getCSRFToken(self::$CSRFSecret));
757     }
758
759     $this->resetFormFields();
760   }
761
762   /**
763    * Gets the default values for the form.
764    *
765    * @return array An array of default values
766    */
767   public function getDefaults()
768   {
769     return $this->defaults;
770   }
771
772   /**
773    * Adds CSRF protection to the current form.
774    *
775    * @param string $secret The secret to use to compute the CSRF token
776    */
777   public function addCSRFProtection($secret)
778   {
779     if (false === $secret || (is_null($secret) && !self::$CSRFProtection))
780     {
781       return;
782     }
783
784     if (is_null($secret))
785     {
786       if (is_null(self::$CSRFSecret))
787       {
788         self::$CSRFSecret = md5(__FILE__.php_uname());
789       }
790
791       $secret = self::$CSRFSecret;
792     }
793
794     $token = $this->getCSRFToken($secret);
795
796     $this->validatorSchema[self::$CSRFFieldName] = new sfValidatorCSRFToken(array('token' => $token));
797     $this->widgetSchema[self::$CSRFFieldName] = new sfWidgetFormInputHidden();
798     $this->setDefault(self::$CSRFFieldName, $token);
799   }
800
801   /**
802    * Returns a CSRF token, given a secret.
803    *
804    * If you want to change the algorithm used to compute the token, you
805    * can override this method.
806    *
807    * @param string $secret The secret string to use (null to use the current secret)
808    *
809    * @return string A token string
810    */
811   public function getCSRFToken($secret = null)
812   {
813     if (is_null($secret))
814     {
815       $secret = self::$CSRFSecret;
816     }
817
818     return md5($secret.session_id().get_class($this));
819   }
820
821   /**
822    * @return true if this form is CSRF protected
823    */
824   public function isCSRFProtected()
825   {
826     return !is_null($this->validatorSchema[self::$CSRFFieldName]);
827   }
828
829   /**
830    * Sets the CSRF field name.
831    *
832    * @param string $name The CSRF field name
833    */
834   static public function setCSRFFieldName($name)
835   {
836     self::$CSRFFieldName = $name;
837   }
838
839   /**
840    * Gets the CSRF field name.
841    *
842    * @return string The CSRF field name
843    */
844   static public function getCSRFFieldName()
845   {
846     return self::$CSRFFieldName;
847   }
848
849   /**
850    * Enables CSRF protection for all forms.
851    *
852    * The given secret will be used for all forms, except if you pass a secret in the constructor.
853    * Even if a secret is automatically generated if you don't provide a secret, you're strongly advised
854    * to provide one by yourself.
855    *
856    * @param string $secret A secret to use when computing the CSRF token
857    */
858   static public function enableCSRFProtection($secret = null)
859   {
860     if (false === $secret)
861     {
862       return self::disableCSRFProtection();
863     }
864
865     self::$CSRFProtection = true;
866
867     if (!is_null($secret))
868     {
869       self::$CSRFSecret = $secret;
870     }
871   }
872
873   /**
874    * Disables CSRF protection for all forms.
875    */
876   static public function disableCSRFProtection()
877   {
878     self::$CSRFProtection = false;
879   }
880
881   /**
882    * Returns true if the form is multipart.
883    *
884    * @return Boolean true if the form is multipart
885    */
886   public function isMultipart()
887   {
888     return $this->widgetSchema->needsMultipartForm();
889   }
890
891   /**
892    * Renders the form tag.
893    *
894    * This methods only renders the opening form tag.
895    * You need to close it after the form rendering.
896    *
897    * This method takes into account the multipart widgets
898    * and converts PUT and DELETE methods to a hidden field
899    * for later processing.
900    *
901    * @param string $url        The URL for the action
902    * @param array  $attributes An array of HTML attributes
903    *
904    * @return string An HTML representation of the opening form tag
905    */
906   public function renderFormTag($url, array $attributes = array())
907   {
908     $attributes['action'] = $url;
909     $attributes['method'] = isset($attributes['method']) ? strtolower($attributes['method']) : 'post';
910     if ($this->isMultipart())
911     {
912       $attributes['enctype'] = 'multipart/form-data';
913     }
914
915     $html = '';
916     if (!in_array($attributes['method'], array('get', 'post')))
917     {
918       $html = $this->getWidgetSchema()->renderTag('input', array('type' => 'hidden', 'name' => 'sf_method', 'value' => $attributes['method'], 'id' => false));
919       $attributes['method'] = 'post';
920     }
921
922     return sprintf('<form%s>', $this->getWidgetSchema()->attributesToHtml($attributes)).$html;
923   }
924
925   public function resetFormFields()
926   {
927     $this->formFields = array();
928     $this->formFieldSchema = null;
929   }
930
931   /**
932    * Returns true if the bound field exists (implements the ArrayAccess interface).
933    *
934    * @param string $name The name of the bound field
935    *
936    * @return Boolean true if the widget exists, false otherwise
937    */
938   public function offsetExists($name)
939   {
940     return isset($this->widgetSchema[$name]);
941   }
942
943   /**
944    * Returns the form field associated with the name (implements the ArrayAccess interface).
945    *
946    * @param string $name The offset of the value to get
947    *
948    * @return sfFormField A form field instance
949    */
950   public function offsetGet($name)
951   {
952     if (!isset($this->formFields[$name]))
953     {
954       if (!$widget = $this->widgetSchema[$name])
955       {
956         throw new InvalidArgumentException(sprintf('Widget "%s" does not exist.', $name));
957       }
958
959       if ($this->isBound)
960       {
961         $value = isset($this->taintedValues[$name]) ? $this->taintedValues[$name] : null;
962       }
963       else if (isset($this->defaults[$name]))
964       {
965         $value = $this->defaults[$name];
966       }
967       else
968       {
969         $value = $widget instanceof sfWidgetFormSchema ? $widget->getDefaults() : $widget->getDefault();
970       }
971
972       $class = $widget instanceof sfWidgetFormSchema ? 'sfFormFieldSchema' : 'sfFormField';
973
974       $this->formFields[$name] = new $class($widget, $this->getFormFieldSchema(), $name, $value, $this->errorSchema[$name]);
975     }
976
977     return $this->formFields[$name];
978   }
979
980   /**
981    * Throws an exception saying that values cannot be set (implements the ArrayAccess interface).
982    *
983    * @param string $offset (ignored)
984    * @param string $value (ignored)
985    *
986    * @throws <b>LogicException</b>
987    */
988   public function offsetSet($offset, $value)
989   {
990     throw new LogicException('Cannot update form fields.');
991   }
992
993   /**
994    * Removes a field from the form.
995    *
996    * It removes the widget and the validator for the given field.
997    *
998    * @param string $offset The field name
999    */
1000   public function offsetUnset($offset)
1001   {
1002     unset(
1003       $this->widgetSchema[$offset],
1004       $this->validatorSchema[$offset],
1005       $this->defaults[$offset],
1006       $this->taintedValues[$offset],
1007       $this->values[$offset],
1008       $this->embeddedForms[$offset]
1009     );
1010
1011     $this->resetFormFields();
1012   }
1013
1014   /**
1015    * Returns a form field for the main widget schema.
1016    *
1017    * @return sfFormFieldSchema A sfFormFieldSchema instance
1018    */
1019   public function getFormFieldSchema()
1020   {
1021     if (is_null($this->formFieldSchema))
1022     {
1023       $values = $this->isBound ? $this->taintedValues : array_merge($this->widgetSchema->getDefaults(), $this->defaults);
1024
1025       $this->formFieldSchema = new sfFormFieldSchema($this->widgetSchema, null, null, $values, $this->errorSchema);
1026     }
1027
1028     return $this->formFieldSchema;
1029   }
1030
1031   /**
1032    * Resets the field names array to the beginning (implements the Iterator interface).
1033    */
1034   public function rewind()
1035   {
1036     $this->fieldNames = $this->widgetSchema->getPositions();
1037
1038     reset($this->fieldNames);
1039     $this->count = count($this->fieldNames);
1040   }
1041
1042   /**
1043    * Gets the key associated with the current form field (implements the Iterator interface).
1044    *
1045    * @return string The key
1046    */
1047   public function key()
1048   {
1049     return current($this->fieldNames);
1050   }
1051
1052   /**
1053    * Returns the current form field (implements the Iterator interface).
1054    *
1055    * @return mixed The escaped value
1056    */
1057   public function current()
1058   {
1059     return $this[current($this->fieldNames)];
1060   }
1061
1062   /**
1063    * Moves to the next form field (implements the Iterator interface).
1064    */
1065   public function next()
1066   {
1067     next($this->fieldNames);
1068     --$this->count;
1069   }
1070
1071   /**
1072    * Returns true if the current form field is valid (implements the Iterator interface).
1073    *
1074    * @return boolean The validity of the current element; true if it is valid
1075    */
1076   public function valid()
1077   {
1078     return $this->count > 0;
1079   }
1080
1081   /**
1082    * Returns the number of form fields (implements the Countable interface).
1083    *
1084    * @return integer The number of embedded form fields
1085    */
1086   public function count()
1087   {
1088     return count($this->getFormFieldSchema());
1089   }
1090
1091   /**
1092    * Converts uploaded file array to a format following the $_GET and $POST naming convention.
1093    *
1094    * It's safe to pass an already converted array, in which case this method just returns the original array unmodified.
1095    *
1096    * @param array $taintedFiles An array representing uploaded file information
1097    *
1098    * @return array An array of re-ordered uploaded file information
1099    */
1100   static public function convertFileInformation(array $taintedFiles)
1101   {
1102     $files = array();
1103     foreach ($taintedFiles as $key => $data)
1104     {
1105       $files[$key] = self::fixPhpFilesArray($data);
1106     }
1107
1108     return $files;
1109   }
1110
1111   static protected function fixPhpFilesArray($data)
1112   {
1113     $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
1114     $keys = array_keys($data);
1115     sort($keys);
1116
1117     if ($fileKeys != $keys || !isset($data['name']) || !is_array($data['name']))
1118     {
1119       return $data;
1120     }
1121
1122     $files = $data;
1123     foreach ($fileKeys as $k)
1124     {
1125       unset($files[$k]);
1126     }
1127     foreach (array_keys($data['name']) as $key)
1128     {
1129       $files[$key] = self::fixPhpFilesArray(array(
1130         'error'    => $data['error'][$key],
1131         'name'     => $data['name'][$key],
1132         'type'     => $data['type'][$key],
1133         'tmp_name' => $data['tmp_name'][$key],
1134         'size'     => $data['size'][$key],
1135       ));
1136     }
1137
1138     return $files;
1139   }
1140
1141   /**
1142    * Returns true if a form thrown an exception in the __toString() method
1143    *
1144    * This is a hack needed because PHP does not allow to throw exceptions in __toString() magic method.
1145    *
1146    * @return boolean
1147    */
1148   static public function hasToStringException()
1149   {
1150     return !is_null(self::$toStringException);
1151   }
1152
1153   /**
1154    * Gets the exception if one was thrown in the __toString() method.
1155    *
1156    * This is a hack needed because PHP does not allow to throw exceptions in __toString() magic method.
1157    *
1158    * @return Exception
1159    */
1160   static public function getToStringException()
1161   {
1162     return self::$toStringException;
1163   }
1164
1165   /**
1166    * Sets an exception thrown by the __toString() method.
1167    *
1168    * This is a hack needed because PHP does not allow to throw exceptions in __toString() magic method.
1169    *
1170    * @param Exception $e The exception thrown by __toString()
1171    */
1172   static public function setToStringException(Exception $e)
1173   {
1174     if (is_null(self::$toStringException))
1175     {
1176       self::$toStringException = $e;
1177     }
1178   }
1179
1180   public function __clone()
1181   {
1182     $this->widgetSchema    = clone $this->widgetSchema;
1183     $this->validatorSchema = clone $this->validatorSchema;
1184
1185     // we rebind the cloned form because Exceptions are not clonable
1186     if ($this->isBound())
1187     {
1188       $this->bind($this->taintedValues, $this->taintedFiles);
1189     }
1190   }
1191
1192   /**
1193    * Merges two arrays without reindexing numeric keys.
1194    *
1195    * @param array $array1 An array to merge
1196    * @param array $array2 An array to merge
1197    *
1198    * @return array The merged array
1199    */
1200   static protected function deepArrayUnion($array1, $array2)
1201   {
1202     foreach ($array2 as $key => $value)
1203     {
1204       if (is_array($value) && isset($array1[$key]) && is_array($array1[$key]))
1205       {
1206         $array1[$key] = self::deepArrayUnion($array1[$key], $value);
1207       }
1208       else
1209       {
1210         $array1[$key] = $value;
1211       }
1212     }
1213
1214     return $array1;
1215   }
1216 }
1217
Note: See TracBrowser for help on using the browser.