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