PageRenderTime 56ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/www/libs/nette-dev/Forms/Form.php

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