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