PageRenderTime 160ms CodeModel.GetById 46ms app.highlight 86ms RepoModel.GetById 0ms app.codeStats 2ms

/lib/form/sfForm.class.php

https://github.com/bheneka/gitta
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}