sfForm.class.php 31 KB

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