Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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: sfForm.class.php 29678 2010-05-30 14:38:42Z Kris.Wallsmith $
27
 */
28
class 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
}