PageRenderTime 55ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/Nette/Forms/Form.php

https://github.com/DocX/nette
PHP | 692 lines | 302 code | 166 blank | 224 comment | 38 complexity | bf17cbb56cf68400fc46cd2ddc2d1381 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * This source file is subject to the "Nette license" that is bundled
  8. * with this package in the file license.txt.
  9. *
  10. * For more information please see http://nettephp.com
  11. *
  12. * @copyright Copyright (c) 2004, 2009 David Grudl
  13. * @license http://nettephp.com/license Nette license
  14. * @link http://nettephp.com
  15. * @category Nette
  16. * @package Nette\Forms
  17. */
  18. /*namespace Nette\Forms;*/
  19. require_once dirname(__FILE__) . '/../Forms/FormContainer.php';
  20. /**
  21. * Creates, validates and renders HTML forms.
  22. *
  23. * @author David Grudl
  24. * @copyright Copyright (c) 2004, 2009 David Grudl
  25. * @package Nette\Forms
  26. *
  27. * @example forms/basic-example.php Form definition using fluent interfaces
  28. * @example forms/manual-rendering.php Manual form rendering and separated form and rules definition
  29. * @example forms/localization.php Localization (with Zend_Translate)
  30. * @example forms/custom-rendering.php Custom form rendering
  31. * @example forms/custom-validator.php How to use custom validator
  32. * @example forms/naming-containers.php How to use naming containers
  33. * @example forms/CSRF-protection.php How to use Cross-Site Request Forgery (CSRF) form protection
  34. * @example forms/custom-encoding.php How to change charset
  35. *
  36. * @property string $action
  37. * @property string $method
  38. * @property-read array $groups
  39. * @property-read array $httpData
  40. * @property string $encoding
  41. * @property Nette\ITranslator $translator
  42. * @property-read array $errors
  43. * @property-read Nette\Web\Html $elementPrototype
  44. * @property IFormRenderer $renderer
  45. * @property-read boold $submitted
  46. */
  47. class Form extends FormContainer
  48. {
  49. /**#@+ operation name */
  50. const EQUAL = ':equal';
  51. const IS_IN = ':equal';
  52. const FILLED = ':filled';
  53. const VALID = ':valid';
  54. // button
  55. const SUBMITTED = ':submitted';
  56. // text
  57. const MIN_LENGTH = ':minLength';
  58. const MAX_LENGTH = ':maxLength';
  59. const LENGTH = ':length';
  60. const EMAIL = ':email';
  61. const URL = ':url';
  62. const REGEXP = ':regexp';
  63. const INTEGER = ':integer';
  64. const NUMERIC = ':integer';
  65. const FLOAT = ':float';
  66. const RANGE = ':range';
  67. // file upload
  68. const MAX_FILE_SIZE = ':fileSize';
  69. const MIME_TYPE = ':mimeType';
  70. // special case
  71. const SCRIPT = 'Nette\Forms\InstantClientScript::javascript';
  72. /**#@-*/
  73. /**#@+ method */
  74. const GET = 'get';
  75. const POST = 'post';
  76. /**#@-*/
  77. /** tracker ID */
  78. const TRACKER_ID = '_form_';
  79. /** protection token ID */
  80. const PROTECTOR_ID = '_token_';
  81. /** @var array of function(Form $sender); Occurs when the form is submitted and successfully validated */
  82. public $onSubmit;
  83. /** @var array of function(Form $sender); Occurs when the form is submitted and not validated */
  84. public $onInvalidSubmit;
  85. /** @var mixed or NULL meaning: not detected yet */
  86. private $submittedBy;
  87. /** @var array */
  88. private $httpData;
  89. /** @var Html <form> element */
  90. private $element;
  91. /** @var IFormRenderer */
  92. private $renderer;
  93. /** @var Nette\ITranslator */
  94. private $translator;
  95. /** @var array of FormGroup */
  96. private $groups = array();
  97. /** @var array */
  98. private $errors = array();
  99. /** @var array */
  100. private $encoding = 'UTF-8';
  101. /**
  102. * Form constructor.
  103. * @param string
  104. */
  105. public function __construct($name = NULL)
  106. {
  107. $this->element = /*Nette\Web\*/Html::el('form');
  108. $this->element->action = ''; // RFC 1808 -> empty uri means 'this'
  109. $this->element->method = self::POST;
  110. $this->monitor(__CLASS__);
  111. if ($name !== NULL) {
  112. $tracker = new HiddenField($name);
  113. $tracker->unmonitor(__CLASS__);
  114. $this[self::TRACKER_ID] = $tracker;
  115. }
  116. parent::__construct(NULL, $name);
  117. }
  118. /**
  119. * This method will be called when the component (or component's parent)
  120. * becomes attached to a monitored object. Do not call this method yourself.
  121. * @param IComponent
  122. * @return void
  123. */
  124. protected function attached($obj)
  125. {
  126. if ($obj instanceof self) {
  127. throw new /*\*/InvalidStateException('Nested forms are forbidden.');
  128. }
  129. }
  130. /**
  131. * Returns self.
  132. * @return Form
  133. */
  134. final public function getForm($need = TRUE)
  135. {
  136. return $this;
  137. }
  138. /**
  139. * Sets form's action.
  140. * @param mixed URI
  141. * @return Form provides a fluent interface
  142. */
  143. public function setAction($url)
  144. {
  145. $this->element->action = $url;
  146. return $this;
  147. }
  148. /**
  149. * Returns form's action.
  150. * @return mixed URI
  151. */
  152. public function getAction()
  153. {
  154. return $this->element->action;
  155. }
  156. /**
  157. * Sets form's method.
  158. * @param string get | post
  159. * @return Form provides a fluent interface
  160. */
  161. public function setMethod($method)
  162. {
  163. if ($this->httpData !== NULL) {
  164. throw new /*\*/InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
  165. }
  166. $this->element->method = strtolower($method);
  167. return $this;
  168. }
  169. /**
  170. * Returns form's method.
  171. * @return string get | post
  172. */
  173. public function getMethod()
  174. {
  175. return $this->element->method;
  176. }
  177. /**
  178. * @deprecated
  179. */
  180. public function addTracker()
  181. {
  182. throw new /*\*/DeprecatedException(__METHOD__ . '() is deprecated; pass form name to the constructor.');
  183. }
  184. /**
  185. * Cross-Site Request Forgery (CSRF) form protection.
  186. * @param string
  187. * @param int
  188. * @return void
  189. */
  190. public function addProtection($message = NULL, $timeout = NULL)
  191. {
  192. $session = $this->getSession()->getNamespace('Nette.Forms.Form/CSRF');
  193. $key = "key$timeout";
  194. if (isset($session->$key)) {
  195. $token = $session->$key;
  196. } else {
  197. $session->$key = $token = md5(uniqid('', TRUE));
  198. }
  199. $session->setExpiration($timeout, $key);
  200. $this[self::PROTECTOR_ID] = new HiddenField($token);
  201. $this[self::PROTECTOR_ID]->addRule(':equal', empty($message) ? 'Security token did not match. Possible CSRF attack.' : $message, $token);
  202. }
  203. /**
  204. * Adds fieldset group to the form.
  205. * @param string caption
  206. * @param bool set this group as current
  207. * @return FormGroup
  208. */
  209. public function addGroup($caption = NULL, $setAsCurrent = TRUE)
  210. {
  211. $group = new FormGroup;
  212. $group->setOption('label', $caption);
  213. $group->setOption('visual', TRUE);
  214. if ($setAsCurrent) {
  215. $this->setCurrentGroup($group);
  216. }
  217. if (isset($this->groups[$caption])) {
  218. return $this->groups[] = $group;
  219. } else {
  220. return $this->groups[$caption] = $group;
  221. }
  222. }
  223. /**
  224. * Removes fieldset group from form.
  225. * @param string|FormGroup
  226. * @return void
  227. */
  228. public function removeGroup($name)
  229. {
  230. if (is_string($name) && isset($this->groups[$name])) {
  231. $group = $this->groups[$name];
  232. } elseif ($name instanceof FormGroup && in_array($name, $this->groups, TRUE)) {
  233. $group = $name;
  234. $name = array_search($group, $this->groups, TRUE);
  235. } else {
  236. throw new /*\*/InvalidArgumentException("Group not found in form '$this->name'");
  237. }
  238. foreach ($group->getControls() as $control) {
  239. $this->removeComponent($control);
  240. }
  241. unset($this->groups[$name]);
  242. }
  243. /**
  244. * Returns all defined groups.
  245. * @return array of FormGroup
  246. */
  247. public function getGroups()
  248. {
  249. return $this->groups;
  250. }
  251. /**
  252. * Returns the specified group.
  253. * @param string name
  254. * @return FormGroup
  255. */
  256. public function getGroup($name)
  257. {
  258. return isset($this->groups[$name]) ? $this->groups[$name] : NULL;
  259. }
  260. /**
  261. * Set the encoding for the values.
  262. * @param string
  263. * @return Form provides a fluent interface
  264. */
  265. public function setEncoding($value)
  266. {
  267. $this->encoding = empty($value) ? 'UTF-8' : strtoupper($value);
  268. if ($this->encoding !== 'UTF-8' && !extension_loaded('mbstring')) {
  269. throw new /*\*/Exception("The PHP extension 'mbstring' is required for this encoding but is not loaded.");
  270. }
  271. return $this;
  272. }
  273. /**
  274. * Returns the encoding.
  275. * @return string
  276. */
  277. final public function getEncoding()
  278. {
  279. return $this->encoding;
  280. }
  281. /********************* translator ****************d*g**/
  282. /**
  283. * Sets translate adapter.
  284. * @param Nette\ITranslator
  285. * @return Form provides a fluent interface
  286. */
  287. public function setTranslator(/*Nette\*/ITranslator $translator = NULL)
  288. {
  289. $this->translator = $translator;
  290. return $this;
  291. }
  292. /**
  293. * Returns translate adapter.
  294. * @return Nette\ITranslator|NULL
  295. */
  296. final public function getTranslator()
  297. {
  298. return $this->translator;
  299. }
  300. /********************* submission ****************d*g**/
  301. /**
  302. * Tells if the form is anchored.
  303. * @return bool
  304. */
  305. public function isAnchored()
  306. {
  307. return TRUE;
  308. }
  309. /**
  310. * Tells if the form was submitted.
  311. * @return ISubmitterControl|FALSE submittor control
  312. */
  313. final public function isSubmitted()
  314. {
  315. if ($this->submittedBy === NULL) {
  316. $this->getHttpData();
  317. $this->submittedBy = !empty($this->httpData);
  318. }
  319. return $this->submittedBy;
  320. }
  321. /**
  322. * Sets the submittor control.
  323. * @param ISubmitterControl
  324. * @return Form provides a fluent interface
  325. */
  326. public function setSubmittedBy(ISubmitterControl $by = NULL)
  327. {
  328. $this->submittedBy = $by === NULL ? FALSE : $by;
  329. return $this;
  330. }
  331. /**
  332. * Returns submitted HTTP data.
  333. * @return array
  334. */
  335. final public function getHttpData()
  336. {
  337. if ($this->httpData === NULL) {
  338. if (!$this->isAnchored()) {
  339. throw new /*\*/InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
  340. }
  341. $this->httpData = (array) $this->receiveHttpData();
  342. }
  343. return $this->httpData;
  344. }
  345. /**
  346. * Fires submit/click events.
  347. * @return void
  348. */
  349. public function fireEvents()
  350. {
  351. if (!$this->isSubmitted()) {
  352. return;
  353. } elseif ($this->submittedBy instanceof ISubmitterControl) {
  354. if (!$this->submittedBy->getValidationScope() || $this->isValid()) {
  355. $this->submittedBy->click();
  356. $this->onSubmit($this);
  357. } else {
  358. $this->submittedBy->onInvalidClick($this->submittedBy);
  359. $this->onInvalidSubmit($this);
  360. }
  361. } elseif ($this->isValid()) {
  362. $this->onSubmit($this);
  363. } else {
  364. $this->onInvalidSubmit($this);
  365. }
  366. }
  367. /**
  368. * Internal: receives submitted HTTP data.
  369. * @return array
  370. */
  371. protected function receiveHttpData()
  372. {
  373. $httpRequest = $this->getHttpRequest();
  374. if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
  375. return;
  376. }
  377. $httpRequest->setEncoding($this->encoding);
  378. if ($httpRequest->isMethod('post')) {
  379. $data = /*Nette\*/ArrayTools::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
  380. } else {
  381. $data = $httpRequest->getQuery();
  382. }
  383. if ($tracker = $this->getComponent(self::TRACKER_ID, FALSE)) {
  384. if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
  385. return;
  386. }
  387. }
  388. return $data;
  389. }
  390. /**
  391. * @deprecated
  392. */
  393. public function processHttpRequest()
  394. {
  395. $this->fireEvents();
  396. }
  397. /********************* data exchange ****************d*g**/
  398. /**
  399. * Returns the values submitted by the form.
  400. * @return array
  401. */
  402. public function getValues()
  403. {
  404. $values = parent::getValues();
  405. unset($values[self::TRACKER_ID], $values[self::PROTECTOR_ID]);
  406. return $values;
  407. }
  408. /********************* validation ****************d*g**/
  409. /**
  410. * Adds error message to the list.
  411. * @param string error message
  412. * @return void
  413. */
  414. public function addError($message)
  415. {
  416. if (!in_array($message, $this->errors, TRUE)) {
  417. $this->errors[] = $message;
  418. $this->valid = FALSE;
  419. }
  420. }
  421. /**
  422. * Returns validation errors.
  423. * @return array
  424. */
  425. public function getErrors()
  426. {
  427. return $this->errors;
  428. }
  429. /**
  430. * @return bool
  431. */
  432. public function hasErrors()
  433. {
  434. return (bool) $this->getErrors();
  435. }
  436. /**
  437. * @return void
  438. */
  439. public function cleanErrors()
  440. {
  441. $this->errors = array();
  442. $this->valid = NULL;
  443. }
  444. /********************* rendering ****************d*g**/
  445. /**
  446. * Returns form's HTML element template.
  447. * @return Nette\Web\Html
  448. */
  449. public function getElementPrototype()
  450. {
  451. return $this->element;
  452. }
  453. /**
  454. * Sets form renderer.
  455. * @param IFormRenderer
  456. * @return Form provides a fluent interface
  457. */
  458. public function setRenderer(IFormRenderer $renderer)
  459. {
  460. $this->renderer = $renderer;
  461. return $this;
  462. }
  463. /**
  464. * Returns form renderer.
  465. * @return IFormRenderer|NULL
  466. */
  467. final public function getRenderer()
  468. {
  469. if ($this->renderer === NULL) {
  470. $this->renderer = new ConventionalRenderer;
  471. }
  472. return $this->renderer;
  473. }
  474. /**
  475. * Renders form.
  476. * @return void
  477. */
  478. public function render()
  479. {
  480. $args = func_get_args();
  481. array_unshift($args, $this);
  482. $s = call_user_func_array(array($this->getRenderer(), 'render'), $args);
  483. if (strcmp($this->encoding, 'UTF-8')) {
  484. echo mb_convert_encoding($s, 'HTML-ENTITIES', 'UTF-8');
  485. } else {
  486. echo $s;
  487. }
  488. }
  489. /**
  490. * Renders form to string.
  491. * @return bool can throw exceptions? (hidden parameter)
  492. * @return string
  493. */
  494. public function __toString()
  495. {
  496. try {
  497. if (strcmp($this->encoding, 'UTF-8')) {
  498. return mb_convert_encoding($this->getRenderer()->render($this), 'HTML-ENTITIES', 'UTF-8');
  499. } else {
  500. return $this->getRenderer()->render($this);
  501. }
  502. } catch (/*\*/Exception $e) {
  503. if (func_get_args() && func_get_arg(0)) {
  504. throw $e;
  505. } else {
  506. trigger_error($e->getMessage(), E_USER_WARNING);
  507. return '';
  508. }
  509. }
  510. }
  511. /********************* backend ****************d*g**/
  512. /**
  513. * @return Nette\Web\IHttpRequest
  514. */
  515. protected function getHttpRequest()
  516. {
  517. return class_exists(/*Nette\*/'Environment') ? /*Nette\*/Environment::getHttpRequest() : new /*Nette\Web\*/HttpRequest;
  518. }
  519. /**
  520. * @return Nette\Web\Session
  521. */
  522. protected function getSession()
  523. {
  524. return /*Nette\*/Environment::getSession();
  525. }
  526. }