Overview

Namespaces

  • Budovy
  • Kdyby
    • BootstrapFormRenderer
      • DI
      • Latte
  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Diagnostics
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
      • Diagnostics
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • PhpGenerator
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
  • NetteModule
  • Nextras
    • Datagrid
  • None
  • PHP
  • Tester
    • CodeCoverage
    • Runner
      • Output
  • Vodacek
    • Forms
      • Controls
  • WebLoader
    • Filter
    • Nette

Classes

  • Container
  • ControlGroup
  • Form
  • Helpers
  • Rule
  • Rules

Interfaces

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