PluginProbe
Packeta / 1.6.1
Packeta v1.6.1
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / forms / src / Forms / Form.php

Form.php in Packeta 1.6.1, at deps/nette/forms/src/Forms/Form.php

532 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7 declare (strict_types=1);
8 namespace Packetery\Nette\Forms;
9
10 use Packetery\Nette;
11 use Packetery\Nette\Utils\Arrays;
12 use Packetery\Nette\Utils\Html;
13 /**
14 * Creates, validates and renders HTML forms.
15 *
16 * @property-read array $errors
17 * @property-read array $ownErrors
18 * @property-read Html $elementPrototype
19 * @property-read FormRenderer $renderer
20 * @property string $action
21 * @property string $method
22 */
23 class Form extends Container implements \Packetery\Nette\HtmlStringable
24 {
25 /** validator */
26 public const EQUAL = ':equal', IS_IN = self::EQUAL, NOT_EQUAL = ':notEqual', IS_NOT_IN = self::NOT_EQUAL, FILLED = ':filled', BLANK = ':blank', REQUIRED = self::FILLED, VALID = ':valid', SUBMITTED = ':submitted', MIN_LENGTH = ':minLength', MAX_LENGTH = ':maxLength', LENGTH = ':length', EMAIL = ':email', URL = ':url', PATTERN = ':pattern', PATTERN_ICASE = ':patternCaseInsensitive', INTEGER = ':integer', NUMERIC = ':numeric', FLOAT = ':float', MIN = ':min', MAX = ':max', RANGE = ':range', COUNT = self::LENGTH, MAX_FILE_SIZE = ':fileSize', MIME_TYPE = ':mimeType', IMAGE = ':image', MAX_POST_SIZE = ':maxPostSize';
27 /** method */
28 public const GET = 'get', POST = 'post';
29 /** submitted data types */
30 public const DATA_TEXT = 1, DATA_LINE = 2, DATA_FILE = 3, DATA_KEYS = 8;
31 /** @internal tracker ID */
32 public const TRACKER_ID = '_form_';
33 /** @internal protection token ID */
34 public const PROTECTOR_ID = '_token_';
35 /**
36 * Occurs when the form is submitted and successfully validated
37 * @var array<callable(self, array|object): void|callable(array|object): void>
38 */
39 public $onSuccess = [];
40 /** @var array<callable(self): void> Occurs when the form is submitted and is not valid */
41 public $onError = [];
42 /** @var array<callable(self): void> Occurs when the form is submitted */
43 public $onSubmit = [];
44 /** @var array<callable(self): void> Occurs before the form is rendered */
45 public $onRender = [];
46 /** @internal @var \Packetery\Nette\Http\IRequest used only by standalone form */
47 public $httpRequest;
48 /** @var bool */
49 protected $crossOrigin = \false;
50 /** @var \Packetery\Nette\Http\IRequest */
51 private static $defaultHttpRequest;
52 /** @var mixed or null meaning: not detected yet */
53 private $submittedBy;
54 /** @var array */
55 private $httpData;
56 /** @var Html element <form> */
57 private $element;
58 /** @var FormRenderer */
59 private $renderer;
60 /** @var \Packetery\Nette\Localization\Translator */
61 private $translator;
62 /** @var ControlGroup[] */
63 private $groups = [];
64 /** @var array */
65 private $errors = [];
66 /** @var bool */
67 private $beforeRenderCalled;
68 /**
69 * Form constructor.
70 */
71 public function __construct(string $name = null)
72 {
73 if ($name !== null) {
74 $this->getElementPrototype()->id = 'frm-' . $name;
75 $tracker = new Controls\HiddenField($name);
76 $tracker->setOmitted();
77 $this[self::TRACKER_ID] = $tracker;
78 $this->setParent(null, $name);
79 }
80 $this->monitor(self::class, function () : void {
81 throw new \Packetery\Nette\InvalidStateException('Nested forms are forbidden.');
82 });
83 }
84 /**
85 * Returns self.
86 * @return static
87 */
88 public function getForm(bool $throw = \true) : self
89 {
90 return $this;
91 }
92 /**
93 * Sets form's action.
94 * @param string|object $url
95 * @return static
96 */
97 public function setAction($url)
98 {
99 $this->getElementPrototype()->action = $url;
100 return $this;
101 }
102 /**
103 * Returns form's action.
104 * @return mixed
105 */
106 public function getAction()
107 {
108 return $this->getElementPrototype()->action;
109 }
110 /**
111 * Sets form's method GET or POST.
112 * @return static
113 */
114 public function setMethod(string $method)
115 {
116 if ($this->httpData !== null) {
117 throw new \Packetery\Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
118 }
119 $this->getElementPrototype()->method = \strtolower($method);
120 return $this;
121 }
122 /**
123 * Returns form's method.
124 */
125 public function getMethod() : string
126 {
127 return $this->getElementPrototype()->method;
128 }
129 /**
130 * Checks if the request method is the given one.
131 */
132 public function isMethod(string $method) : bool
133 {
134 return \strcasecmp($this->getElementPrototype()->method, $method) === 0;
135 }
136 /**
137 * Changes forms's HTML attribute.
138 * @return static
139 */
140 public function setHtmlAttribute(string $name, $value = \true)
141 {
142 $this->getElementPrototype()->{$name} = $value;
143 return $this;
144 }
145 /**
146 * Disables CSRF protection using a SameSite cookie.
147 */
148 public function allowCrossOrigin() : void
149 {
150 $this->crossOrigin = \true;
151 }
152 /**
153 * Cross-Site Request Forgery (CSRF) form protection.
154 */
155 public function addProtection(string $errorMessage = null) : Controls\CsrfProtection
156 {
157 $control = new Controls\CsrfProtection($errorMessage);
158 $this->addComponent($control, self::PROTECTOR_ID, \key((array) $this->getComponents()));
159 return $control;
160 }
161 /**
162 * Adds fieldset group to the form.
163 */
164 public function addGroup(string $caption = null, bool $setAsCurrent = \true) : ControlGroup
165 {
166 $group = new ControlGroup();
167 $group->setOption('label', $caption);
168 $group->setOption('visual', \true);
169 if ($setAsCurrent) {
170 $this->setCurrentGroup($group);
171 }
172 return !\is_scalar($caption) || isset($this->groups[$caption]) ? $this->groups[] = $group : ($this->groups[$caption] = $group);
173 }
174 /**
175 * Removes fieldset group from form.
176 * @param string|ControlGroup $name
177 */
178 public function removeGroup($name) : void
179 {
180 if (\is_string($name) && isset($this->groups[$name])) {
181 $group = $this->groups[$name];
182 } elseif ($name instanceof ControlGroup && \in_array($name, $this->groups, \true)) {
183 $group = $name;
184 $name = \array_search($group, $this->groups, \true);
185 } else {
186 throw new \Packetery\Nette\InvalidArgumentException("Group not found in form '{$this->name}'");
187 }
188 foreach ($group->getControls() as $control) {
189 $control->getParent()->removeComponent($control);
190 }
191 unset($this->groups[$name]);
192 }
193 /**
194 * Returns all defined groups.
195 * @return ControlGroup[]
196 */
197 public function getGroups() : array
198 {
199 return $this->groups;
200 }
201 /**
202 * Returns the specified group.
203 * @param string|int $name
204 */
205 public function getGroup($name) : ?ControlGroup
206 {
207 return $this->groups[$name] ?? null;
208 }
209 /********************* translator ****************d*g**/
210 /**
211 * Sets translate adapter.
212 * @return static
213 */
214 public function setTranslator(?Nette\Localization\Translator $translator)
215 {
216 $this->translator = $translator;
217 return $this;
218 }
219 /**
220 * Returns translate adapter.
221 */
222 public function getTranslator() : ?Nette\Localization\Translator
223 {
224 return $this->translator;
225 }
226 /********************* submission ****************d*g**/
227 /**
228 * Tells if the form is anchored.
229 */
230 public function isAnchored() : bool
231 {
232 return \true;
233 }
234 /**
235 * Tells if the form was submitted.
236 * @return SubmitterControl|bool submittor control
237 */
238 public function isSubmitted()
239 {
240 if ($this->submittedBy === null) {
241 $this->getHttpData();
242 }
243 return $this->submittedBy;
244 }
245 /**
246 * Tells if the form was submitted and successfully validated.
247 */
248 public function isSuccess() : bool
249 {
250 return $this->isSubmitted() && $this->isValid();
251 }
252 /**
253 * Sets the submittor control.
254 * @return static
255 * @internal
256 */
257 public function setSubmittedBy(?SubmitterControl $by)
258 {
259 $this->submittedBy = $by ?? \false;
260 return $this;
261 }
262 /**
263 * Returns submitted HTTP data.
264 * @return mixed
265 */
266 public function getHttpData(int $type = null, string $htmlName = null)
267 {
268 if ($this->httpData === null) {
269 if (!$this->isAnchored()) {
270 throw new \Packetery\Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
271 }
272 $data = $this->receiveHttpData();
273 $this->httpData = (array) $data;
274 $this->submittedBy = \is_array($data);
275 }
276 if ($htmlName === null) {
277 return $this->httpData;
278 }
279 return Helpers::extractHttpData($this->httpData, $htmlName, $type);
280 }
281 /**
282 * Fires submit/click events.
283 */
284 public function fireEvents() : void
285 {
286 if (!$this->isSubmitted()) {
287 return;
288 } elseif (!$this->getErrors()) {
289 $this->validate();
290 }
291 if ($this->submittedBy instanceof Controls\SubmitButton) {
292 if ($this->isValid()) {
293 $this->invokeHandlers($this->submittedBy->onClick, $this->submittedBy);
294 } else {
295 Arrays::invoke($this->submittedBy->onInvalidClick, $this->submittedBy);
296 }
297 }
298 if ($this->isValid()) {
299 $this->invokeHandlers($this->onSuccess);
300 }
301 if (!$this->isValid()) {
302 Arrays::invoke($this->onError, $this);
303 }
304 Arrays::invoke($this->onSubmit, $this);
305 }
306 private function invokeHandlers(iterable $handlers, $button = null) : void
307 {
308 foreach ($handlers as $handler) {
309 $params = \Packetery\Nette\Utils\Callback::toReflection($handler)->getParameters();
310 $types = \array_map([\Packetery\Nette\Utils\Reflection::class, 'getParameterType'], $params);
311 if (!isset($types[0])) {
312 $arg0 = $button ?: $this;
313 } elseif ($this instanceof $types[0]) {
314 $arg0 = $this;
315 } elseif ($button instanceof $types[0]) {
316 $arg0 = $button;
317 } else {
318 $arg0 = $this->getValues($types[0]);
319 }
320 $arg1 = isset($params[1]) ? $this->getValues($types[1]) : null;
321 $handler($arg0, $arg1);
322 if (!$this->isValid()) {
323 return;
324 }
325 }
326 }
327 /**
328 * Resets form.
329 * @return static
330 */
331 public function reset()
332 {
333 $this->setSubmittedBy(null);
334 $this->setValues([], \true);
335 return $this;
336 }
337 /**
338 * Internal: returns submitted HTTP data or null when form was not submitted.
339 */
340 protected function receiveHttpData() : ?array
341 {
342 $httpRequest = $this->getHttpRequest();
343 if (\strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
344 return null;
345 }
346 if ($httpRequest->isMethod('post')) {
347 if (!$this->crossOrigin && !$httpRequest->isSameSite()) {
348 return null;
349 }
350 $data = \Packetery\Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
351 } else {
352 $data = $httpRequest->getQuery();
353 if (!$data) {
354 return null;
355 }
356 }
357 if ($tracker = $this->getComponent(self::TRACKER_ID, \false)) {
358 if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
359 return null;
360 }
361 }
362 return $data;
363 }
364 /********************* validation ****************d*g**/
365 public function validate(array $controls = null) : void
366 {
367 $this->cleanErrors();
368 if ($controls === null && $this->submittedBy instanceof SubmitterControl) {
369 $controls = $this->submittedBy->getValidationScope();
370 }
371 $this->validateMaxPostSize();
372 parent::validate($controls);
373 }
374 /** @internal */
375 public function validateMaxPostSize() : void
376 {
377 if (!$this->submittedBy || !$this->isMethod('post') || empty($_SERVER['CONTENT_LENGTH'])) {
378 return;
379 }
380 $maxSize = Helpers::iniGetSize('post_max_size');
381 if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
382 $this->addError(\sprintf(Validator::$messages[self::MAX_FILE_SIZE], $maxSize));
383 }
384 }
385 /**
386 * Adds global error message.
387 * @param string|object $message
388 */
389 public function addError($message, bool $translate = \true) : void
390 {
391 if ($translate && $this->translator) {
392 $message = $this->translator->translate($message);
393 }
394 $this->errors[] = $message;
395 }
396 /**
397 * Returns global validation errors.
398 */
399 public function getErrors() : array
400 {
401 return \array_unique(\array_merge($this->errors, parent::getErrors()));
402 }
403 public function hasErrors() : bool
404 {
405 return (bool) $this->getErrors();
406 }
407 public function cleanErrors() : void
408 {
409 $this->errors = [];
410 }
411 /**
412 * Returns form's validation errors.
413 */
414 public function getOwnErrors() : array
415 {
416 return \array_unique($this->errors);
417 }
418 /********************* rendering ****************d*g**/
419 /**
420 * Returns form's HTML element template.
421 */
422 public function getElementPrototype() : Html
423 {
424 if (!$this->element) {
425 $this->element = Html::el('form');
426 $this->element->action = '';
427 // RFC 1808 -> empty uri means 'this'
428 $this->element->method = self::POST;
429 }
430 return $this->element;
431 }
432 /**
433 * Sets form renderer.
434 * @return static
435 */
436 public function setRenderer(?FormRenderer $renderer)
437 {
438 $this->renderer = $renderer;
439 return $this;
440 }
441 /**
442 * Returns form renderer.
443 */
444 public function getRenderer() : FormRenderer
445 {
446 if ($this->renderer === null) {
447 $this->renderer = new Rendering\DefaultFormRenderer();
448 }
449 return $this->renderer;
450 }
451 protected function beforeRender()
452 {
453 }
454 /**
455 * Must be called before form is rendered and render() is not used.
456 */
457 public function fireRenderEvents() : void
458 {
459 if (!$this->beforeRenderCalled) {
460 $this->beforeRenderCalled = \true;
461 $this->beforeRender();
462 Arrays::invoke($this->onRender, $this);
463 }
464 }
465 /**
466 * Renders form.
467 */
468 public function render(...$args) : void
469 {
470 $this->fireRenderEvents();
471 echo $this->getRenderer()->render($this, ...$args);
472 }
473 /**
474 * Renders form to string.
475 * @param can throw exceptions? (hidden parameter)
476 */
477 public function __toString() : string
478 {
479 try {
480 $this->fireRenderEvents();
481 return $this->getRenderer()->render($this);
482 } catch (\Throwable $e) {
483 if (\func_num_args() || \PHP_VERSION_ID >= 70400) {
484 throw $e;
485 }
486 \trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", \E_USER_ERROR);
487 return '';
488 }
489 }
490 public function getToggles() : array
491 {
492 $toggles = [];
493 foreach ($this->getComponents(\true, Controls\BaseControl::class) as $control) {
494 $toggles = $control->getRules()->getToggleStates($toggles);
495 }
496 return $toggles;
497 }
498 /********************* backend ****************d*g**/
499 /**
500 * Initialize standalone forms.
501 */
502 public static function initialize(bool $reinit = \false) : void
503 {
504 if ($reinit) {
505 self::$defaultHttpRequest = null;
506 return;
507 } elseif (self::$defaultHttpRequest) {
508 return;
509 }
510 self::$defaultHttpRequest = (new \Packetery\Nette\Http\RequestFactory())->fromGlobals();
511 if (\PHP_SAPI !== 'cli') {
512 if (\headers_sent($file, $line)) {
513 throw new \Packetery\Nette\InvalidStateException('Create a form or call \\Packetery\\Nette\\Forms\\Form::initialize() before the headers are sent to initialize CSRF protection.' . ($file ? " (output started at {$file}:{$line})" : '') . '. ');
514 }
515 \Packetery\Nette\Http\Helpers::initCookie(self::$defaultHttpRequest, new \Packetery\Nette\Http\Response());
516 }
517 }
518 /** @internal */
519 public function setHttpRequest(\Packetery\Nette\Http\IRequest $request)
520 {
521 $this->httpRequest = $request;
522 }
523 private function getHttpRequest() : \Packetery\Nette\Http\IRequest
524 {
525 if (!$this->httpRequest) {
526 self::initialize();
527 $this->httpRequest = self::$defaultHttpRequest;
528 }
529 return $this->httpRequest;
530 }
531 }
532