PageRenderTime 206ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

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