PluginProbe
Packeta / 2.1
Packeta v2.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 2.1, at deps/nette/forms/src/Forms/Form.php

533 lines 17.1 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 SubmitterControl|bool */
53 private $submittedBy;
54 /** @var array|null */
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 * @param string|object $caption
164 */
165 public function addGroup($caption = null, bool $setAsCurrent = \true) : ControlGroup
166 {
167 $group = new ControlGroup();
168 $group->setOption('label', $caption);
169 $group->setOption('visual', \true);
170 if ($setAsCurrent) {
171 $this->setCurrentGroup($group);
172 }
173 return !\is_scalar($caption) || isset($this->groups[$caption]) ? $this->groups[] = $group : ($this->groups[$caption] = $group);
174 }
175 /**
176 * Removes fieldset group from form.
177 * @param string|ControlGroup $name
178 */
179 public function removeGroup($name) : void
180 {
181 if (\is_string($name) && isset($this->groups[$name])) {
182 $group = $this->groups[$name];
183 } elseif ($name instanceof ControlGroup && \in_array($name, $this->groups, \true)) {
184 $group = $name;
185 $name = \array_search($group, $this->groups, \true);
186 } else {
187 throw new \Packetery\Nette\InvalidArgumentException("Group not found in form '{$this->name}'");
188 }
189 foreach ($group->getControls() as $control) {
190 $control->getParent()->removeComponent($control);
191 }
192 unset($this->groups[$name]);
193 }
194 /**
195 * Returns all defined groups.
196 * @return ControlGroup[]
197 */
198 public function getGroups() : array
199 {
200 return $this->groups;
201 }
202 /**
203 * Returns the specified group.
204 * @param string|int $name
205 */
206 public function getGroup($name) : ?ControlGroup
207 {
208 return $this->groups[$name] ?? null;
209 }
210 /********************* translator ****************d*g**/
211 /**
212 * Sets translate adapter.
213 * @return static
214 */
215 public function setTranslator(?Nette\Localization\Translator $translator)
216 {
217 $this->translator = $translator;
218 return $this;
219 }
220 /**
221 * Returns translate adapter.
222 */
223 public function getTranslator() : ?Nette\Localization\Translator
224 {
225 return $this->translator;
226 }
227 /********************* submission ****************d*g**/
228 /**
229 * Tells if the form is anchored.
230 */
231 public function isAnchored() : bool
232 {
233 return \true;
234 }
235 /**
236 * Tells if the form was submitted.
237 * @return SubmitterControl|bool submittor control
238 */
239 public function isSubmitted()
240 {
241 if ($this->httpData === null) {
242 $this->getHttpData();
243 }
244 return $this->submittedBy;
245 }
246 /**
247 * Tells if the form was submitted and successfully validated.
248 */
249 public function isSuccess() : bool
250 {
251 return $this->isSubmitted() && $this->isValid();
252 }
253 /**
254 * Sets the submittor control.
255 * @return static
256 * @internal
257 */
258 public function setSubmittedBy(?SubmitterControl $by)
259 {
260 $this->submittedBy = $by ?? \false;
261 return $this;
262 }
263 /**
264 * Returns submitted HTTP data.
265 * @return mixed
266 */
267 public function getHttpData(int $type = null, string $htmlName = null)
268 {
269 if ($this->httpData === null) {
270 if (!$this->isAnchored()) {
271 throw new \Packetery\Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
272 }
273 $data = $this->receiveHttpData();
274 $this->httpData = (array) $data;
275 $this->submittedBy = \is_array($data);
276 }
277 if ($htmlName === null) {
278 return $this->httpData;
279 }
280 return Helpers::extractHttpData($this->httpData, $htmlName, $type);
281 }
282 /**
283 * Fires submit/click events.
284 */
285 public function fireEvents() : void
286 {
287 if (!$this->isSubmitted()) {
288 return;
289 } elseif (!$this->getErrors()) {
290 $this->validate();
291 }
292 $handled = \count($this->onSuccess ?? []) || \count($this->onSubmit ?? []);
293 if ($this->submittedBy instanceof Controls\SubmitButton) {
294 $handled = $handled || \count($this->submittedBy->onClick ?? []);
295 if ($this->isValid()) {
296 $this->invokeHandlers($this->submittedBy->onClick, $this->submittedBy);
297 } else {
298 Arrays::invoke($this->submittedBy->onInvalidClick, $this->submittedBy);
299 }
300 }
301 if ($this->isValid()) {
302 $this->invokeHandlers($this->onSuccess);
303 }
304 if (!$this->isValid()) {
305 Arrays::invoke($this->onError, $this);
306 }
307 Arrays::invoke($this->onSubmit, $this);
308 if (!$handled) {
309 \trigger_error("Form was submitted but there are no associated handlers (form '{$this->getName()}').", \E_USER_WARNING);
310 }
311 }
312 private function invokeHandlers(iterable $handlers, $button = null) : void
313 {
314 foreach ($handlers as $handler) {
315 $params = \Packetery\Nette\Utils\Callback::toReflection($handler)->getParameters();
316 $types = \array_map([\Packetery\Nette\Utils\Reflection::class, 'getParameterType'], $params);
317 if (!isset($types[0])) {
318 $arg0 = $button ?: $this;
319 } elseif ($this instanceof $types[0]) {
320 $arg0 = $this;
321 } elseif ($button instanceof $types[0]) {
322 $arg0 = $button;
323 } else {
324 $arg0 = $this->getValues($types[0]);
325 }
326 $arg1 = isset($params[1]) ? $this->getValues($types[1]) : null;
327 $handler($arg0, $arg1);
328 if (!$this->isValid()) {
329 return;
330 }
331 }
332 }
333 /**
334 * Resets form.
335 * @return static
336 */
337 public function reset()
338 {
339 $this->setSubmittedBy(null);
340 $this->setValues([], \true);
341 return $this;
342 }
343 /**
344 * Internal: returns submitted HTTP data or null when form was not submitted.
345 */
346 protected function receiveHttpData() : ?array
347 {
348 $httpRequest = $this->getHttpRequest();
349 if (\strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
350 return null;
351 }
352 if ($httpRequest->isMethod('post')) {
353 if (!$this->crossOrigin && !$httpRequest->isSameSite()) {
354 return null;
355 }
356 $data = \Packetery\Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
357 } else {
358 $data = $httpRequest->getQuery();
359 if (!$data) {
360 return null;
361 }
362 }
363 if ($tracker = $this->getComponent(self::TRACKER_ID, \false)) {
364 if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
365 return null;
366 }
367 }
368 return $data;
369 }
370 /********************* validation ****************d*g**/
371 public function validate(array $controls = null) : void
372 {
373 $this->cleanErrors();
374 if ($controls === null && $this->submittedBy instanceof SubmitterControl) {
375 $controls = $this->submittedBy->getValidationScope();
376 }
377 $this->validateMaxPostSize();
378 parent::validate($controls);
379 }
380 /** @internal */
381 public function validateMaxPostSize() : void
382 {
383 if (!$this->submittedBy || !$this->isMethod('post') || empty($_SERVER['CONTENT_LENGTH'])) {
384 return;
385 }
386 $maxSize = Helpers::iniGetSize('post_max_size');
387 if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
388 $this->addError(\sprintf(Validator::$messages[self::MAX_FILE_SIZE], $maxSize));
389 }
390 }
391 /**
392 * Adds global error message.
393 * @param string|object $message
394 */
395 public function addError($message, bool $translate = \true) : void
396 {
397 if ($translate && $this->translator) {
398 $message = $this->translator->translate($message);
399 }
400 $this->errors[] = $message;
401 }
402 /**
403 * Returns global validation errors.
404 */
405 public function getErrors() : array
406 {
407 return \array_unique(\array_merge($this->errors, parent::getErrors()));
408 }
409 public function hasErrors() : bool
410 {
411 return (bool) $this->getErrors();
412 }
413 public function cleanErrors() : void
414 {
415 $this->errors = [];
416 }
417 /**
418 * Returns form's validation errors.
419 */
420 public function getOwnErrors() : array
421 {
422 return \array_unique($this->errors);
423 }
424 /********************* rendering ****************d*g**/
425 /**
426 * Returns form's HTML element template.
427 */
428 public function getElementPrototype() : Html
429 {
430 if (!$this->element) {
431 $this->element = Html::el('form');
432 $this->element->action = '';
433 // RFC 1808 -> empty uri means 'this'
434 $this->element->method = self::POST;
435 }
436 return $this->element;
437 }
438 /**
439 * Sets form renderer.
440 * @return static
441 */
442 public function setRenderer(?FormRenderer $renderer)
443 {
444 $this->renderer = $renderer;
445 return $this;
446 }
447 /**
448 * Returns form renderer.
449 */
450 public function getRenderer() : FormRenderer
451 {
452 if ($this->renderer === null) {
453 $this->renderer = new Rendering\DefaultFormRenderer();
454 }
455 return $this->renderer;
456 }
457 protected function beforeRender()
458 {
459 }
460 /**
461 * Must be called before form is rendered and render() is not used.
462 */
463 public function fireRenderEvents() : void
464 {
465 if (!$this->beforeRenderCalled) {
466 $this->beforeRenderCalled = \true;
467 $this->beforeRender();
468 Arrays::invoke($this->onRender, $this);
469 }
470 }
471 /**
472 * Renders form.
473 */
474 public function render(...$args) : void
475 {
476 $this->fireRenderEvents();
477 echo $this->getRenderer()->render($this, ...$args);
478 }
479 /**
480 * Renders form to string.
481 * @param can throw exceptions? (hidden parameter)
482 */
483 public function __toString() : string
484 {
485 try {
486 $this->fireRenderEvents();
487 return $this->getRenderer()->render($this);
488 } catch (\Throwable $e) {
489 if (\func_num_args() || \PHP_VERSION_ID >= 70400) {
490 throw $e;
491 }
492 \trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", \E_USER_ERROR);
493 return '';
494 }
495 }
496 public function getToggles() : array
497 {
498 $toggles = [];
499 foreach ($this->getComponents(\true, Controls\BaseControl::class) as $control) {
500 $toggles = $control->getRules()->getToggleStates($toggles);
501 }
502 return $toggles;
503 }
504 /********************* backend ****************d*g**/
505 /**
506 * Initialize standalone forms.
507 */
508 public static function initialize(bool $reinit = \false) : void
509 {
510 if ($reinit) {
511 self::$defaultHttpRequest = null;
512 return;
513 } elseif (self::$defaultHttpRequest) {
514 return;
515 }
516 self::$defaultHttpRequest = (new \Packetery\Nette\Http\RequestFactory())->fromGlobals();
517 if (\PHP_SAPI !== 'cli') {
518 if (\headers_sent($file, $line)) {
519 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})" : '') . '. ');
520 }
521 \Packetery\Nette\Http\Helpers::initCookie(self::$defaultHttpRequest, new \Packetery\Nette\Http\Response());
522 }
523 }
524 private function getHttpRequest() : \Packetery\Nette\Http\IRequest
525 {
526 if (!$this->httpRequest) {
527 self::initialize();
528 $this->httpRequest = self::$defaultHttpRequest;
529 }
530 return $this->httpRequest;
531 }
532 }
533