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