1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Forms;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17: 18: 19: 20: 21: 22:
23: class Form extends Container implements Nette\Utils\IHtmlString
24: {
25:
26: const EQUAL = ':equal',
27: IS_IN = self::EQUAL,
28: NOT_EQUAL = ':notEqual',
29: IS_NOT_IN = self::NOT_EQUAL,
30: FILLED = ':filled',
31: BLANK = ':blank',
32: REQUIRED = self::FILLED,
33: VALID = ':valid';
34:
35:
36: const PROTECTION = Controls\CsrfProtection::PROTECTION;
37:
38:
39: const SUBMITTED = ':submitted';
40:
41:
42: const MIN_LENGTH = ':minLength',
43: MAX_LENGTH = ':maxLength',
44: LENGTH = ':length',
45: EMAIL = ':email',
46: URL = ':url',
47: PATTERN = ':pattern',
48: INTEGER = ':integer',
49: NUMERIC = ':integer',
50: FLOAT = ':float',
51: MIN = ':min',
52: MAX = ':max',
53: RANGE = ':range';
54:
55:
56: const COUNT = self::LENGTH;
57:
58:
59: const MAX_FILE_SIZE = ':fileSize',
60: MIME_TYPE = ':mimeType',
61: IMAGE = ':image',
62: MAX_POST_SIZE = ':maxPostSize';
63:
64:
65: const GET = 'get',
66: POST = 'post';
67:
68:
69: const DATA_TEXT = 1;
70: const DATA_LINE = 2;
71: const DATA_FILE = 3;
72: const DATA_KEYS = 8;
73:
74:
75: const TRACKER_ID = '_form_';
76:
77:
78: const PROTECTOR_ID = '_token_';
79:
80:
81: public $onSuccess;
82:
83:
84: public $onError;
85:
86:
87: public $onSubmit;
88:
89:
90: public $onRender;
91:
92:
93: private $submittedBy;
94:
95:
96: private $httpData;
97:
98:
99: private $element;
100:
101:
102: private $renderer;
103:
104:
105: private $translator;
106:
107:
108: private $groups = [];
109:
110:
111: private $errors = [];
112:
113:
114: public $httpRequest;
115:
116:
117: private $beforeRenderCalled;
118:
119:
120: 121: 122: 123:
124: public function __construct($name = NULL)
125: {
126: parent::__construct();
127: if ($name !== NULL) {
128: $this->getElementPrototype()->id = 'frm-' . $name;
129: $tracker = new Controls\HiddenField($name);
130: $tracker->setOmitted();
131: $this[self::TRACKER_ID] = $tracker;
132: $this->setParent(NULL, $name);
133: }
134: }
135:
136:
137: 138: 139:
140: protected function validateParent(Nette\ComponentModel\IContainer $parent)
141: {
142: parent::validateParent($parent);
143: $this->monitor(__CLASS__);
144: }
145:
146:
147: 148: 149: 150: 151: 152:
153: protected function attached($obj)
154: {
155: if ($obj instanceof self) {
156: throw new Nette\InvalidStateException('Nested forms are forbidden.');
157: }
158: }
159:
160:
161: 162: 163: 164:
165: public function getForm($need = TRUE)
166: {
167: return $this;
168: }
169:
170:
171: 172: 173: 174: 175:
176: public function setAction($url)
177: {
178: $this->getElementPrototype()->action = $url;
179: return $this;
180: }
181:
182:
183: 184: 185: 186:
187: public function getAction()
188: {
189: return $this->getElementPrototype()->action;
190: }
191:
192:
193: 194: 195: 196: 197:
198: public function setMethod($method)
199: {
200: if ($this->httpData !== NULL) {
201: throw new Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
202: }
203: $this->getElementPrototype()->method = strtolower($method);
204: return $this;
205: }
206:
207:
208: 209: 210: 211:
212: public function getMethod()
213: {
214: return $this->getElementPrototype()->method;
215: }
216:
217:
218: 219: 220: 221: 222:
223: public function isMethod($method)
224: {
225: return strcasecmp($this->getElementPrototype()->method, $method) === 0;
226: }
227:
228:
229: 230: 231: 232: 233:
234: public function addProtection($message = NULL)
235: {
236: $control = new Controls\CsrfProtection($message);
237: $this->addComponent($control, self::PROTECTOR_ID, key($this->getComponents()));
238: return $control;
239: }
240:
241:
242: 243: 244: 245: 246: 247:
248: public function addGroup($caption = NULL, $setAsCurrent = TRUE)
249: {
250: $group = new ControlGroup;
251: $group->setOption('label', $caption);
252: $group->setOption('visual', TRUE);
253:
254: if ($setAsCurrent) {
255: $this->setCurrentGroup($group);
256: }
257:
258: if (!is_scalar($caption) || isset($this->groups[$caption])) {
259: return $this->groups[] = $group;
260: } else {
261: return $this->groups[$caption] = $group;
262: }
263: }
264:
265:
266: 267: 268: 269: 270:
271: public function removeGroup($name)
272: {
273: if (is_string($name) && isset($this->groups[$name])) {
274: $group = $this->groups[$name];
275:
276: } elseif ($name instanceof ControlGroup && in_array($name, $this->groups, TRUE)) {
277: $group = $name;
278: $name = array_search($group, $this->groups, TRUE);
279:
280: } else {
281: throw new Nette\InvalidArgumentException("Group not found in form '$this->name'");
282: }
283:
284: foreach ($group->getControls() as $control) {
285: $control->getParent()->removeComponent($control);
286: }
287:
288: unset($this->groups[$name]);
289: }
290:
291:
292: 293: 294: 295:
296: public function getGroups()
297: {
298: return $this->groups;
299: }
300:
301:
302: 303: 304: 305: 306:
307: public function getGroup($name)
308: {
309: return isset($this->groups[$name]) ? $this->groups[$name] : NULL;
310: }
311:
312:
313:
314:
315:
316: 317: 318: 319:
320: public function setTranslator(Nette\Localization\ITranslator $translator = NULL)
321: {
322: $this->translator = $translator;
323: return $this;
324: }
325:
326:
327: 328: 329: 330:
331: public function getTranslator()
332: {
333: return $this->translator;
334: }
335:
336:
337:
338:
339:
340: 341: 342: 343:
344: public function isAnchored()
345: {
346: return TRUE;
347: }
348:
349:
350: 351: 352: 353:
354: public function isSubmitted()
355: {
356: if ($this->submittedBy === NULL) {
357: $this->getHttpData();
358: }
359: return $this->submittedBy;
360: }
361:
362:
363: 364: 365: 366:
367: public function isSuccess()
368: {
369: return $this->isSubmitted() && $this->isValid();
370: }
371:
372:
373: 374: 375: 376: 377:
378: public function setSubmittedBy(ISubmitterControl $by = NULL)
379: {
380: $this->submittedBy = $by === NULL ? FALSE : $by;
381: return $this;
382: }
383:
384:
385: 386: 387: 388:
389: public function getHttpData($type = NULL, $htmlName = NULL)
390: {
391: if ($this->httpData === NULL) {
392: if (!$this->isAnchored()) {
393: throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
394: }
395: $data = $this->receiveHttpData();
396: $this->httpData = (array) $data;
397: $this->submittedBy = is_array($data);
398: }
399: if ($htmlName === NULL) {
400: return $this->httpData;
401: }
402: return Helpers::extractHttpData($this->httpData, $htmlName, $type);
403: }
404:
405:
406: 407: 408: 409:
410: public function fireEvents()
411: {
412: if (!$this->isSubmitted()) {
413: return;
414:
415: } elseif (!$this->getErrors()) {
416: $this->validate();
417: }
418:
419: if ($this->submittedBy instanceof ISubmitterControl) {
420: if ($this->isValid()) {
421: $this->submittedBy->onClick($this->submittedBy);
422: } else {
423: $this->submittedBy->onInvalidClick($this->submittedBy);
424: }
425: }
426:
427: if (!$this->isValid()) {
428: $this->onError($this);
429:
430: } elseif ($this->onSuccess !== NULL) {
431: if (!is_array($this->onSuccess) && !$this->onSuccess instanceof \Traversable) {
432: throw new Nette\UnexpectedValueException('Property Form::$onSuccess must be array or Traversable, ' . gettype($this->onSuccess) . ' given.');
433: }
434: foreach ($this->onSuccess as $handler) {
435: $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
436: $values = isset($params[1]) ? $this->getValues($params[1]->isArray()) : NULL;
437: Nette\Utils\Callback::invoke($handler, $this, $values);
438: if (!$this->isValid()) {
439: $this->onError($this);
440: break;
441: }
442: }
443: }
444:
445: $this->onSubmit($this);
446: }
447:
448:
449: 450: 451: 452:
453: protected function receiveHttpData()
454: {
455: $httpRequest = $this->getHttpRequest();
456: if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
457: return;
458: }
459:
460: if ($httpRequest->isMethod('post')) {
461: $data = Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
462: } else {
463: $data = $httpRequest->getQuery();
464: if (!$data) {
465: return;
466: }
467: }
468:
469: if ($tracker = $this->getComponent(self::TRACKER_ID, FALSE)) {
470: if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
471: return;
472: }
473: }
474:
475: return $data;
476: }
477:
478:
479:
480:
481:
482: public function validate(array $controls = NULL)
483: {
484: $this->cleanErrors();
485: if ($controls === NULL && $this->submittedBy instanceof ISubmitterControl) {
486: $controls = $this->submittedBy->getValidationScope();
487: }
488: $this->validateMaxPostSize();
489: parent::validate($controls);
490: }
491:
492:
493:
494: public function validateMaxPostSize()
495: {
496: if (!$this->submittedBy || !$this->isMethod('post') || empty($_SERVER['CONTENT_LENGTH'])) {
497: return;
498: }
499: $maxSize = ini_get('post_max_size');
500: $units = ['k' => 10, 'm' => 20, 'g' => 30];
501: if (isset($units[$ch = strtolower(substr($maxSize, -1))])) {
502: $maxSize = (int) $maxSize << $units[$ch];
503: }
504: if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
505: $this->addError(sprintf(Validator::$messages[self::MAX_FILE_SIZE], $maxSize));
506: }
507: }
508:
509:
510: 511: 512: 513: 514:
515: public function addError($message)
516: {
517: $this->errors[] = $message;
518: }
519:
520:
521: 522: 523: 524:
525: public function getErrors()
526: {
527: return array_unique(array_merge($this->errors, parent::getErrors()));
528: }
529:
530:
531: 532: 533:
534: public function hasErrors()
535: {
536: return (bool) $this->getErrors();
537: }
538:
539:
540: 541: 542:
543: public function cleanErrors()
544: {
545: $this->errors = [];
546: }
547:
548:
549: 550: 551: 552:
553: public function getOwnErrors()
554: {
555: return array_unique($this->errors);
556: }
557:
558:
559:
560:
561:
562: 563: 564: 565:
566: public function getElementPrototype()
567: {
568: if (!$this->element) {
569: $this->element = Nette\Utils\Html::el('form');
570: $this->element->action = '';
571: $this->element->method = self::POST;
572: }
573: return $this->element;
574: }
575:
576:
577: 578: 579: 580:
581: public function setRenderer(IFormRenderer $renderer = NULL)
582: {
583: $this->renderer = $renderer;
584: return $this;
585: }
586:
587:
588: 589: 590: 591:
592: public function getRenderer()
593: {
594: if ($this->renderer === NULL) {
595: $this->renderer = new Rendering\DefaultFormRenderer;
596: }
597: return $this->renderer;
598: }
599:
600:
601: 602: 603:
604: protected function beforeRender()
605: {
606: }
607:
608:
609: 610: 611: 612:
613: public function fireRenderEvents()
614: {
615: if (!$this->beforeRenderCalled) {
616: foreach ($this->getComponents(TRUE, Controls\BaseControl::class) as $control) {
617: $control->getRules()->check();
618: }
619: $this->beforeRenderCalled = TRUE;
620: $this->beforeRender();
621: $this->onRender($this);
622: }
623: }
624:
625:
626: 627: 628: 629:
630: public function render(...$args)
631: {
632: $this->fireRenderEvents();
633: echo $this->getRenderer()->render($this, ...$args);
634: }
635:
636:
637: 638: 639: 640: 641:
642: public function __toString()
643: {
644: try {
645: $this->fireRenderEvents();
646: return $this->getRenderer()->render($this);
647:
648: } catch (\Throwable $e) {
649: } catch (\Exception $e) {
650: }
651: if (isset($e)) {
652: if (func_num_args()) {
653: throw $e;
654: }
655: trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
656: }
657: }
658:
659:
660:
661:
662:
663: 664: 665:
666: private function getHttpRequest()
667: {
668: if (!$this->httpRequest) {
669: $factory = new Nette\Http\RequestFactory;
670: $this->httpRequest = $factory->createHttpRequest();
671: }
672: return $this->httpRequest;
673: }
674:
675:
676: 677: 678:
679: public function getToggles()
680: {
681: $toggles = [];
682: foreach ($this->getComponents(TRUE, Controls\BaseControl::class) as $control) {
683: $toggles = $control->getRules()->getToggleStates($toggles);
684: }
685: return $toggles;
686: }
687:
688: }
689: