PageRenderTime 5ms CodeModel.GetById 27ms app.highlight 19ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/vendor/symfony/lib/form/sfForm.class.php

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