1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 0.10.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Helper;
16:
17: use Cake\Core\Configure;
18: use Cake\Core\Exception\Exception;
19: use Cake\Routing\Router;
20: use Cake\Utility\Hash;
21: use Cake\Utility\Inflector;
22: use Cake\View\Form\ContextFactory;
23: use Cake\View\Form\ContextInterface;
24: use Cake\View\Helper;
25: use Cake\View\StringTemplateTrait;
26: use Cake\View\View;
27: use Cake\View\Widget\WidgetLocator;
28: use Cake\View\Widget\WidgetRegistry;
29: use DateTime;
30: use RuntimeException;
31: use Traversable;
32:
33: /**
34: * Form helper library.
35: *
36: * Automatic generation of HTML FORMs from given data.
37: *
38: * @method string text($fieldName, array $options = [])
39: * @method string number($fieldName, array $options = [])
40: * @method string email($fieldName, array $options = [])
41: * @method string password($fieldName, array $options = [])
42: * @method string search($fieldName, array $options = [])
43: * @property \Cake\View\Helper\HtmlHelper $Html
44: * @property \Cake\View\Helper\UrlHelper $Url
45: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html
46: */
47: class FormHelper extends Helper
48: {
49: use IdGeneratorTrait;
50: use SecureFieldTokenTrait;
51: use StringTemplateTrait;
52:
53: /**
54: * Other helpers used by FormHelper
55: *
56: * @var array
57: */
58: public $helpers = ['Url', 'Html'];
59:
60: /**
61: * The various pickers that make up a datetime picker.
62: *
63: * @var array
64: */
65: protected $_datetimeParts = ['year', 'month', 'day', 'hour', 'minute', 'second', 'meridian'];
66:
67: /**
68: * Special options used for datetime inputs.
69: *
70: * @var array
71: */
72: protected $_datetimeOptions = [
73: 'interval', 'round', 'monthNames', 'minYear', 'maxYear',
74: 'orderYear', 'timeFormat', 'second'
75: ];
76:
77: /**
78: * Default config for the helper.
79: *
80: * @var array
81: */
82: protected $_defaultConfig = [
83: 'idPrefix' => null,
84: 'errorClass' => 'form-error',
85: 'typeMap' => [
86: 'string' => 'text',
87: 'text' => 'textarea',
88: 'uuid' => 'string',
89: 'datetime' => 'datetime',
90: 'timestamp' => 'datetime',
91: 'date' => 'date',
92: 'time' => 'time',
93: 'boolean' => 'checkbox',
94: 'float' => 'number',
95: 'integer' => 'number',
96: 'tinyinteger' => 'number',
97: 'smallinteger' => 'number',
98: 'decimal' => 'number',
99: 'binary' => 'file',
100: ],
101: 'templates' => [
102: // Used for button elements in button().
103: 'button' => '<button{{attrs}}>{{text}}</button>',
104: // Used for checkboxes in checkbox() and multiCheckbox().
105: 'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
106: // Input group wrapper for checkboxes created via control().
107: 'checkboxFormGroup' => '{{label}}',
108: // Wrapper container for checkboxes.
109: 'checkboxWrapper' => '<div class="checkbox">{{label}}</div>',
110: // Widget ordering for date/time/datetime pickers.
111: 'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}',
112: // Error message wrapper elements.
113: 'error' => '<div class="error-message">{{content}}</div>',
114: // Container for error items.
115: 'errorList' => '<ul>{{content}}</ul>',
116: // Error item wrapper.
117: 'errorItem' => '<li>{{text}}</li>',
118: // File input used by file().
119: 'file' => '<input type="file" name="{{name}}"{{attrs}}>',
120: // Fieldset element used by allControls().
121: 'fieldset' => '<fieldset{{attrs}}>{{content}}</fieldset>',
122: // Open tag used by create().
123: 'formStart' => '<form{{attrs}}>',
124: // Close tag used by end().
125: 'formEnd' => '</form>',
126: // General grouping container for control(). Defines input/label ordering.
127: 'formGroup' => '{{label}}{{input}}',
128: // Wrapper content used to hide other content.
129: 'hiddenBlock' => '<div style="display:none;">{{content}}</div>',
130: // Generic input element.
131: 'input' => '<input type="{{type}}" name="{{name}}"{{attrs}}/>',
132: // Submit input element.
133: 'inputSubmit' => '<input type="{{type}}"{{attrs}}/>',
134: // Container element used by control().
135: 'inputContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
136: // Container element used by control() when a field has an error.
137: 'inputContainerError' => '<div class="input {{type}}{{required}} error">{{content}}{{error}}</div>',
138: // Label element when inputs are not nested inside the label.
139: 'label' => '<label{{attrs}}>{{text}}</label>',
140: // Label element used for radio and multi-checkbox inputs.
141: 'nestingLabel' => '{{hidden}}<label{{attrs}}>{{input}}{{text}}</label>',
142: // Legends created by allControls()
143: 'legend' => '<legend>{{text}}</legend>',
144: // Multi-Checkbox input set title element.
145: 'multicheckboxTitle' => '<legend>{{text}}</legend>',
146: // Multi-Checkbox wrapping container.
147: 'multicheckboxWrapper' => '<fieldset{{attrs}}>{{content}}</fieldset>',
148: // Option element used in select pickers.
149: 'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
150: // Option group element used in select pickers.
151: 'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
152: // Select element,
153: 'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
154: // Multi-select element,
155: 'selectMultiple' => '<select name="{{name}}[]" multiple="multiple"{{attrs}}>{{content}}</select>',
156: // Radio input element,
157: 'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
158: // Wrapping container for radio input/label,
159: 'radioWrapper' => '{{label}}',
160: // Textarea input element,
161: 'textarea' => '<textarea name="{{name}}"{{attrs}}>{{value}}</textarea>',
162: // Container for submit buttons.
163: 'submitContainer' => '<div class="submit">{{content}}</div>',
164: //Confirm javascript template for postLink()
165: 'confirmJs' => '{{confirm}}',
166: ],
167: // set HTML5 validation message to custom required/empty messages
168: 'autoSetCustomValidity' => false,
169: ];
170:
171: /**
172: * Default widgets
173: *
174: * @var array
175: */
176: protected $_defaultWidgets = [
177: 'button' => ['Button'],
178: 'checkbox' => ['Checkbox'],
179: 'file' => ['File'],
180: 'label' => ['Label'],
181: 'nestingLabel' => ['NestingLabel'],
182: 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'],
183: 'radio' => ['Radio', 'nestingLabel'],
184: 'select' => ['SelectBox'],
185: 'textarea' => ['Textarea'],
186: 'datetime' => ['DateTime', 'select'],
187: '_default' => ['Basic'],
188: ];
189:
190: /**
191: * List of fields created, used with secure forms.
192: *
193: * @var array
194: */
195: public $fields = [];
196:
197: /**
198: * Constant used internally to skip the securing process,
199: * and neither add the field to the hash or to the unlocked fields.
200: *
201: * @var string
202: */
203: const SECURE_SKIP = 'skip';
204:
205: /**
206: * Defines the type of form being created. Set by FormHelper::create().
207: *
208: * @var string|null
209: */
210: public $requestType;
211:
212: /**
213: * An array of field names that have been excluded from
214: * the Token hash used by SecurityComponent's validatePost method
215: *
216: * @see \Cake\View\Helper\FormHelper::_secure()
217: * @see \Cake\Controller\Component\SecurityComponent::validatePost()
218: * @var string[]
219: */
220: protected $_unlockedFields = [];
221:
222: /**
223: * Locator for input widgets.
224: *
225: * @var \Cake\View\Widget\WidgetLocator
226: */
227: protected $_locator;
228:
229: /**
230: * Context for the current form.
231: *
232: * @var \Cake\View\Form\ContextInterface|null
233: */
234: protected $_context;
235:
236: /**
237: * Context factory.
238: *
239: * @var \Cake\View\Form\ContextFactory
240: */
241: protected $_contextFactory;
242:
243: /**
244: * The action attribute value of the last created form.
245: * Used to make form/request specific hashes for SecurityComponent.
246: *
247: * @var string
248: */
249: protected $_lastAction = '';
250:
251: /**
252: * The sources to be used when retrieving prefilled input values.
253: *
254: * @var string[]
255: */
256: protected $_valueSources = ['context'];
257:
258: /**
259: * Grouped input types.
260: *
261: * @var string[]
262: */
263: protected $_groupedInputTypes = ['radio', 'multicheckbox', 'date', 'time', 'datetime'];
264:
265: /**
266: * Construct the widgets and binds the default context providers
267: *
268: * @param \Cake\View\View $View The View this helper is being attached to.
269: * @param array $config Configuration settings for the helper.
270: */
271: public function __construct(View $View, array $config = [])
272: {
273: $locator = null;
274: $widgets = $this->_defaultWidgets;
275: if (isset($config['registry'])) {
276: deprecationWarning('`registry` config key is deprecated in FormHelper, use `locator` instead.');
277: $config['locator'] = $config['registry'];
278: unset($config['registry']);
279: }
280: if (isset($config['locator'])) {
281: $locator = $config['locator'];
282: unset($config['locator']);
283: }
284: if (isset($config['widgets'])) {
285: if (is_string($config['widgets'])) {
286: $config['widgets'] = (array)$config['widgets'];
287: }
288: $widgets = $config['widgets'] + $widgets;
289: unset($config['widgets']);
290: }
291:
292: if (isset($config['groupedInputTypes'])) {
293: $this->_groupedInputTypes = $config['groupedInputTypes'];
294: unset($config['groupedInputTypes']);
295: }
296:
297: parent::__construct($View, $config);
298:
299: if (!$locator) {
300: $locator = new WidgetLocator($this->templater(), $this->_View, $widgets);
301: }
302: $this->setWidgetLocator($locator);
303: $this->_idPrefix = $this->getConfig('idPrefix');
304: }
305:
306: /**
307: * Set the widget registry the helper will use.
308: *
309: * @param \Cake\View\Widget\WidgetLocator|null $instance The registry instance to set.
310: * @param array $widgets An array of widgets
311: * @return \Cake\View\Widget\WidgetLocator
312: * @deprecated 3.6.0 Use FormHelper::widgetLocator() instead.
313: */
314: public function widgetRegistry(WidgetRegistry $instance = null, $widgets = [])
315: {
316: deprecationWarning('widgetRegistry is deprecated, use widgetLocator instead.');
317:
318: if ($instance) {
319: $instance->add($widgets);
320: $this->setWidgetLocator($instance);
321: }
322:
323: return $this->getWidgetLocator();
324: }
325:
326: /**
327: * Get the widget locator currently used by the helper.
328: *
329: * @return \Cake\View\Widget\WidgetLocator Current locator instance
330: * @since 3.6.0
331: */
332: public function getWidgetLocator()
333: {
334: return $this->_locator;
335: }
336:
337: /**
338: * Set the widget locator the helper will use.
339: *
340: * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set.
341: * @return $this
342: * @since 3.6.0
343: */
344: public function setWidgetLocator(WidgetLocator $instance)
345: {
346: $this->_locator = $instance;
347:
348: return $this;
349: }
350:
351: /**
352: * Set the context factory the helper will use.
353: *
354: * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set.
355: * @param array $contexts An array of context providers.
356: * @return \Cake\View\Form\ContextFactory
357: */
358: public function contextFactory(ContextFactory $instance = null, array $contexts = [])
359: {
360: if ($instance === null) {
361: if ($this->_contextFactory === null) {
362: $this->_contextFactory = ContextFactory::createWithDefaults($contexts);
363: }
364:
365: return $this->_contextFactory;
366: }
367: $this->_contextFactory = $instance;
368:
369: return $this->_contextFactory;
370: }
371:
372: /**
373: * Returns an HTML form element.
374: *
375: * ### Options:
376: *
377: * - `type` Form method defaults to autodetecting based on the form context. If
378: * the form context's isCreate() method returns false, a PUT request will be done.
379: * - `method` Set the form's method attribute explicitly.
380: * - `action` The controller action the form submits to, (optional). Use this option if you
381: * don't need to change the controller from the current request's controller. Deprecated since 3.2, use `url`.
382: * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url'
383: * you should leave 'action' undefined.
384: * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')`
385: * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype`
386: * to `multipart/form-data`.
387: * - `templates` The templates you want to use for this form. Any templates will be merged on top of
388: * the already loaded templates. This option can either be a filename in /config that contains
389: * the templates you want to load, or an array of templates to use.
390: * - `context` Additional options for the context class. For example the EntityContext accepts a 'table'
391: * option that allows you to set the specific Table class the form should be based on.
392: * - `idPrefix` Prefix for generated ID attributes.
393: * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources()
394: * - `templateVars` Provide template variables for the formStart template.
395: *
396: * @param mixed $context The context for which the form is being defined.
397: * Can be a ContextInterface instance, ORM entity, ORM resultset, or an
398: * array of meta data. You can use false or null to make a context-less form.
399: * @param array $options An array of html attributes and options.
400: * @return string An formatted opening FORM tag.
401: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create
402: */
403: public function create($context = null, array $options = [])
404: {
405: $append = '';
406:
407: if ($context instanceof ContextInterface) {
408: $this->context($context);
409: } else {
410: if (empty($options['context'])) {
411: $options['context'] = [];
412: }
413: $options['context']['entity'] = $context;
414: $context = $this->_getContext($options['context']);
415: unset($options['context']);
416: }
417:
418: $isCreate = $context->isCreate();
419:
420: $options += [
421: 'type' => $isCreate ? 'post' : 'put',
422: 'action' => null,
423: 'url' => null,
424: 'encoding' => strtolower(Configure::read('App.encoding')),
425: 'templates' => null,
426: 'idPrefix' => null,
427: 'valueSources' => null,
428: ];
429:
430: if (isset($options['action'])) {
431: trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED);
432: }
433:
434: if (isset($options['valueSources'])) {
435: $this->setValueSources($options['valueSources']);
436: unset($options['valueSources']);
437: }
438:
439: if ($options['idPrefix'] !== null) {
440: $this->_idPrefix = $options['idPrefix'];
441: }
442: $templater = $this->templater();
443:
444: if (!empty($options['templates'])) {
445: $templater->push();
446: $method = is_string($options['templates']) ? 'load' : 'add';
447: $templater->{$method}($options['templates']);
448: }
449: unset($options['templates']);
450:
451: if ($options['action'] === false || $options['url'] === false) {
452: $url = $this->_View->getRequest()->getRequestTarget();
453: $action = null;
454: } else {
455: $url = $this->_formUrl($context, $options);
456: $action = $this->Url->build($url);
457: }
458:
459: $this->_lastAction($url);
460: unset($options['url'], $options['action'], $options['idPrefix']);
461:
462: $htmlAttributes = [];
463: switch (strtolower($options['type'])) {
464: case 'get':
465: $htmlAttributes['method'] = 'get';
466: break;
467: // Set enctype for form
468: case 'file':
469: $htmlAttributes['enctype'] = 'multipart/form-data';
470: $options['type'] = $isCreate ? 'post' : 'put';
471: // Move on
472: case 'post':
473: // Move on
474: case 'put':
475: // Move on
476: case 'delete':
477: // Set patch method
478: case 'patch':
479: $append .= $this->hidden('_method', [
480: 'name' => '_method',
481: 'value' => strtoupper($options['type']),
482: 'secure' => static::SECURE_SKIP
483: ]);
484: // Default to post method
485: default:
486: $htmlAttributes['method'] = 'post';
487: }
488: if (isset($options['method'])) {
489: $htmlAttributes['method'] = strtolower($options['method']);
490: }
491: if (isset($options['enctype'])) {
492: $htmlAttributes['enctype'] = strtolower($options['enctype']);
493: }
494:
495: $this->requestType = strtolower($options['type']);
496:
497: if (!empty($options['encoding'])) {
498: $htmlAttributes['accept-charset'] = $options['encoding'];
499: }
500: unset($options['type'], $options['encoding']);
501:
502: $htmlAttributes += $options;
503:
504: $this->fields = [];
505: if ($this->requestType !== 'get') {
506: $append .= $this->_csrfField();
507: }
508:
509: if (!empty($append)) {
510: $append = $templater->format('hiddenBlock', ['content' => $append]);
511: }
512:
513: $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]);
514:
515: return $this->formatTemplate('formStart', [
516: 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr,
517: 'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : []
518: ]) . $append;
519: }
520:
521: /**
522: * Create the URL for a form based on the options.
523: *
524: * @param \Cake\View\Form\ContextInterface $context The context object to use.
525: * @param array $options An array of options from create()
526: * @return string|array The action attribute for the form.
527: */
528: protected function _formUrl($context, $options)
529: {
530: $request = $this->_View->getRequest();
531:
532: if ($options['action'] === null && $options['url'] === null) {
533: return $request->getRequestTarget();
534: }
535:
536: if (is_string($options['url']) ||
537: (is_array($options['url']) && isset($options['url']['_name']))
538: ) {
539: return $options['url'];
540: }
541:
542: if (isset($options['action']) && empty($options['url']['action'])) {
543: $options['url']['action'] = $options['action'];
544: }
545:
546: $actionDefaults = [
547: 'plugin' => $this->_View->getPlugin(),
548: 'controller' => $request->getParam('controller'),
549: 'action' => $request->getParam('action'),
550: ];
551:
552: $action = (array)$options['url'] + $actionDefaults;
553:
554: $pk = $context->primaryKey();
555: if (count($pk)) {
556: $id = $this->getSourceValue($pk[0]);
557: }
558: if (empty($action[0]) && isset($id)) {
559: $action[0] = $id;
560: }
561:
562: return $action;
563: }
564:
565: /**
566: * Correctly store the last created form action URL.
567: *
568: * @param string|array $url The URL of the last form.
569: * @return void
570: */
571: protected function _lastAction($url)
572: {
573: $action = Router::url($url, true);
574: $query = parse_url($action, PHP_URL_QUERY);
575: $query = $query ? '?' . $query : '';
576:
577: $path = parse_url($action, PHP_URL_PATH) ?: '';
578: $this->_lastAction = $path . $query;
579: }
580:
581: /**
582: * Return a CSRF input if the request data is present.
583: * Used to secure forms in conjunction with CsrfComponent &
584: * SecurityComponent
585: *
586: * @return string
587: */
588: protected function _csrfField()
589: {
590: $request = $this->_View->getRequest();
591:
592: if ($request->getParam('_Token.unlockedFields')) {
593: foreach ((array)$request->getParam('_Token.unlockedFields') as $unlocked) {
594: $this->_unlockedFields[] = $unlocked;
595: }
596: }
597: if (!$request->getParam('_csrfToken')) {
598: return '';
599: }
600:
601: return $this->hidden('_csrfToken', [
602: 'value' => $request->getParam('_csrfToken'),
603: 'secure' => static::SECURE_SKIP,
604: 'autocomplete' => 'off',
605: ]);
606: }
607:
608: /**
609: * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden
610: * input fields where appropriate.
611: *
612: * Resets some parts of the state, shared among multiple FormHelper::create() calls, to defaults.
613: *
614: * @param array $secureAttributes Secure attributes which will be passed as HTML attributes
615: * into the hidden input elements generated for the Security Component.
616: * @return string A closing FORM tag.
617: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#closing-the-form
618: */
619: public function end(array $secureAttributes = [])
620: {
621: $out = '';
622:
623: if ($this->requestType !== 'get' && $this->_View->getRequest()->getParam('_Token')) {
624: $out .= $this->secure($this->fields, $secureAttributes);
625: $this->fields = [];
626: $this->_unlockedFields = [];
627: }
628: $out .= $this->formatTemplate('formEnd', []);
629:
630: $this->templater()->pop();
631: $this->requestType = null;
632: $this->_context = null;
633: $this->_valueSources = ['context'];
634: $this->_idPrefix = $this->getConfig('idPrefix');
635:
636: return $out;
637: }
638:
639: /**
640: * Generates a hidden field with a security hash based on the fields used in
641: * the form.
642: *
643: * If $secureAttributes is set, these HTML attributes will be merged into
644: * the hidden input tags generated for the Security Component. This is
645: * especially useful to set HTML5 attributes like 'form'.
646: *
647: * @param array $fields If set specifies the list of fields to use when
648: * generating the hash, else $this->fields is being used.
649: * @param array $secureAttributes will be passed as HTML attributes into the hidden
650: * input elements generated for the Security Component.
651: * @return string A hidden input field with a security hash, or empty string when
652: * secured forms are not in use.
653: */
654: public function secure(array $fields = [], array $secureAttributes = [])
655: {
656: if (!$this->_View->getRequest()->getParam('_Token')) {
657: return '';
658: }
659: $debugSecurity = Configure::read('debug');
660: if (isset($secureAttributes['debugSecurity'])) {
661: $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity'];
662: unset($secureAttributes['debugSecurity']);
663: }
664: $secureAttributes['secure'] = static::SECURE_SKIP;
665: $secureAttributes['autocomplete'] = 'off';
666:
667: $tokenData = $this->_buildFieldToken(
668: $this->_lastAction,
669: $fields,
670: $this->_unlockedFields
671: );
672: $tokenFields = array_merge($secureAttributes, [
673: 'value' => $tokenData['fields'],
674: ]);
675: $out = $this->hidden('_Token.fields', $tokenFields);
676: $tokenUnlocked = array_merge($secureAttributes, [
677: 'value' => $tokenData['unlocked'],
678: ]);
679: $out .= $this->hidden('_Token.unlocked', $tokenUnlocked);
680: if ($debugSecurity) {
681: $tokenDebug = array_merge($secureAttributes, [
682: 'value' => urlencode(json_encode([
683: $this->_lastAction,
684: $fields,
685: $this->_unlockedFields
686: ])),
687: ]);
688: $out .= $this->hidden('_Token.debug', $tokenDebug);
689: }
690:
691: return $this->formatTemplate('hiddenBlock', ['content' => $out]);
692: }
693:
694: /**
695: * Add to or get the list of fields that are currently unlocked.
696: * Unlocked fields are not included in the field hash used by SecurityComponent
697: * unlocking a field once its been added to the list of secured fields will remove
698: * it from the list of fields.
699: *
700: * @param string|null $name The dot separated name for the field.
701: * @return array|null Either null, or the list of fields.
702: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#working-with-securitycomponent
703: */
704: public function unlockField($name = null)
705: {
706: if ($name === null) {
707: return $this->_unlockedFields;
708: }
709: if (!in_array($name, $this->_unlockedFields, true)) {
710: $this->_unlockedFields[] = $name;
711: }
712: $index = array_search($name, $this->fields, true);
713: if ($index !== false) {
714: unset($this->fields[$index]);
715: }
716: unset($this->fields[$name]);
717: }
718:
719: /**
720: * Determine which fields of a form should be used for hash.
721: * Populates $this->fields
722: *
723: * @param bool $lock Whether this field should be part of the validation
724: * or excluded as part of the unlockedFields.
725: * @param string|array $field Reference to field to be secured. Can be dot
726: * separated string to indicate nesting or array of fieldname parts.
727: * @param mixed $value Field value, if value should not be tampered with.
728: * @return void
729: */
730: protected function _secure($lock, $field, $value = null)
731: {
732: if (empty($field) && $field !== '0') {
733: return;
734: }
735:
736: if (is_string($field)) {
737: $field = Hash::filter(explode('.', $field));
738: }
739:
740: foreach ($this->_unlockedFields as $unlockField) {
741: $unlockParts = explode('.', $unlockField);
742: if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) {
743: return;
744: }
745: }
746:
747: $field = implode('.', $field);
748: $field = preg_replace('/(\.\d+)+$/', '', $field);
749:
750: if ($lock) {
751: if (!in_array($field, $this->fields, true)) {
752: if ($value !== null) {
753: $this->fields[$field] = $value;
754:
755: return;
756: }
757: if (isset($this->fields[$field]) && $value === null) {
758: unset($this->fields[$field]);
759: }
760: $this->fields[] = $field;
761: }
762: } else {
763: $this->unlockField($field);
764: }
765: }
766:
767: /**
768: * Returns true if there is an error for the given field, otherwise false
769: *
770: * @param string $field This should be "modelname.fieldname"
771: * @return bool If there are errors this method returns true, else false.
772: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#displaying-and-checking-errors
773: */
774: public function isFieldError($field)
775: {
776: return $this->_getContext()->hasError($field);
777: }
778:
779: /**
780: * Returns a formatted error message for given form field, '' if no errors.
781: *
782: * Uses the `error`, `errorList` and `errorItem` templates. The `errorList` and
783: * `errorItem` templates are used to format multiple error messages per field.
784: *
785: * ### Options:
786: *
787: * - `escape` boolean - Whether or not to html escape the contents of the error.
788: *
789: * @param string $field A field name, like "modelname.fieldname"
790: * @param string|array|null $text Error message as string or array of messages. If an array,
791: * it should be a hash of key names => messages.
792: * @param array $options See above.
793: * @return string Formatted errors or ''.
794: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#displaying-and-checking-errors
795: */
796: public function error($field, $text = null, array $options = [])
797: {
798: if (substr($field, -5) === '._ids') {
799: $field = substr($field, 0, -5);
800: }
801: $options += ['escape' => true];
802:
803: $context = $this->_getContext();
804: if (!$context->hasError($field)) {
805: return '';
806: }
807: $error = $context->error($field);
808:
809: if (is_array($text)) {
810: $tmp = [];
811: foreach ($error as $k => $e) {
812: if (isset($text[$k])) {
813: $tmp[] = $text[$k];
814: } elseif (isset($text[$e])) {
815: $tmp[] = $text[$e];
816: } else {
817: $tmp[] = $e;
818: }
819: }
820: $text = $tmp;
821: }
822:
823: if ($text !== null) {
824: $error = $text;
825: }
826:
827: if ($options['escape']) {
828: $error = h($error);
829: unset($options['escape']);
830: }
831:
832: if (is_array($error)) {
833: if (count($error) > 1) {
834: $errorText = [];
835: foreach ($error as $err) {
836: $errorText[] = $this->formatTemplate('errorItem', ['text' => $err]);
837: }
838: $error = $this->formatTemplate('errorList', [
839: 'content' => implode('', $errorText)
840: ]);
841: } else {
842: $error = array_pop($error);
843: }
844: }
845:
846: return $this->formatTemplate('error', ['content' => $error]);
847: }
848:
849: /**
850: * Returns a formatted LABEL element for HTML forms.
851: *
852: * Will automatically generate a `for` attribute if one is not provided.
853: *
854: * ### Options
855: *
856: * - `for` - Set the for attribute, if its not defined the for attribute
857: * will be generated from the $fieldName parameter using
858: * FormHelper::_domId().
859: * - `escape` - Set to `false` to turn off escaping of label text.
860: * Defaults to `true`.
861: *
862: * Examples:
863: *
864: * The text and for attribute are generated off of the fieldname
865: *
866: * ```
867: * echo $this->Form->label('published');
868: * <label for="PostPublished">Published</label>
869: * ```
870: *
871: * Custom text:
872: *
873: * ```
874: * echo $this->Form->label('published', 'Publish');
875: * <label for="published">Publish</label>
876: * ```
877: *
878: * Custom attributes:
879: *
880: * ```
881: * echo $this->Form->label('published', 'Publish', [
882: * 'for' => 'post-publish'
883: * ]);
884: * <label for="post-publish">Publish</label>
885: * ```
886: *
887: * Nesting an input tag:
888: *
889: * ```
890: * echo $this->Form->label('published', 'Publish', [
891: * 'for' => 'published',
892: * 'input' => $this->text('published'),
893: * ]);
894: * <label for="post-publish">Publish <input type="text" name="published"></label>
895: * ```
896: *
897: * If you want to nest inputs in the labels, you will need to modify the default templates.
898: *
899: * @param string $fieldName This should be "modelname.fieldname"
900: * @param string|null $text Text that will appear in the label field. If
901: * $text is left undefined the text will be inflected from the
902: * fieldName.
903: * @param array $options An array of HTML attributes.
904: * @return string The formatted LABEL element
905: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-labels
906: */
907: public function label($fieldName, $text = null, array $options = [])
908: {
909: if ($text === null) {
910: $text = $fieldName;
911: if (substr($text, -5) === '._ids') {
912: $text = substr($text, 0, -5);
913: }
914: if (strpos($text, '.') !== false) {
915: $fieldElements = explode('.', $text);
916: $text = array_pop($fieldElements);
917: }
918: if (substr($text, -3) === '_id') {
919: $text = substr($text, 0, -3);
920: }
921: $text = __(Inflector::humanize(Inflector::underscore($text)));
922: }
923:
924: if (isset($options['for'])) {
925: $labelFor = $options['for'];
926: unset($options['for']);
927: } else {
928: $labelFor = $this->_domId($fieldName);
929: }
930: $attrs = $options + [
931: 'for' => $labelFor,
932: 'text' => $text,
933: ];
934: if (isset($options['input'])) {
935: if (is_array($options['input'])) {
936: $attrs = $options['input'] + $attrs;
937: }
938:
939: return $this->widget('nestingLabel', $attrs);
940: }
941:
942: return $this->widget('label', $attrs);
943: }
944:
945: /**
946: * Generate a set of controls for `$fields`. If $fields is empty the fields
947: * of current model will be used.
948: *
949: * You can customize individual controls through `$fields`.
950: * ```
951: * $this->Form->allControls([
952: * 'name' => ['label' => 'custom label']
953: * ]);
954: * ```
955: *
956: * You can exclude fields by specifying them as `false`:
957: *
958: * ```
959: * $this->Form->allControls(['title' => false]);
960: * ```
961: *
962: * In the above example, no field would be generated for the title field.
963: *
964: * @param array $fields An array of customizations for the fields that will be
965: * generated. This array allows you to set custom types, labels, or other options.
966: * @param array $options Options array. Valid keys are:
967: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
968: * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
969: * be enabled
970: * - `legend` Set to false to disable the legend for the generated control set. Or supply a string
971: * to customize the legend text.
972: * @return string Completed form controls.
973: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms
974: */
975: public function allControls(array $fields = [], array $options = [])
976: {
977: $context = $this->_getContext();
978:
979: $modelFields = $context->fieldNames();
980:
981: $fields = array_merge(
982: Hash::normalize($modelFields),
983: Hash::normalize($fields)
984: );
985:
986: return $this->controls($fields, $options);
987: }
988:
989: /**
990: * Generate a set of controls for `$fields`. If $fields is empty the fields
991: * of current model will be used.
992: *
993: * @param array $fields An array of customizations for the fields that will be
994: * generated. This array allows you to set custom types, labels, or other options.
995: * @param array $options Options array. Valid keys are:
996: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
997: * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
998: * be enabled
999: * - `legend` Set to false to disable the legend for the generated control set. Or supply a string
1000: * to customize the legend text.
1001: * @return string Completed form controls.
1002: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms
1003: * @deprecated 3.4.0 Use FormHelper::allControls() instead.
1004: */
1005: public function allInputs(array $fields = [], array $options = [])
1006: {
1007: deprecationWarning(
1008: 'FormHelper::allInputs() is deprecated. ' .
1009: 'Use FormHelper::allControls() instead.'
1010: );
1011:
1012: return $this->allControls($fields, $options);
1013: }
1014:
1015: /**
1016: * Generate a set of controls for `$fields` wrapped in a fieldset element.
1017: *
1018: * You can customize individual controls through `$fields`.
1019: * ```
1020: * $this->Form->controls([
1021: * 'name' => ['label' => 'custom label'],
1022: * 'email'
1023: * ]);
1024: * ```
1025: *
1026: * @param array $fields An array of the fields to generate. This array allows
1027: * you to set custom types, labels, or other options.
1028: * @param array $options Options array. Valid keys are:
1029: * - `fieldset` Set to false to disable the fieldset. You can also pass an
1030: * array of params to be applied as HTML attributes to the fieldset tag.
1031: * If you pass an empty array, the fieldset will be enabled.
1032: * - `legend` Set to false to disable the legend for the generated input set.
1033: * Or supply a string to customize the legend text.
1034: * @return string Completed form inputs.
1035: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms
1036: */
1037: public function controls(array $fields, array $options = [])
1038: {
1039: $fields = Hash::normalize($fields);
1040:
1041: $out = '';
1042: foreach ($fields as $name => $opts) {
1043: if ($opts === false) {
1044: continue;
1045: }
1046:
1047: $out .= $this->control($name, (array)$opts);
1048: }
1049:
1050: return $this->fieldset($out, $options);
1051: }
1052:
1053: /**
1054: * Generate a set of controls for `$fields` wrapped in a fieldset element.
1055: *
1056: * @param array $fields An array of the fields to generate. This array allows
1057: * you to set custom types, labels, or other options.
1058: * @param array $options Options array. Valid keys are:
1059: * - `fieldset` Set to false to disable the fieldset. You can also pass an
1060: * array of params to be applied as HTML attributes to the fieldset tag.
1061: * If you pass an empty array, the fieldset will be enabled.
1062: * - `legend` Set to false to disable the legend for the generated input set.
1063: * Or supply a string to customize the legend text.
1064: * @return string Completed form inputs.
1065: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms
1066: * @deprecated 3.4.0 Use FormHelper::controls() instead.
1067: */
1068: public function inputs(array $fields, array $options = [])
1069: {
1070: deprecationWarning(
1071: 'FormHelper::inputs() is deprecated. ' .
1072: 'Use FormHelper::controls() instead.'
1073: );
1074:
1075: return $this->controls($fields, $options);
1076: }
1077:
1078: /**
1079: * Wrap a set of inputs in a fieldset
1080: *
1081: * @param string $fields the form inputs to wrap in a fieldset
1082: * @param array $options Options array. Valid keys are:
1083: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
1084: * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
1085: * be enabled
1086: * - `legend` Set to false to disable the legend for the generated input set. Or supply a string
1087: * to customize the legend text.
1088: * @return string Completed form inputs.
1089: */
1090: public function fieldset($fields = '', array $options = [])
1091: {
1092: $fieldset = $legend = true;
1093: $context = $this->_getContext();
1094: $out = $fields;
1095:
1096: if (isset($options['legend'])) {
1097: $legend = $options['legend'];
1098: }
1099: if (isset($options['fieldset'])) {
1100: $fieldset = $options['fieldset'];
1101: }
1102:
1103: if ($legend === true) {
1104: $isCreate = $context->isCreate();
1105: $modelName = Inflector::humanize(Inflector::singularize($this->_View->getRequest()->getParam('controller')));
1106: if (!$isCreate) {
1107: $legend = __d('cake', 'Edit {0}', $modelName);
1108: } else {
1109: $legend = __d('cake', 'New {0}', $modelName);
1110: }
1111: }
1112:
1113: if ($fieldset !== false) {
1114: if ($legend) {
1115: $out = $this->formatTemplate('legend', ['text' => $legend]) . $out;
1116: }
1117:
1118: $fieldsetParams = ['content' => $out, 'attrs' => ''];
1119: if (is_array($fieldset) && !empty($fieldset)) {
1120: $fieldsetParams['attrs'] = $this->templater()->formatAttributes($fieldset);
1121: }
1122: $out = $this->formatTemplate('fieldset', $fieldsetParams);
1123: }
1124:
1125: return $out;
1126: }
1127:
1128: /**
1129: * Generates a form control element complete with label and wrapper div.
1130: *
1131: * ### Options
1132: *
1133: * See each field type method for more information. Any options that are part of
1134: * $attributes or $options for the different **type** methods can be included in `$options` for input().
1135: * Additionally, any unknown keys that are not in the list below, or part of the selected type's options
1136: * will be treated as a regular HTML attribute for the generated input.
1137: *
1138: * - `type` - Force the type of widget you want. e.g. `type => 'select'`
1139: * - `label` - Either a string label, or an array of options for the label. See FormHelper::label().
1140: * - `options` - For widgets that take options e.g. radio, select.
1141: * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field
1142: * error and error messages).
1143: * - `empty` - String or boolean to enable empty select box options.
1144: * - `nestedInput` - Used with checkbox and radio inputs. Set to false to render inputs outside of label
1145: * elements. Can be set to true on any input to force the input inside the label. If you
1146: * enable this option for radio buttons you will also need to modify the default `radioWrapper` template.
1147: * - `templates` - The templates you want to use for this input. Any templates will be merged on top of
1148: * the already loaded templates. This option can either be a filename in /config that contains
1149: * the templates you want to load, or an array of templates to use.
1150: * - `labelOptions` - Either `false` to disable label around nestedWidgets e.g. radio, multicheckbox or an array
1151: * of attributes for the label tag. `selected` will be added to any classes e.g. `class => 'myclass'` where
1152: * widget is checked
1153: *
1154: * @param string $fieldName This should be "modelname.fieldname"
1155: * @param array $options Each type of input takes different options.
1156: * @return string Completed form widget.
1157: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-form-inputs
1158: */
1159: public function control($fieldName, array $options = [])
1160: {
1161: $options += [
1162: 'type' => null,
1163: 'label' => null,
1164: 'error' => null,
1165: 'required' => null,
1166: 'options' => null,
1167: 'templates' => [],
1168: 'templateVars' => [],
1169: 'labelOptions' => true
1170: ];
1171: $options = $this->_parseOptions($fieldName, $options);
1172: $options += ['id' => $this->_domId($fieldName)];
1173:
1174: $templater = $this->templater();
1175: $newTemplates = $options['templates'];
1176:
1177: if ($newTemplates) {
1178: $templater->push();
1179: $templateMethod = is_string($options['templates']) ? 'load' : 'add';
1180: $templater->{$templateMethod}($options['templates']);
1181: }
1182: unset($options['templates']);
1183:
1184: $error = null;
1185: $errorSuffix = '';
1186: if ($options['type'] !== 'hidden' && $options['error'] !== false) {
1187: if (is_array($options['error'])) {
1188: $error = $this->error($fieldName, $options['error'], $options['error']);
1189: } else {
1190: $error = $this->error($fieldName, $options['error']);
1191: }
1192: $errorSuffix = empty($error) ? '' : 'Error';
1193: unset($options['error']);
1194: }
1195:
1196: $label = $options['label'];
1197: unset($options['label']);
1198:
1199: $labelOptions = $options['labelOptions'];
1200: unset($options['labelOptions']);
1201:
1202: $nestedInput = false;
1203: if ($options['type'] === 'checkbox') {
1204: $nestedInput = true;
1205: }
1206: $nestedInput = isset($options['nestedInput']) ? $options['nestedInput'] : $nestedInput;
1207: unset($options['nestedInput']);
1208:
1209: if ($nestedInput === true && $options['type'] === 'checkbox' && !array_key_exists('hiddenField', $options) && $label !== false) {
1210: $options['hiddenField'] = '_split';
1211: }
1212:
1213: $input = $this->_getInput($fieldName, $options + ['labelOptions' => $labelOptions]);
1214: if ($options['type'] === 'hidden' || $options['type'] === 'submit') {
1215: if ($newTemplates) {
1216: $templater->pop();
1217: }
1218:
1219: return $input;
1220: }
1221:
1222: $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options);
1223: if ($nestedInput) {
1224: $result = $this->_groupTemplate(compact('label', 'error', 'options'));
1225: } else {
1226: $result = $this->_groupTemplate(compact('input', 'label', 'error', 'options'));
1227: }
1228: $result = $this->_inputContainerTemplate([
1229: 'content' => $result,
1230: 'error' => $error,
1231: 'errorSuffix' => $errorSuffix,
1232: 'options' => $options
1233: ]);
1234:
1235: if ($newTemplates) {
1236: $templater->pop();
1237: }
1238:
1239: return $result;
1240: }
1241:
1242: /**
1243: * Generates a form control element complete with label and wrapper div.
1244: *
1245: * @param string $fieldName This should be "modelname.fieldname"
1246: * @param array $options Each type of input takes different options.
1247: * @return string Completed form widget.
1248: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-form-inputs
1249: * @deprecated 3.4.0 Use FormHelper::control() instead.
1250: */
1251: public function input($fieldName, array $options = [])
1252: {
1253: deprecationWarning(
1254: 'FormHelper::input() is deprecated. ' .
1255: 'Use FormHelper::control() instead.'
1256: );
1257:
1258: return $this->control($fieldName, $options);
1259: }
1260:
1261: /**
1262: * Generates an group template element
1263: *
1264: * @param array $options The options for group template
1265: * @return string The generated group template
1266: */
1267: protected function _groupTemplate($options)
1268: {
1269: $groupTemplate = $options['options']['type'] . 'FormGroup';
1270: if (!$this->templater()->get($groupTemplate)) {
1271: $groupTemplate = 'formGroup';
1272: }
1273:
1274: return $this->formatTemplate($groupTemplate, [
1275: 'input' => isset($options['input']) ? $options['input'] : [],
1276: 'label' => $options['label'],
1277: 'error' => $options['error'],
1278: 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : []
1279: ]);
1280: }
1281:
1282: /**
1283: * Generates an input container template
1284: *
1285: * @param array $options The options for input container template
1286: * @return string The generated input container template
1287: */
1288: protected function _inputContainerTemplate($options)
1289: {
1290: $inputContainerTemplate = $options['options']['type'] . 'Container' . $options['errorSuffix'];
1291: if (!$this->templater()->get($inputContainerTemplate)) {
1292: $inputContainerTemplate = 'inputContainer' . $options['errorSuffix'];
1293: }
1294:
1295: return $this->formatTemplate($inputContainerTemplate, [
1296: 'content' => $options['content'],
1297: 'error' => $options['error'],
1298: 'required' => $options['options']['required'] ? ' required' : '',
1299: 'type' => $options['options']['type'],
1300: 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : []
1301: ]);
1302: }
1303:
1304: /**
1305: * Generates an input element
1306: *
1307: * @param string $fieldName the field name
1308: * @param array $options The options for the input element
1309: * @return string The generated input element
1310: */
1311: protected function _getInput($fieldName, $options)
1312: {
1313: $label = $options['labelOptions'];
1314: unset($options['labelOptions']);
1315: switch (strtolower($options['type'])) {
1316: case 'select':
1317: $opts = $options['options'];
1318: unset($options['options']);
1319:
1320: return $this->select($fieldName, $opts, $options + ['label' => $label]);
1321: case 'radio':
1322: $opts = $options['options'];
1323: unset($options['options']);
1324:
1325: return $this->radio($fieldName, $opts, $options + ['label' => $label]);
1326: case 'multicheckbox':
1327: $opts = $options['options'];
1328: unset($options['options']);
1329:
1330: return $this->multiCheckbox($fieldName, $opts, $options + ['label' => $label]);
1331: case 'input':
1332: throw new RuntimeException("Invalid type 'input' used for field '$fieldName'");
1333:
1334: default:
1335: return $this->{$options['type']}($fieldName, $options);
1336: }
1337: }
1338:
1339: /**
1340: * Generates input options array
1341: *
1342: * @param string $fieldName The name of the field to parse options for.
1343: * @param array $options Options list.
1344: * @return array Options
1345: */
1346: protected function _parseOptions($fieldName, $options)
1347: {
1348: $needsMagicType = false;
1349: if (empty($options['type'])) {
1350: $needsMagicType = true;
1351: $options['type'] = $this->_inputType($fieldName, $options);
1352: }
1353:
1354: $options = $this->_magicOptions($fieldName, $options, $needsMagicType);
1355:
1356: return $options;
1357: }
1358:
1359: /**
1360: * Returns the input type that was guessed for the provided fieldName,
1361: * based on the internal type it is associated too, its name and the
1362: * variables that can be found in the view template
1363: *
1364: * @param string $fieldName the name of the field to guess a type for
1365: * @param array $options the options passed to the input method
1366: * @return string
1367: */
1368: protected function _inputType($fieldName, $options)
1369: {
1370: $context = $this->_getContext();
1371:
1372: if ($context->isPrimaryKey($fieldName)) {
1373: return 'hidden';
1374: }
1375:
1376: if (substr($fieldName, -3) === '_id') {
1377: return 'select';
1378: }
1379:
1380: $internalType = $context->type($fieldName);
1381: $map = $this->_config['typeMap'];
1382: $type = isset($map[$internalType]) ? $map[$internalType] : 'text';
1383: $fieldName = array_slice(explode('.', $fieldName), -1)[0];
1384:
1385: switch (true) {
1386: case isset($options['checked']):
1387: return 'checkbox';
1388: case isset($options['options']):
1389: return 'select';
1390: case in_array($fieldName, ['passwd', 'password']):
1391: return 'password';
1392: case in_array($fieldName, ['tel', 'telephone', 'phone']):
1393: return 'tel';
1394: case $fieldName === 'email':
1395: return 'email';
1396: case isset($options['rows']) || isset($options['cols']):
1397: return 'textarea';
1398: }
1399:
1400: return $type;
1401: }
1402:
1403: /**
1404: * Selects the variable containing the options for a select field if present,
1405: * and sets the value to the 'options' key in the options array.
1406: *
1407: * @param string $fieldName The name of the field to find options for.
1408: * @param array $options Options list.
1409: * @return array
1410: */
1411: protected function _optionsOptions($fieldName, $options)
1412: {
1413: if (isset($options['options'])) {
1414: return $options;
1415: }
1416:
1417: $pluralize = true;
1418: if (substr($fieldName, -5) === '._ids') {
1419: $fieldName = substr($fieldName, 0, -5);
1420: $pluralize = false;
1421: } elseif (substr($fieldName, -3) === '_id') {
1422: $fieldName = substr($fieldName, 0, -3);
1423: }
1424: $fieldName = array_slice(explode('.', $fieldName), -1)[0];
1425:
1426: $varName = Inflector::variable(
1427: $pluralize ? Inflector::pluralize($fieldName) : $fieldName
1428: );
1429: $varOptions = $this->_View->get($varName);
1430: if (!is_array($varOptions) && !($varOptions instanceof Traversable)) {
1431: return $options;
1432: }
1433: if ($options['type'] !== 'radio') {
1434: $options['type'] = 'select';
1435: }
1436: $options['options'] = $varOptions;
1437:
1438: return $options;
1439: }
1440:
1441: /**
1442: * Magically set option type and corresponding options
1443: *
1444: * @param string $fieldName The name of the field to generate options for.
1445: * @param array $options Options list.
1446: * @param bool $allowOverride Whether or not it is allowed for this method to
1447: * overwrite the 'type' key in options.
1448: * @return array
1449: */
1450: protected function _magicOptions($fieldName, $options, $allowOverride)
1451: {
1452: $context = $this->_getContext();
1453:
1454: $options += [
1455: 'templateVars' => []
1456: ];
1457:
1458: if (!isset($options['required']) && $options['type'] !== 'hidden') {
1459: $options['required'] = $context->isRequired($fieldName);
1460: }
1461:
1462: if (method_exists($context, 'getRequiredMessage')) {
1463: $message = $context->getRequiredMessage($fieldName);
1464: $message = h($message);
1465:
1466: if ($options['required'] && $message) {
1467: $options['templateVars']['customValidityMessage'] = $message;
1468:
1469: if ($this->getConfig('autoSetCustomValidity')) {
1470: $options['oninvalid'] = "this.setCustomValidity(''); if (!this.validity.valid) this.setCustomValidity('$message')";
1471: $options['oninput'] = "this.setCustomValidity('')";
1472: }
1473: }
1474: }
1475:
1476: $type = $context->type($fieldName);
1477: $fieldDef = $context->attributes($fieldName);
1478:
1479: if ($options['type'] === 'number' && !isset($options['step'])) {
1480: if ($type === 'decimal' && isset($fieldDef['precision'])) {
1481: $decimalPlaces = $fieldDef['precision'];
1482: $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces));
1483: } elseif ($type === 'float') {
1484: $options['step'] = 'any';
1485: }
1486: }
1487:
1488: $typesWithOptions = ['text', 'number', 'radio', 'select'];
1489: $magicOptions = (in_array($options['type'], ['radio', 'select']) || $allowOverride);
1490: if ($magicOptions && in_array($options['type'], $typesWithOptions)) {
1491: $options = $this->_optionsOptions($fieldName, $options);
1492: }
1493:
1494: if ($allowOverride && substr($fieldName, -5) === '._ids') {
1495: $options['type'] = 'select';
1496: if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] != 'checkbox')) {
1497: $options['multiple'] = true;
1498: }
1499: }
1500:
1501: if ($options['type'] === 'select' && array_key_exists('step', $options)) {
1502: unset($options['step']);
1503: }
1504:
1505: $typesWithMaxLength = ['text', 'textarea', 'email', 'tel', 'url', 'search'];
1506: if (!array_key_exists('maxlength', $options)
1507: && in_array($options['type'], $typesWithMaxLength)
1508: ) {
1509: $maxLength = null;
1510: if (method_exists($context, 'getMaxLength')) {
1511: $maxLength = $context->getMaxLength($fieldName);
1512: }
1513:
1514: if ($maxLength === null && !empty($fieldDef['length'])) {
1515: $maxLength = $fieldDef['length'];
1516: }
1517:
1518: if ($maxLength !== null) {
1519: $options['maxlength'] = min($maxLength, 100000);
1520: }
1521: }
1522:
1523: if (in_array($options['type'], ['datetime', 'date', 'time', 'select'])) {
1524: $options += ['empty' => false];
1525: }
1526:
1527: return $options;
1528: }
1529:
1530: /**
1531: * Generate label for input
1532: *
1533: * @param string $fieldName The name of the field to generate label for.
1534: * @param array $options Options list.
1535: * @return bool|string false or Generated label element
1536: */
1537: protected function _getLabel($fieldName, $options)
1538: {
1539: if ($options['type'] === 'hidden') {
1540: return false;
1541: }
1542:
1543: $label = null;
1544: if (isset($options['label'])) {
1545: $label = $options['label'];
1546: }
1547:
1548: if ($label === false && $options['type'] === 'checkbox') {
1549: return $options['input'];
1550: }
1551: if ($label === false) {
1552: return false;
1553: }
1554:
1555: return $this->_inputLabel($fieldName, $label, $options);
1556: }
1557:
1558: /**
1559: * Extracts a single option from an options array.
1560: *
1561: * @param string $name The name of the option to pull out.
1562: * @param array $options The array of options you want to extract.
1563: * @param mixed $default The default option value
1564: * @return mixed the contents of the option or default
1565: */
1566: protected function _extractOption($name, $options, $default = null)
1567: {
1568: if (array_key_exists($name, $options)) {
1569: return $options[$name];
1570: }
1571:
1572: return $default;
1573: }
1574:
1575: /**
1576: * Generate a label for an input() call.
1577: *
1578: * $options can contain a hash of id overrides. These overrides will be
1579: * used instead of the generated values if present.
1580: *
1581: * @param string $fieldName The name of the field to generate label for.
1582: * @param string $label Label text.
1583: * @param array $options Options for the label element.
1584: * @return string Generated label element
1585: */
1586: protected function _inputLabel($fieldName, $label, $options)
1587: {
1588: $options += ['id' => null, 'input' => null, 'nestedInput' => false, 'templateVars' => []];
1589: $labelAttributes = ['templateVars' => $options['templateVars']];
1590: if (is_array($label)) {
1591: $labelText = null;
1592: if (isset($label['text'])) {
1593: $labelText = $label['text'];
1594: unset($label['text']);
1595: }
1596: $labelAttributes = array_merge($labelAttributes, $label);
1597: } else {
1598: $labelText = $label;
1599: }
1600:
1601: $labelAttributes['for'] = $options['id'];
1602: if (in_array($options['type'], $this->_groupedInputTypes, true)) {
1603: $labelAttributes['for'] = false;
1604: }
1605: if ($options['nestedInput']) {
1606: $labelAttributes['input'] = $options['input'];
1607: }
1608: if (isset($options['escape'])) {
1609: $labelAttributes['escape'] = $options['escape'];
1610: }
1611:
1612: return $this->label($fieldName, $labelText, $labelAttributes);
1613: }
1614:
1615: /**
1616: * Creates a checkbox input widget.
1617: *
1618: * ### Options:
1619: *
1620: * - `value` - the value of the checkbox
1621: * - `checked` - boolean indicate that this checkbox is checked.
1622: * - `hiddenField` - boolean to indicate if you want the results of checkbox() to include
1623: * a hidden input with a value of ''.
1624: * - `disabled` - create a disabled input.
1625: * - `default` - Set the default value for the checkbox. This allows you to start checkboxes
1626: * as checked, without having to check the POST data. A matching POST data value, will overwrite
1627: * the default value.
1628: *
1629: * @param string $fieldName Name of a field, like this "modelname.fieldname"
1630: * @param array $options Array of HTML attributes.
1631: * @return string|array An HTML text input element.
1632: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-checkboxes
1633: */
1634: public function checkbox($fieldName, array $options = [])
1635: {
1636: $options += ['hiddenField' => true, 'value' => 1];
1637:
1638: // Work around value=>val translations.
1639: $value = $options['value'];
1640: unset($options['value']);
1641: $options = $this->_initInputField($fieldName, $options);
1642: $options['value'] = $value;
1643:
1644: $output = '';
1645: if ($options['hiddenField']) {
1646: $hiddenOptions = [
1647: 'name' => $options['name'],
1648: 'value' => $options['hiddenField'] !== true && $options['hiddenField'] !== '_split' ? $options['hiddenField'] : '0',
1649: 'form' => isset($options['form']) ? $options['form'] : null,
1650: 'secure' => false
1651: ];
1652: if (isset($options['disabled']) && $options['disabled']) {
1653: $hiddenOptions['disabled'] = 'disabled';
1654: }
1655: $output = $this->hidden($fieldName, $hiddenOptions);
1656: }
1657:
1658: if ($options['hiddenField'] === '_split') {
1659: unset($options['hiddenField'], $options['type']);
1660:
1661: return ['hidden' => $output, 'input' => $this->widget('checkbox', $options)];
1662: }
1663: unset($options['hiddenField'], $options['type']);
1664:
1665: return $output . $this->widget('checkbox', $options);
1666: }
1667:
1668: /**
1669: * Creates a set of radio widgets.
1670: *
1671: * ### Attributes:
1672: *
1673: * - `value` - Indicates the value when this radio button is checked.
1674: * - `label` - Either `false` to disable label around the widget or an array of attributes for
1675: * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where widget
1676: * is checked
1677: * - `hiddenField` - boolean to indicate if you want the results of radio() to include
1678: * a hidden input with a value of ''. This is useful for creating radio sets that are non-continuous.
1679: * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. Use an array of
1680: * values to disable specific radio buttons.
1681: * - `empty` - Set to `true` to create an input with the value '' as the first option. When `true`
1682: * the radio label will be 'empty'. Set this option to a string to control the label value.
1683: *
1684: * @param string $fieldName Name of a field, like this "modelname.fieldname"
1685: * @param array|\Traversable $options Radio button options array.
1686: * @param array $attributes Array of attributes.
1687: * @return string Completed radio widget set.
1688: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-radio-buttons
1689: */
1690: public function radio($fieldName, $options = [], array $attributes = [])
1691: {
1692: $attributes['options'] = $options;
1693: $attributes['idPrefix'] = $this->_idPrefix;
1694: $attributes = $this->_initInputField($fieldName, $attributes);
1695:
1696: $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true;
1697: unset($attributes['hiddenField']);
1698:
1699: $radio = $this->widget('radio', $attributes);
1700:
1701: $hidden = '';
1702: if ($hiddenField) {
1703: $hidden = $this->hidden($fieldName, [
1704: 'value' => $hiddenField === true ? '' : $hiddenField,
1705: 'form' => isset($attributes['form']) ? $attributes['form'] : null,
1706: 'name' => $attributes['name'],
1707: ]);
1708: }
1709:
1710: return $hidden . $radio;
1711: }
1712:
1713: /**
1714: * Missing method handler - implements various simple input types. Is used to create inputs
1715: * of various types. e.g. `$this->Form->text();` will create `<input type="text" />` while
1716: * `$this->Form->range();` will create `<input type="range" />`
1717: *
1718: * ### Usage
1719: *
1720: * ```
1721: * $this->Form->search('User.query', ['value' => 'test']);
1722: * ```
1723: *
1724: * Will make an input like:
1725: *
1726: * `<input type="search" id="UserQuery" name="User[query]" value="test" />`
1727: *
1728: * The first argument to an input type should always be the fieldname, in `Model.field` format.
1729: * The second argument should always be an array of attributes for the input.
1730: *
1731: * @param string $method Method name / input type to make.
1732: * @param array $params Parameters for the method call
1733: * @return string Formatted input method.
1734: * @throws \Cake\Core\Exception\Exception When there are no params for the method call.
1735: */
1736: public function __call($method, $params)
1737: {
1738: $options = [];
1739: if (empty($params)) {
1740: throw new Exception(sprintf('Missing field name for FormHelper::%s', $method));
1741: }
1742: if (isset($params[1])) {
1743: $options = $params[1];
1744: }
1745: if (!isset($options['type'])) {
1746: $options['type'] = $method;
1747: }
1748: $options = $this->_initInputField($params[0], $options);
1749:
1750: return $this->widget($options['type'], $options);
1751: }
1752:
1753: /**
1754: * Creates a textarea widget.
1755: *
1756: * ### Options:
1757: *
1758: * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true.
1759: *
1760: * @param string $fieldName Name of a field, in the form "modelname.fieldname"
1761: * @param array $options Array of HTML attributes, and special options above.
1762: * @return string A generated HTML text input element
1763: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-textareas
1764: */
1765: public function textarea($fieldName, array $options = [])
1766: {
1767: $options = $this->_initInputField($fieldName, $options);
1768: unset($options['type']);
1769:
1770: return $this->widget('textarea', $options);
1771: }
1772:
1773: /**
1774: * Creates a hidden input field.
1775: *
1776: * @param string $fieldName Name of a field, in the form of "modelname.fieldname"
1777: * @param array $options Array of HTML attributes.
1778: * @return string A generated hidden input
1779: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-hidden-inputs
1780: */
1781: public function hidden($fieldName, array $options = [])
1782: {
1783: $options += ['required' => false, 'secure' => true];
1784:
1785: $secure = $options['secure'];
1786: unset($options['secure']);
1787:
1788: $options = $this->_initInputField($fieldName, array_merge(
1789: $options,
1790: ['secure' => static::SECURE_SKIP]
1791: ));
1792:
1793: if ($secure === true) {
1794: $this->_secure(true, $this->_secureFieldName($options['name']), (string)$options['val']);
1795: }
1796:
1797: $options['type'] = 'hidden';
1798:
1799: return $this->widget('hidden', $options);
1800: }
1801:
1802: /**
1803: * Creates file input widget.
1804: *
1805: * @param string $fieldName Name of a field, in the form "modelname.fieldname"
1806: * @param array $options Array of HTML attributes.
1807: * @return string A generated file input.
1808: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-file-inputs
1809: */
1810: public function file($fieldName, array $options = [])
1811: {
1812: $options += ['secure' => true];
1813: $options = $this->_initInputField($fieldName, $options);
1814:
1815: unset($options['type']);
1816:
1817: return $this->widget('file', $options);
1818: }
1819:
1820: /**
1821: * Creates a `<button>` tag.
1822: *
1823: * The type attribute defaults to `type="submit"`
1824: * You can change it to a different value by using `$options['type']`.
1825: *
1826: * ### Options:
1827: *
1828: * - `escape` - HTML entity encode the $title of the button. Defaults to false.
1829: * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
1830: *
1831: * @param string $title The button's caption. Not automatically HTML encoded
1832: * @param array $options Array of options and HTML attributes.
1833: * @return string A HTML button tag.
1834: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-button-elements
1835: */
1836: public function button($title, array $options = [])
1837: {
1838: $options += ['type' => 'submit', 'escape' => false, 'secure' => false, 'confirm' => null];
1839: $options['text'] = $title;
1840:
1841: $confirmMessage = $options['confirm'];
1842: unset($options['confirm']);
1843: if ($confirmMessage) {
1844: $options['onclick'] = $this->_confirm($confirmMessage, 'return true;', 'return false;', $options);
1845: }
1846:
1847: return $this->widget('button', $options);
1848: }
1849:
1850: /**
1851: * Create a `<button>` tag with a surrounding `<form>` that submits via POST as default.
1852: *
1853: * This method creates a `<form>` element. So do not use this method in an already opened form.
1854: * Instead use FormHelper::submit() or FormHelper::button() to create buttons inside opened forms.
1855: *
1856: * ### Options:
1857: *
1858: * - `data` - Array with key/value to pass in input hidden
1859: * - `method` - Request method to use. Set to 'delete' or others to simulate
1860: * HTTP/1.1 DELETE (or others) request. Defaults to 'post'.
1861: * - `form` - Array with any option that FormHelper::create() can take
1862: * - Other options is the same of button method.
1863: * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
1864: *
1865: * @param string $title The button's caption. Not automatically HTML encoded
1866: * @param string|array $url URL as string or array
1867: * @param array $options Array of options and HTML attributes.
1868: * @return string A HTML button tag.
1869: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-standalone-buttons-and-post-links
1870: */
1871: public function postButton($title, $url, array $options = [])
1872: {
1873: $formOptions = ['url' => $url];
1874: if (isset($options['method'])) {
1875: $formOptions['type'] = $options['method'];
1876: unset($options['method']);
1877: }
1878: if (isset($options['form']) && is_array($options['form'])) {
1879: $formOptions = $options['form'] + $formOptions;
1880: unset($options['form']);
1881: }
1882: $out = $this->create(false, $formOptions);
1883: if (isset($options['data']) && is_array($options['data'])) {
1884: foreach (Hash::flatten($options['data']) as $key => $value) {
1885: $out .= $this->hidden($key, ['value' => $value]);
1886: }
1887: unset($options['data']);
1888: }
1889: $out .= $this->button($title, $options);
1890: $out .= $this->end();
1891:
1892: return $out;
1893: }
1894:
1895: /**
1896: * Creates an HTML link, but access the URL using the method you specify
1897: * (defaults to POST). Requires javascript to be enabled in browser.
1898: *
1899: * This method creates a `<form>` element. If you want to use this method inside of an
1900: * existing form, you must use the `block` option so that the new form is being set to
1901: * a view block that can be rendered outside of the main form.
1902: *
1903: * If all you are looking for is a button to submit your form, then you should use
1904: * `FormHelper::button()` or `FormHelper::submit()` instead.
1905: *
1906: * ### Options:
1907: *
1908: * - `data` - Array with key/value to pass in input hidden
1909: * - `method` - Request method to use. Set to 'delete' to simulate
1910: * HTTP/1.1 DELETE request. Defaults to 'post'.
1911: * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
1912: * - `block` - Set to true to append form to view block "postLink" or provide
1913: * custom block name.
1914: * - Other options are the same of HtmlHelper::link() method.
1915: * - The option `onclick` will be replaced.
1916: *
1917: * @param string $title The content to be wrapped by <a> tags.
1918: * @param string|array|null $url Cake-relative URL or array of URL parameters, or
1919: * external URL (starts with http://)
1920: * @param array $options Array of HTML attributes.
1921: * @return string An `<a />` element.
1922: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-standalone-buttons-and-post-links
1923: */
1924: public function postLink($title, $url = null, array $options = [])
1925: {
1926: $options += ['block' => null, 'confirm' => null];
1927:
1928: $requestMethod = 'POST';
1929: if (!empty($options['method'])) {
1930: $requestMethod = strtoupper($options['method']);
1931: unset($options['method']);
1932: }
1933:
1934: $confirmMessage = $options['confirm'];
1935: unset($options['confirm']);
1936:
1937: $formName = str_replace('.', '', uniqid('post_', true));
1938: $formOptions = [
1939: 'name' => $formName,
1940: 'style' => 'display:none;',
1941: 'method' => 'post',
1942: ];
1943: if (isset($options['target'])) {
1944: $formOptions['target'] = $options['target'];
1945: unset($options['target']);
1946: }
1947: $templater = $this->templater();
1948:
1949: $restoreAction = $this->_lastAction;
1950: $this->_lastAction($url);
1951:
1952: $action = $templater->formatAttributes([
1953: 'action' => $this->Url->build($url),
1954: 'escape' => false
1955: ]);
1956:
1957: $out = $this->formatTemplate('formStart', [
1958: 'attrs' => $templater->formatAttributes($formOptions) . $action
1959: ]);
1960: $out .= $this->hidden('_method', [
1961: 'value' => $requestMethod,
1962: 'secure' => static::SECURE_SKIP
1963: ]);
1964: $out .= $this->_csrfField();
1965:
1966: $fields = [];
1967: if (isset($options['data']) && is_array($options['data'])) {
1968: foreach (Hash::flatten($options['data']) as $key => $value) {
1969: $fields[$key] = $value;
1970: $out .= $this->hidden($key, ['value' => $value, 'secure' => static::SECURE_SKIP]);
1971: }
1972: unset($options['data']);
1973: }
1974: $out .= $this->secure($fields);
1975: $out .= $this->formatTemplate('formEnd', []);
1976: $this->_lastAction = $restoreAction;
1977:
1978: if ($options['block']) {
1979: if ($options['block'] === true) {
1980: $options['block'] = __FUNCTION__;
1981: }
1982: $this->_View->append($options['block'], $out);
1983: $out = '';
1984: }
1985: unset($options['block']);
1986:
1987: $url = '#';
1988: $onClick = 'document.' . $formName . '.submit();';
1989: if ($confirmMessage) {
1990: $confirm = $this->_confirm($confirmMessage, $onClick, '', $options);
1991: } else {
1992: $confirm = $onClick . ' ';
1993: }
1994: $confirm .= 'event.returnValue = false; return false;';
1995: $options['onclick'] = $this->templater()->format('confirmJs', [
1996: 'confirmMessage' => $this->_cleanConfirmMessage($confirmMessage),
1997: 'formName' => $formName,
1998: 'confirm' => $confirm
1999: ]);
2000:
2001: $out .= $this->Html->link($title, $url, $options);
2002:
2003: return $out;
2004: }
2005:
2006: /**
2007: * Creates a submit button element. This method will generate `<input />` elements that
2008: * can be used to submit, and reset forms by using $options. image submits can be created by supplying an
2009: * image path for $caption.
2010: *
2011: * ### Options
2012: *
2013: * - `type` - Set to 'reset' for reset inputs. Defaults to 'submit'
2014: * - `templateVars` - Additional template variables for the input element and its container.
2015: * - Other attributes will be assigned to the input element.
2016: *
2017: * @param string|null $caption The label appearing on the button OR if string contains :// or the
2018: * extension .jpg, .jpe, .jpeg, .gif, .png use an image if the extension
2019: * exists, AND the first character is /, image is relative to webroot,
2020: * OR if the first character is not /, image is relative to webroot/img.
2021: * @param array $options Array of options. See above.
2022: * @return string A HTML submit button
2023: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-buttons-and-submit-elements
2024: */
2025: public function submit($caption = null, array $options = [])
2026: {
2027: if (!is_string($caption) && empty($caption)) {
2028: $caption = __d('cake', 'Submit');
2029: }
2030: $options += [
2031: 'type' => 'submit',
2032: 'secure' => false,
2033: 'templateVars' => []
2034: ];
2035:
2036: if (isset($options['name'])) {
2037: $this->_secure($options['secure'], $this->_secureFieldName($options['name']));
2038: }
2039: unset($options['secure']);
2040:
2041: $isUrl = strpos($caption, '://') !== false;
2042: $isImage = preg_match('/\.(jpg|jpe|jpeg|gif|png|ico)$/', $caption);
2043:
2044: $type = $options['type'];
2045: unset($options['type']);
2046:
2047: if ($isUrl || $isImage) {
2048: $unlockFields = ['x', 'y'];
2049: if (isset($options['name'])) {
2050: $unlockFields = [
2051: $options['name'] . '_x',
2052: $options['name'] . '_y'
2053: ];
2054: }
2055: foreach ($unlockFields as $ignore) {
2056: $this->unlockField($ignore);
2057: }
2058: $type = 'image';
2059: }
2060:
2061: if ($isUrl) {
2062: $options['src'] = $caption;
2063: } elseif ($isImage) {
2064: if ($caption[0] !== '/') {
2065: $url = $this->Url->webroot(Configure::read('App.imageBaseUrl') . $caption);
2066: } else {
2067: $url = $this->Url->webroot(trim($caption, '/'));
2068: }
2069: $url = $this->Url->assetTimestamp($url);
2070: $options['src'] = $url;
2071: } else {
2072: $options['value'] = $caption;
2073: }
2074:
2075: $input = $this->formatTemplate('inputSubmit', [
2076: 'type' => $type,
2077: 'attrs' => $this->templater()->formatAttributes($options),
2078: 'templateVars' => $options['templateVars']
2079: ]);
2080:
2081: return $this->formatTemplate('submitContainer', [
2082: 'content' => $input,
2083: 'templateVars' => $options['templateVars']
2084: ]);
2085: }
2086:
2087: /**
2088: * Returns a formatted SELECT element.
2089: *
2090: * ### Attributes:
2091: *
2092: * - `multiple` - show a multiple select box. If set to 'checkbox' multiple checkboxes will be
2093: * created instead.
2094: * - `empty` - If true, the empty select option is shown. If a string,
2095: * that string is displayed as the empty element.
2096: * - `escape` - If true contents of options will be HTML entity encoded. Defaults to true.
2097: * - `val` The selected value of the input.
2098: * - `disabled` - Control the disabled attribute. When creating a select box, set to true to disable the
2099: * select box. Set to an array to disable specific option elements.
2100: *
2101: * ### Using options
2102: *
2103: * A simple array will create normal options:
2104: *
2105: * ```
2106: * $options = [1 => 'one', 2 => 'two'];
2107: * $this->Form->select('Model.field', $options));
2108: * ```
2109: *
2110: * While a nested options array will create optgroups with options inside them.
2111: * ```
2112: * $options = [
2113: * 1 => 'bill',
2114: * 'fred' => [
2115: * 2 => 'fred',
2116: * 3 => 'fred jr.'
2117: * ]
2118: * ];
2119: * $this->Form->select('Model.field', $options);
2120: * ```
2121: *
2122: * If you have multiple options that need to have the same value attribute, you can
2123: * use an array of arrays to express this:
2124: *
2125: * ```
2126: * $options = [
2127: * ['text' => 'United states', 'value' => 'USA'],
2128: * ['text' => 'USA', 'value' => 'USA'],
2129: * ];
2130: * ```
2131: *
2132: * @param string $fieldName Name attribute of the SELECT
2133: * @param array|\Traversable $options Array of the OPTION elements (as 'value'=>'Text' pairs) to be used in the
2134: * SELECT element
2135: * @param array $attributes The HTML attributes of the select element.
2136: * @return string Formatted SELECT element
2137: * @see \Cake\View\Helper\FormHelper::multiCheckbox() for creating multiple checkboxes.
2138: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-select-pickers
2139: */
2140: public function select($fieldName, $options = [], array $attributes = [])
2141: {
2142: $attributes += [
2143: 'disabled' => null,
2144: 'escape' => true,
2145: 'hiddenField' => true,
2146: 'multiple' => null,
2147: 'secure' => true,
2148: 'empty' => false,
2149: ];
2150:
2151: if ($attributes['multiple'] === 'checkbox') {
2152: unset($attributes['multiple'], $attributes['empty']);
2153:
2154: return $this->multiCheckbox($fieldName, $options, $attributes);
2155: }
2156:
2157: unset($attributes['label']);
2158:
2159: // Secure the field if there are options, or it's a multi select.
2160: // Single selects with no options don't submit, but multiselects do.
2161: if ($attributes['secure'] &&
2162: empty($options) &&
2163: empty($attributes['empty']) &&
2164: empty($attributes['multiple'])
2165: ) {
2166: $attributes['secure'] = false;
2167: }
2168:
2169: $attributes = $this->_initInputField($fieldName, $attributes);
2170: $attributes['options'] = $options;
2171:
2172: $hidden = '';
2173: if ($attributes['multiple'] && $attributes['hiddenField']) {
2174: $hiddenAttributes = [
2175: 'name' => $attributes['name'],
2176: 'value' => '',
2177: 'form' => isset($attributes['form']) ? $attributes['form'] : null,
2178: 'secure' => false,
2179: ];
2180: $hidden = $this->hidden($fieldName, $hiddenAttributes);
2181: }
2182: unset($attributes['hiddenField'], $attributes['type']);
2183:
2184: return $hidden . $this->widget('select', $attributes);
2185: }
2186:
2187: /**
2188: * Creates a set of checkboxes out of options.
2189: *
2190: * ### Options
2191: *
2192: * - `escape` - If true contents of options will be HTML entity encoded. Defaults to true.
2193: * - `val` The selected value of the input.
2194: * - `class` - When using multiple = checkbox the class name to apply to the divs. Defaults to 'checkbox'.
2195: * - `disabled` - Control the disabled attribute. When creating checkboxes, `true` will disable all checkboxes.
2196: * You can also set disabled to a list of values you want to disable when creating checkboxes.
2197: * - `hiddenField` - Set to false to remove the hidden field that ensures a value
2198: * is always submitted.
2199: * - `label` - Either `false` to disable label around the widget or an array of attributes for
2200: * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where
2201: * widget is checked
2202: *
2203: * Can be used in place of a select box with the multiple attribute.
2204: *
2205: * @param string $fieldName Name attribute of the SELECT
2206: * @param array|\Traversable $options Array of the OPTION elements
2207: * (as 'value'=>'Text' pairs) to be used in the checkboxes element.
2208: * @param array $attributes The HTML attributes of the select element.
2209: * @return string Formatted SELECT element
2210: * @see \Cake\View\Helper\FormHelper::select() for supported option formats.
2211: */
2212: public function multiCheckbox($fieldName, $options, array $attributes = [])
2213: {
2214: $attributes += [
2215: 'disabled' => null,
2216: 'escape' => true,
2217: 'hiddenField' => true,
2218: 'secure' => true,
2219: ];
2220: $attributes = $this->_initInputField($fieldName, $attributes);
2221: $attributes['options'] = $options;
2222: $attributes['idPrefix'] = $this->_idPrefix;
2223:
2224: $hidden = '';
2225: if ($attributes['hiddenField']) {
2226: $hiddenAttributes = [
2227: 'name' => $attributes['name'],
2228: 'value' => '',
2229: 'secure' => false,
2230: 'disabled' => $attributes['disabled'] === true || $attributes['disabled'] === 'disabled',
2231: ];
2232: $hidden = $this->hidden($fieldName, $hiddenAttributes);
2233: }
2234: unset($attributes['hiddenField']);
2235:
2236: return $hidden . $this->widget('multicheckbox', $attributes);
2237: }
2238:
2239: /**
2240: * Helper method for the various single datetime component methods.
2241: *
2242: * @param array $options The options array.
2243: * @param string $keep The option to not disable.
2244: * @return array
2245: */
2246: protected function _singleDatetime($options, $keep)
2247: {
2248: $off = array_diff($this->_datetimeParts, [$keep]);
2249: $off = array_combine(
2250: $off,
2251: array_fill(0, count($off), false)
2252: );
2253:
2254: $attributes = array_diff_key(
2255: $options,
2256: array_flip(array_merge($this->_datetimeOptions, ['value', 'empty']))
2257: );
2258: $options = $options + $off + [$keep => $attributes];
2259:
2260: if (isset($options['value'])) {
2261: $options['val'] = $options['value'];
2262: }
2263:
2264: return $options;
2265: }
2266:
2267: /**
2268: * Returns a SELECT element for days.
2269: *
2270: * ### Options:
2271: *
2272: * - `empty` - If true, the empty select option is shown. If a string,
2273: * that string is displayed as the empty element.
2274: * - `value` The selected value of the input.
2275: *
2276: * @param string|null $fieldName Prefix name for the SELECT element
2277: * @param array $options Options & HTML attributes for the select element
2278: * @return string A generated day select box.
2279: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-day-inputs
2280: */
2281: public function day($fieldName = null, array $options = [])
2282: {
2283: $options = $this->_singleDatetime($options, 'day');
2284:
2285: if (isset($options['val']) && $options['val'] > 0 && $options['val'] <= 31) {
2286: $options['val'] = [
2287: 'year' => date('Y'),
2288: 'month' => date('m'),
2289: 'day' => (int)$options['val']
2290: ];
2291: }
2292:
2293: return $this->dateTime($fieldName, $options);
2294: }
2295:
2296: /**
2297: * Returns a SELECT element for years
2298: *
2299: * ### Attributes:
2300: *
2301: * - `empty` - If true, the empty select option is shown. If a string,
2302: * that string is displayed as the empty element.
2303: * - `orderYear` - Ordering of year values in select options.
2304: * Possible values 'asc', 'desc'. Default 'desc'
2305: * - `value` The selected value of the input.
2306: * - `maxYear` The max year to appear in the select element.
2307: * - `minYear` The min year to appear in the select element.
2308: *
2309: * @param string $fieldName Prefix name for the SELECT element
2310: * @param array $options Options & attributes for the select elements.
2311: * @return string Completed year select input
2312: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-year-inputs
2313: */
2314: public function year($fieldName, array $options = [])
2315: {
2316: $options = $this->_singleDatetime($options, 'year');
2317:
2318: $len = isset($options['val']) ? strlen($options['val']) : 0;
2319: if (isset($options['val']) && $len > 0 && $len < 5) {
2320: $options['val'] = [
2321: 'year' => (int)$options['val'],
2322: 'month' => date('m'),
2323: 'day' => date('d')
2324: ];
2325: }
2326:
2327: return $this->dateTime($fieldName, $options);
2328: }
2329:
2330: /**
2331: * Returns a SELECT element for months.
2332: *
2333: * ### Options:
2334: *
2335: * - `monthNames` - If false, 2 digit numbers will be used instead of text.
2336: * If an array, the given array will be used.
2337: * - `empty` - If true, the empty select option is shown. If a string,
2338: * that string is displayed as the empty element.
2339: * - `value` The selected value of the input.
2340: *
2341: * @param string $fieldName Prefix name for the SELECT element
2342: * @param array $options Attributes for the select element
2343: * @return string A generated month select dropdown.
2344: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-month-inputs
2345: */
2346: public function month($fieldName, array $options = [])
2347: {
2348: $options = $this->_singleDatetime($options, 'month');
2349:
2350: if (isset($options['val']) && $options['val'] > 0 && $options['val'] <= 12) {
2351: $options['val'] = [
2352: 'year' => date('Y'),
2353: 'month' => (int)$options['val'],
2354: 'day' => date('d')
2355: ];
2356: }
2357:
2358: return $this->dateTime($fieldName, $options);
2359: }
2360:
2361: /**
2362: * Returns a SELECT element for hours.
2363: *
2364: * ### Attributes:
2365: *
2366: * - `empty` - If true, the empty select option is shown. If a string,
2367: * that string is displayed as the empty element.
2368: * - `value` The selected value of the input.
2369: * - `format` Set to 12 or 24 to use 12 or 24 hour formatting. Defaults to 24.
2370: *
2371: * @param string $fieldName Prefix name for the SELECT element
2372: * @param array $options List of HTML attributes
2373: * @return string Completed hour select input
2374: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-hour-inputs
2375: */
2376: public function hour($fieldName, array $options = [])
2377: {
2378: $options += ['format' => 24];
2379: $options = $this->_singleDatetime($options, 'hour');
2380:
2381: $options['timeFormat'] = $options['format'];
2382: unset($options['format']);
2383:
2384: if (isset($options['val']) && $options['val'] > 0 && $options['val'] <= 24) {
2385: $options['val'] = [
2386: 'hour' => (int)$options['val'],
2387: 'minute' => date('i'),
2388: ];
2389: }
2390:
2391: return $this->dateTime($fieldName, $options);
2392: }
2393:
2394: /**
2395: * Returns a SELECT element for minutes.
2396: *
2397: * ### Attributes:
2398: *
2399: * - `empty` - If true, the empty select option is shown. If a string,
2400: * that string is displayed as the empty element.
2401: * - `value` The selected value of the input.
2402: * - `interval` The interval that minute options should be created at.
2403: * - `round` How you want the value rounded when it does not fit neatly into an
2404: * interval. Accepts 'up', 'down', and null.
2405: *
2406: * @param string $fieldName Prefix name for the SELECT element
2407: * @param array $options Array of options.
2408: * @return string Completed minute select input.
2409: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-minute-inputs
2410: */
2411: public function minute($fieldName, array $options = [])
2412: {
2413: $options = $this->_singleDatetime($options, 'minute');
2414:
2415: if (isset($options['val']) && $options['val'] > 0 && $options['val'] <= 60) {
2416: $options['val'] = [
2417: 'hour' => date('H'),
2418: 'minute' => (int)$options['val'],
2419: ];
2420: }
2421:
2422: return $this->dateTime($fieldName, $options);
2423: }
2424:
2425: /**
2426: * Returns a SELECT element for AM or PM.
2427: *
2428: * ### Attributes:
2429: *
2430: * - `empty` - If true, the empty select option is shown. If a string,
2431: * that string is displayed as the empty element.
2432: * - `value` The selected value of the input.
2433: *
2434: * @param string $fieldName Prefix name for the SELECT element
2435: * @param array $options Array of options
2436: * @return string Completed meridian select input
2437: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-meridian-inputs
2438: */
2439: public function meridian($fieldName, array $options = [])
2440: {
2441: $options = $this->_singleDatetime($options, 'meridian');
2442:
2443: if (isset($options['val'])) {
2444: $hour = date('H');
2445: $options['val'] = [
2446: 'hour' => $hour,
2447: 'minute' => (int)$options['val'],
2448: 'meridian' => $hour > 11 ? 'pm' : 'am',
2449: ];
2450: }
2451:
2452: return $this->dateTime($fieldName, $options);
2453: }
2454:
2455: /**
2456: * Returns a set of SELECT elements for a full datetime setup: day, month and year, and then time.
2457: *
2458: * ### Date Options:
2459: *
2460: * - `empty` - If true, the empty select option is shown. If a string,
2461: * that string is displayed as the empty element.
2462: * - `value` | `default` The default value to be used by the input. A value in `$this->data`
2463: * matching the field name will override this value. If no default is provided `time()` will be used.
2464: * - `monthNames` If false, 2 digit numbers will be used instead of text.
2465: * If an array, the given array will be used.
2466: * - `minYear` The lowest year to use in the year select
2467: * - `maxYear` The maximum year to use in the year select
2468: * - `orderYear` - Order of year values in select options.
2469: * Possible values 'asc', 'desc'. Default 'desc'.
2470: *
2471: * ### Time options:
2472: *
2473: * - `empty` - If true, the empty select option is shown. If a string,
2474: * - `value` | `default` The default value to be used by the input. A value in `$this->data`
2475: * matching the field name will override this value. If no default is provided `time()` will be used.
2476: * - `timeFormat` The time format to use, either 12 or 24.
2477: * - `interval` The interval for the minutes select. Defaults to 1
2478: * - `round` - Set to `up` or `down` if you want to force rounding in either direction. Defaults to null.
2479: * - `second` Set to true to enable seconds drop down.
2480: *
2481: * To control the order of inputs, and any elements/content between the inputs you
2482: * can override the `dateWidget` template. By default the `dateWidget` template is:
2483: *
2484: * `{{month}}{{day}}{{year}}{{hour}}{{minute}}{{second}}{{meridian}}`
2485: *
2486: * @param string $fieldName Prefix name for the SELECT element
2487: * @param array $options Array of Options
2488: * @return string Generated set of select boxes for the date and time formats chosen.
2489: * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-date-and-time-inputs
2490: */
2491: public function dateTime($fieldName, array $options = [])
2492: {
2493: $options += [
2494: 'empty' => true,
2495: 'value' => null,
2496: 'interval' => 1,
2497: 'round' => null,
2498: 'monthNames' => true,
2499: 'minYear' => null,
2500: 'maxYear' => null,
2501: 'orderYear' => 'desc',
2502: 'timeFormat' => 24,
2503: 'second' => false,
2504: ];
2505: $options = $this->_initInputField($fieldName, $options);
2506: $options = $this->_datetimeOptions($options);
2507:
2508: return $this->widget('datetime', $options);
2509: }
2510:
2511: /**
2512: * Helper method for converting from FormHelper options data to widget format.
2513: *
2514: * @param array $options Options to convert.
2515: * @return array Converted options.
2516: */
2517: protected function _datetimeOptions($options)
2518: {
2519: foreach ($this->_datetimeParts as $type) {
2520: if (!array_key_exists($type, $options)) {
2521: $options[$type] = [];
2522: }
2523: if ($options[$type] === true) {
2524: $options[$type] = [];
2525: }
2526:
2527: // Pass boolean/scalar empty options to each type.
2528: if (is_array($options[$type]) && isset($options['empty']) && !is_array($options['empty'])) {
2529: $options[$type]['empty'] = $options['empty'];
2530: }
2531:
2532: // Move empty options into each type array.
2533: if (isset($options['empty'][$type])) {
2534: $options[$type]['empty'] = $options['empty'][$type];
2535: }
2536: if (isset($options['required']) && is_array($options[$type])) {
2537: $options[$type]['required'] = $options['required'];
2538: }
2539: }
2540:
2541: $hasYear = is_array($options['year']);
2542: if ($hasYear && isset($options['minYear'])) {
2543: $options['year']['start'] = $options['minYear'];
2544: }
2545: if ($hasYear && isset($options['maxYear'])) {
2546: $options['year']['end'] = $options['maxYear'];
2547: }
2548: if ($hasYear && isset($options['orderYear'])) {
2549: $options['year']['order'] = $options['orderYear'];
2550: }
2551: unset($options['minYear'], $options['maxYear'], $options['orderYear']);
2552:
2553: if (is_array($options['month'])) {
2554: $options['month']['names'] = $options['monthNames'];
2555: }
2556: unset($options['monthNames']);
2557:
2558: if (is_array($options['hour']) && isset($options['timeFormat'])) {
2559: $options['hour']['format'] = $options['timeFormat'];
2560: }
2561: unset($options['timeFormat']);
2562:
2563: if (is_array($options['minute'])) {
2564: $options['minute']['interval'] = $options['interval'];
2565: $options['minute']['round'] = $options['round'];
2566: }
2567: unset($options['interval'], $options['round']);
2568:
2569: if ($options['val'] === true || $options['val'] === null && isset($options['empty']) && $options['empty'] === false) {
2570: $val = new DateTime();
2571: $currentYear = $val->format('Y');
2572: if (isset($options['year']['end']) && $options['year']['end'] < $currentYear) {
2573: $val->setDate($options['year']['end'], $val->format('n'), $val->format('j'));
2574: }
2575: $options['val'] = $val;
2576: }
2577:
2578: unset($options['empty']);
2579:
2580: return $options;
2581: }
2582:
2583: /**
2584: * Generate time inputs.
2585: *
2586: * ### Options:
2587: *
2588: * See dateTime() for time options.
2589: *
2590: * @param string $fieldName Prefix name for the SELECT element
2591: * @param array $options Array of Options
2592: * @return string Generated set of select boxes for time formats chosen.
2593: * @see \Cake\View\Helper\FormHelper::dateTime() for templating options.
2594: */
2595: public function time($fieldName, array $options = [])
2596: {
2597: $options += [
2598: 'empty' => true,
2599: 'value' => null,
2600: 'interval' => 1,
2601: 'round' => null,
2602: 'timeFormat' => 24,
2603: 'second' => false,
2604: ];
2605: $options['year'] = $options['month'] = $options['day'] = false;
2606: $options = $this->_initInputField($fieldName, $options);
2607: $options = $this->_datetimeOptions($options);
2608:
2609: return $this->widget('datetime', $options);
2610: }
2611:
2612: /**
2613: * Generate date inputs.
2614: *
2615: * ### Options:
2616: *
2617: * See dateTime() for date options.
2618: *
2619: * @param string $fieldName Prefix name for the SELECT element
2620: * @param array $options Array of Options
2621: * @return string Generated set of select boxes for time formats chosen.
2622: * @see \Cake\View\Helper\FormHelper::dateTime() for templating options.
2623: */
2624: public function date($fieldName, array $options = [])
2625: {
2626: $options += [
2627: 'empty' => true,
2628: 'value' => null,
2629: 'monthNames' => true,
2630: 'minYear' => null,
2631: 'maxYear' => null,
2632: 'orderYear' => 'desc',
2633: ];
2634: $options['hour'] = $options['minute'] = false;
2635: $options['meridian'] = $options['second'] = false;
2636:
2637: $options = $this->_initInputField($fieldName, $options);
2638: $options = $this->_datetimeOptions($options);
2639:
2640: return $this->widget('datetime', $options);
2641: }
2642:
2643: /**
2644: * Sets field defaults and adds field to form security input hash.
2645: * Will also add the error class if the field contains validation errors.
2646: *
2647: * ### Options
2648: *
2649: * - `secure` - boolean whether or not the field should be added to the security fields.
2650: * Disabling the field using the `disabled` option, will also omit the field from being
2651: * part of the hashed key.
2652: * - `default` - mixed - The value to use if there is no value in the form's context.
2653: * - `disabled` - mixed - Either a boolean indicating disabled state, or the string in
2654: * a numerically indexed value.
2655: * - `id` - mixed - If `true` it will be auto generated based on field name.
2656: *
2657: * This method will convert a numerically indexed 'disabled' into an associative
2658: * array value. FormHelper's internals expect associative options.
2659: *
2660: * The output of this function is a more complete set of input attributes that
2661: * can be passed to a form widget to generate the actual input.
2662: *
2663: * @param string $field Name of the field to initialize options for.
2664: * @param array $options Array of options to append options into.
2665: * @return array Array of options for the input.
2666: */
2667: protected function _initInputField($field, $options = [])
2668: {
2669: if (!isset($options['secure'])) {
2670: $options['secure'] = (bool)$this->_View->getRequest()->getParam('_Token');
2671: }
2672: $context = $this->_getContext();
2673:
2674: if (isset($options['id']) && $options['id'] === true) {
2675: $options['id'] = $this->_domId($field);
2676: }
2677:
2678: $disabledIndex = array_search('disabled', $options, true);
2679: if (is_int($disabledIndex)) {
2680: unset($options[$disabledIndex]);
2681: $options['disabled'] = true;
2682: }
2683:
2684: if (!isset($options['name'])) {
2685: $endsWithBrackets = '';
2686: if (substr($field, -2) === '[]') {
2687: $field = substr($field, 0, -2);
2688: $endsWithBrackets = '[]';
2689: }
2690: $parts = explode('.', $field);
2691: $first = array_shift($parts);
2692: $options['name'] = $first . (!empty($parts) ? '[' . implode('][', $parts) . ']' : '') . $endsWithBrackets;
2693: }
2694:
2695: if (isset($options['value']) && !isset($options['val'])) {
2696: $options['val'] = $options['value'];
2697: unset($options['value']);
2698: }
2699: if (!isset($options['val'])) {
2700: $valOptions = [
2701: 'default' => isset($options['default']) ? $options['default'] : null,
2702: 'schemaDefault' => isset($options['schemaDefault']) ? $options['schemaDefault'] : true,
2703: ];
2704: $options['val'] = $this->getSourceValue($field, $valOptions);
2705: }
2706: if (!isset($options['val']) && isset($options['default'])) {
2707: $options['val'] = $options['default'];
2708: }
2709: unset($options['value'], $options['default']);
2710:
2711: if ($context->hasError($field)) {
2712: $options = $this->addClass($options, $this->_config['errorClass']);
2713: }
2714: $isDisabled = $this->_isDisabled($options);
2715: if ($isDisabled) {
2716: $options['secure'] = self::SECURE_SKIP;
2717: }
2718: if ($options['secure'] === self::SECURE_SKIP) {
2719: return $options;
2720: }
2721: if (!isset($options['required']) && empty($options['disabled']) && $context->isRequired($field)) {
2722: $options['required'] = true;
2723: }
2724:
2725: return $options;
2726: }
2727:
2728: /**
2729: * Determine if a field is disabled.
2730: *
2731: * @param array $options The option set.
2732: * @return bool Whether or not the field is disabled.
2733: */
2734: protected function _isDisabled(array $options)
2735: {
2736: if (!isset($options['disabled'])) {
2737: return false;
2738: }
2739: if (is_scalar($options['disabled'])) {
2740: return ($options['disabled'] === true || $options['disabled'] === 'disabled');
2741: }
2742: if (!isset($options['options'])) {
2743: return false;
2744: }
2745: if (is_array($options['options'])) {
2746: // Simple list options
2747: $first = $options['options'][array_keys($options['options'])[0]];
2748: if (is_scalar($first)) {
2749: return array_diff($options['options'], $options['disabled']) === [];
2750: }
2751: // Complex option types
2752: if (is_array($first)) {
2753: $disabled = array_filter($options['options'], function ($i) use ($options) {
2754: return in_array($i['value'], $options['disabled']);
2755: });
2756:
2757: return count($disabled) > 0;
2758: }
2759: }
2760:
2761: return false;
2762: }
2763:
2764: /**
2765: * Get the field name for use with _secure().
2766: *
2767: * Parses the name attribute to create a dot separated name value for use
2768: * in secured field hash. If filename is of form Model[field] an array of
2769: * fieldname parts like ['Model', 'field'] is returned.
2770: *
2771: * @param string $name The form inputs name attribute.
2772: * @return array Array of field name params like ['Model.field'] or
2773: * ['Model', 'field'] for array fields or empty array if $name is empty.
2774: */
2775: protected function _secureFieldName($name)
2776: {
2777: if (empty($name) && $name !== '0') {
2778: return [];
2779: }
2780:
2781: if (strpos($name, '[') === false) {
2782: return [$name];
2783: }
2784: $parts = explode('[', $name);
2785: $parts = array_map(function ($el) {
2786: return trim($el, ']');
2787: }, $parts);
2788:
2789: return array_filter($parts, 'strlen');
2790: }
2791:
2792: /**
2793: * Add a new context type.
2794: *
2795: * Form context types allow FormHelper to interact with
2796: * data providers that come from outside CakePHP. For example
2797: * if you wanted to use an alternative ORM like Doctrine you could
2798: * create and connect a new context class to allow FormHelper to
2799: * read metadata from doctrine.
2800: *
2801: * @param string $type The type of context. This key
2802: * can be used to overwrite existing providers.
2803: * @param callable $check A callable that returns an object
2804: * when the form context is the correct type.
2805: * @return void
2806: */
2807: public function addContextProvider($type, callable $check)
2808: {
2809: $this->contextFactory()->addProvider($type, $check);
2810: }
2811:
2812: /**
2813: * Get the context instance for the current form set.
2814: *
2815: * If there is no active form null will be returned.
2816: *
2817: * @param \Cake\View\Form\ContextInterface|null $context Either the new context when setting, or null to get.
2818: * @return \Cake\View\Form\ContextInterface The context for the form.
2819: */
2820: public function context($context = null)
2821: {
2822: if ($context instanceof ContextInterface) {
2823: $this->_context = $context;
2824: }
2825:
2826: return $this->_getContext();
2827: }
2828:
2829: /**
2830: * Find the matching context provider for the data.
2831: *
2832: * If no type can be matched a NullContext will be returned.
2833: *
2834: * @param mixed $data The data to get a context provider for.
2835: * @return \Cake\View\Form\ContextInterface Context provider.
2836: * @throws \RuntimeException when the context class does not implement the
2837: * ContextInterface.
2838: */
2839: protected function _getContext($data = [])
2840: {
2841: if (isset($this->_context) && empty($data)) {
2842: return $this->_context;
2843: }
2844: $data += ['entity' => null];
2845:
2846: return $this->_context = $this->contextFactory()
2847: ->get($this->_View->getRequest(), $data);
2848: }
2849:
2850: /**
2851: * Add a new widget to FormHelper.
2852: *
2853: * Allows you to add or replace widget instances with custom code.
2854: *
2855: * @param string $name The name of the widget. e.g. 'text'.
2856: * @param array|\Cake\View\Widget\WidgetInterface $spec Either a string class
2857: * name or an object implementing the WidgetInterface.
2858: * @return void
2859: */
2860: public function addWidget($name, $spec)
2861: {
2862: $this->_locator->add([$name => $spec]);
2863: }
2864:
2865: /**
2866: * Render a named widget.
2867: *
2868: * This is a lower level method. For built-in widgets, you should be using
2869: * methods like `text`, `hidden`, and `radio`. If you are using additional
2870: * widgets you should use this method render the widget without the label
2871: * or wrapping div.
2872: *
2873: * @param string $name The name of the widget. e.g. 'text'.
2874: * @param array $data The data to render.
2875: * @return string
2876: */
2877: public function widget($name, array $data = [])
2878: {
2879: $secure = null;
2880: if (isset($data['secure'])) {
2881: $secure = $data['secure'];
2882: unset($data['secure']);
2883: }
2884: $widget = $this->_locator->get($name);
2885: $out = $widget->render($data, $this->context());
2886: if (isset($data['name']) && $secure !== null && $secure !== self::SECURE_SKIP) {
2887: foreach ($widget->secureFields($data) as $field) {
2888: $this->_secure($secure, $this->_secureFieldName($field));
2889: }
2890: }
2891:
2892: return $out;
2893: }
2894:
2895: /**
2896: * Restores the default values built into FormHelper.
2897: *
2898: * This method will not reset any templates set in custom widgets.
2899: *
2900: * @return void
2901: */
2902: public function resetTemplates()
2903: {
2904: $this->setTemplates($this->_defaultConfig['templates']);
2905: }
2906:
2907: /**
2908: * Event listeners.
2909: *
2910: * @return array
2911: */
2912: public function implementedEvents()
2913: {
2914: return [];
2915: }
2916:
2917: /**
2918: * Gets the value sources.
2919: *
2920: * Returns a list, but at least one item, of valid sources, such as: `'context'`, `'data'` and `'query'`.
2921: *
2922: * @return string[] List of value sources.
2923: */
2924: public function getValueSources()
2925: {
2926: return $this->_valueSources;
2927: }
2928:
2929: /**
2930: * Sets the value sources.
2931: *
2932: * Valid values are `'context'`, `'data'` and `'query'`.
2933: * You need to supply one valid context or multiple, as a list of strings. Order sets priority.
2934: *
2935: * @param string|string[] $sources A string or a list of strings identifying a source.
2936: * @return $this
2937: */
2938: public function setValueSources($sources)
2939: {
2940: $this->_valueSources = array_values(array_intersect((array)$sources, ['context', 'data', 'query']));
2941:
2942: return $this;
2943: }
2944:
2945: /**
2946: * Gets a single field value from the sources available.
2947: *
2948: * @param string $fieldname The fieldname to fetch the value for.
2949: * @param array|null $options The options containing default values.
2950: * @return string|null Field value derived from sources or defaults.
2951: */
2952: public function getSourceValue($fieldname, $options = [])
2953: {
2954: $valueMap = [
2955: 'data' => 'getData',
2956: 'query' => 'getQuery'
2957: ];
2958: foreach ($this->getValueSources() as $valuesSource) {
2959: if ($valuesSource === 'context') {
2960: $val = $this->_getContext()->val($fieldname, $options);
2961: if ($val !== null) {
2962: return $val;
2963: }
2964: }
2965: if (isset($valueMap[$valuesSource])) {
2966: $method = $valueMap[$valuesSource];
2967: $value = $this->_View->getRequest()->{$method}($fieldname);
2968: if ($value !== null) {
2969: return $value;
2970: }
2971: }
2972: }
2973:
2974: return null;
2975: }
2976: }
2977: