Completed
Pull Request — master (#13352)
by
unknown
03:52
created

FormHelper::addContextProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
 *
44
 * @property \Cake\View\Helper\HtmlHelper $Html
45
 * @property \Cake\View\Helper\UrlHelper $Url
46
 * @link https://book.cakephp.org/3.0/en/views/helpers/form.html
47
 */
48
class FormHelper extends Helper
49
{
50
51
    use IdGeneratorTrait;
52
    use SecureFieldTokenTrait;
53
    use StringTemplateTrait;
54
55
    /**
56
     * Other helpers used by FormHelper
57
     *
58
     * @var array
59
     */
60
    public $helpers = ['Url', 'Html'];
61
62
    /**
63
     * The various pickers that make up a datetime picker.
64
     *
65
     * @var array
66
     */
67
    protected $_datetimeParts = ['year', 'month', 'day', 'hour', 'minute', 'second', 'meridian'];
68
69
    /**
70
     * Special options used for datetime inputs.
71
     *
72
     * @var array
73
     */
74
    protected $_datetimeOptions = [
75
        'interval', 'round', 'monthNames', 'minYear', 'maxYear',
76
        'orderYear', 'timeFormat', 'second'
77
    ];
78
79
    /**
80
     * Default config for the helper.
81
     *
82
     * @var array
83
     */
84
    protected $_defaultConfig = [
85
        'idPrefix' => null,
86
        'errorClass' => 'form-error',
87
        'typeMap' => [
88
            'string' => 'text',
89
            'text' => 'textarea',
90
            'uuid' => 'string',
91
            'datetime' => 'datetime',
92
            'timestamp' => 'datetime',
93
            'date' => 'date',
94
            'time' => 'time',
95
            'boolean' => 'checkbox',
96
            'float' => 'number',
97
            'integer' => 'number',
98
            'tinyinteger' => 'number',
99
            'smallinteger' => 'number',
100
            'decimal' => 'number',
101
            'binary' => 'file',
102
        ],
103
        'templates' => [
104
            // Used for button elements in button().
105
            'button' => '<button{{attrs}}>{{text}}</button>',
106
            // Used for checkboxes in checkbox() and multiCheckbox().
107
            'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
108
            // Input group wrapper for checkboxes created via control().
109
            'checkboxFormGroup' => '{{label}}',
110
            // Wrapper container for checkboxes.
111
            'checkboxWrapper' => '<div class="checkbox">{{label}}</div>',
112
            // Widget ordering for date/time/datetime pickers.
113
            'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}',
114
            // Error message wrapper elements.
115
            'error' => '<div class="error-message">{{content}}</div>',
116
            // Container for error items.
117
            'errorList' => '<ul>{{content}}</ul>',
118
            // Error item wrapper.
119
            'errorItem' => '<li>{{text}}</li>',
120
            // File input used by file().
121
            'file' => '<input type="file" name="{{name}}"{{attrs}}>',
122
            // Fieldset element used by allControls().
123
            'fieldset' => '<fieldset{{attrs}}>{{content}}</fieldset>',
124
            // Open tag used by create().
125
            'formStart' => '<form{{attrs}}>',
126
            // Close tag used by end().
127
            'formEnd' => '</form>',
128
            // General grouping container for control(). Defines input/label ordering.
129
            'formGroup' => '{{label}}{{input}}',
130
            // Wrapper content used to hide other content.
131
            'hiddenBlock' => '<div style="display:none;">{{content}}</div>',
132
            // Generic input element.
133
            'input' => '<input type="{{type}}" name="{{name}}"{{attrs}}/>',
134
            // Submit input element.
135
            'inputSubmit' => '<input type="{{type}}"{{attrs}}/>',
136
            // Container element used by control().
137
            'inputContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
138
            // Container element used by control() when a field has an error.
139
            'inputContainerError' => '<div class="input {{type}}{{required}} error">{{content}}{{error}}</div>',
140
            // Label element when inputs are not nested inside the label.
141
            'label' => '<label{{attrs}}>{{text}}</label>',
142
            // Label element used for radio and multi-checkbox inputs.
143
            'nestingLabel' => '{{hidden}}<label{{attrs}}>{{input}}{{text}}</label>',
144
            // Legends created by allControls()
145
            'legend' => '<legend>{{text}}</legend>',
146
            // Multi-Checkbox input set title element.
147
            'multicheckboxTitle' => '<legend>{{text}}</legend>',
148
            // Multi-Checkbox wrapping container.
149
            'multicheckboxWrapper' => '<fieldset{{attrs}}>{{content}}</fieldset>',
150
            // Option element used in select pickers.
151
            'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
152
            // Option group element used in select pickers.
153
            'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
154
            // Select element,
155
            'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
156
            // Multi-select element,
157
            'selectMultiple' => '<select name="{{name}}[]" multiple="multiple"{{attrs}}>{{content}}</select>',
158
            // Radio input element,
159
            'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
160
            // Wrapping container for radio input/label,
161
            'radioWrapper' => '{{label}}',
162
            // Textarea input element,
163
            'textarea' => '<textarea name="{{name}}"{{attrs}}>{{value}}</textarea>',
164
            // Container for submit buttons.
165
            'submitContainer' => '<div class="submit">{{content}}</div>',
166
            //Confirm javascript template for postLink()
167
            'confirmJs' => '{{confirm}}',
168
        ],
169
        // set HTML5 validation message to custom required/empty messages
170
        'autoSetCustomValidity' => false,
171
    ];
172
173
    /**
174
     * Default widgets
175
     *
176
     * @var array
177
     */
178
    protected $_defaultWidgets = [
179
        'button' => ['Button'],
180
        'checkbox' => ['Checkbox'],
181
        'file' => ['File'],
182
        'label' => ['Label'],
183
        'nestingLabel' => ['NestingLabel'],
184
        'multicheckbox' => ['MultiCheckbox', 'nestingLabel'],
185
        'radio' => ['Radio', 'nestingLabel'],
186
        'select' => ['SelectBox'],
187
        'textarea' => ['Textarea'],
188
        'datetime' => ['DateTime', 'select'],
189
        '_default' => ['Basic'],
190
    ];
191
192
    /**
193
     * List of fields created, used with secure forms.
194
     *
195
     * @var array
196
     */
197
    public $fields = [];
198
199
    /**
200
     * Constant used internally to skip the securing process,
201
     * and neither add the field to the hash or to the unlocked fields.
202
     *
203
     * @var string
204
     */
205
    const SECURE_SKIP = 'skip';
206
207
    /**
208
     * Defines the type of form being created. Set by FormHelper::create().
209
     *
210
     * @var string|null
211
     */
212
    public $requestType;
213
214
    /**
215
     * An array of field names that have been excluded from
216
     * the Token hash used by SecurityComponent's validatePost method
217
     *
218
     * @see \Cake\View\Helper\FormHelper::_secure()
219
     * @see \Cake\Controller\Component\SecurityComponent::validatePost()
220
     * @var array
221
     */
222
    protected $_unlockedFields = [];
223
224
    /**
225
     * Locator for input widgets.
226
     *
227
     * @var \Cake\View\Widget\WidgetLocator
228
     */
229
    protected $_locator;
230
231
    /**
232
     * Context for the current form.
233
     *
234
     * @var \Cake\View\Form\ContextInterface|null
235
     */
236
    protected $_context;
237
238
    /**
239
     * Context factory.
240
     *
241
     * @var \Cake\View\Form\ContextFactory
242
     */
243
    protected $_contextFactory;
244
245
    /**
246
     * The action attribute value of the last created form.
247
     * Used to make form/request specific hashes for SecurityComponent.
248
     *
249
     * @var string
250
     */
251
    protected $_lastAction = '';
252
253
    /**
254
     * The sources to be used when retrieving prefilled input values.
255
     *
256
     * @var array
257
     */
258
    protected $_valueSources = ['context'];
259
260
    /**
261
     * Grouped input types.
262
     *
263
     * @var array
264
     */
265
    protected $_groupedInputTypes = ['radio', 'multicheckbox', 'date', 'time', 'datetime'];
266
267
    /**
268
     * Construct the widgets and binds the default context providers
269
     *
270
     * @param \Cake\View\View $View The View this helper is being attached to.
271
     * @param array $config Configuration settings for the helper.
272
     */
273
    public function __construct(View $View, array $config = [])
274
    {
275
        $locator = null;
276
        $widgets = $this->_defaultWidgets;
277
        if (isset($config['registry'])) {
278
            deprecationWarning('`registry` config key is deprecated in FormHelper, use `locator` instead.');
279
            $config['locator'] = $config['registry'];
280
            unset($config['registry']);
281
        }
282
        if (isset($config['locator'])) {
283
            $locator = $config['locator'];
284
            unset($config['locator']);
285
        }
286
        if (isset($config['widgets'])) {
287
            if (is_string($config['widgets'])) {
288
                $config['widgets'] = (array)$config['widgets'];
289
            }
290
            $widgets = $config['widgets'] + $widgets;
291
            unset($config['widgets']);
292
        }
293
294
        if (isset($config['groupedInputTypes'])) {
295
            $this->_groupedInputTypes = $config['groupedInputTypes'];
296
            unset($config['groupedInputTypes']);
297
        }
298
299
        parent::__construct($View, $config);
300
301
        if (!$locator) {
302
            $locator = new WidgetLocator($this->templater(), $this->_View, $widgets);
303
        }
304
        $this->setWidgetLocator($locator);
305
        $this->_idPrefix = $this->getConfig('idPrefix');
306
    }
307
308
    /**
309
     * Set the widget registry the helper will use.
310
     *
311
     * @param \Cake\View\Widget\WidgetRegistry|null $instance The registry instance to set.
312
     * @param array $widgets An array of widgets
313
     * @return \Cake\View\Widget\WidgetRegistry
314
     * @deprecated 3.6.0 Use FormHelper::widgetLocator() instead.
315
     */
316
    public function widgetRegistry(WidgetRegistry $instance = null, $widgets = [])
317
    {
318
        deprecationWarning('widgetRegistry is deprecated, use widgetLocator instead.');
319
320
        if ($instance) {
321
            $instance->add($widgets);
322
            $this->setWidgetLocator($instance);
323
        }
324
325
        return $this->getWidgetLocator();
326
    }
327
328
    /**
329
     * Get the widget locator currently used by the helper.
330
     *
331
     * @return \Cake\View\Widget\WidgetLocator Current locator instance
332
     * @since 3.6.0
333
     */
334
    public function getWidgetLocator()
335
    {
336
        return $this->_locator;
337
    }
338
339
    /**
340
     * Set the widget locator the helper will use.
341
     *
342
     * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set.
343
     * @return $this
344
     * @since 3.6.0
345
     */
346
    public function setWidgetLocator(WidgetLocator $instance)
347
    {
348
        $this->_locator = $instance;
349
350
        return $this;
351
    }
352
353
    /**
354
     * Set the context factory the helper will use.
355
     *
356
     * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set.
357
     * @param array $contexts An array of context providers.
358
     * @return \Cake\View\Form\ContextFactory
359
     */
360
    public function contextFactory(ContextFactory $instance = null, array $contexts = [])
361
    {
362
        if ($instance === null) {
363
            if ($this->_contextFactory === null) {
364
                $this->_contextFactory = ContextFactory::createWithDefaults($contexts);
365
            }
366
367
            return $this->_contextFactory;
368
        }
369
        $this->_contextFactory = $instance;
370
371
        return $this->_contextFactory;
372
    }
373
374
    /**
375
     * Returns an HTML form element.
376
     *
377
     * ### Options:
378
     *
379
     * - `type` Form method defaults to autodetecting based on the form context. If
380
     *   the form context's isCreate() method returns false, a PUT request will be done.
381
     * - `method` Set the form's method attribute explicitly.
382
     * - `action` The controller action the form submits to, (optional). Use this option if you
383
     *   don't need to change the controller from the current request's controller. Deprecated since 3.2, use `url`.
384
     * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url'
385
     *    you should leave 'action' undefined.
386
     * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')`
387
     * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype`
388
     *   to `multipart/form-data`.
389
     * - `templates` The templates you want to use for this form. Any templates will be merged on top of
390
     *   the already loaded templates. This option can either be a filename in /config that contains
391
     *   the templates you want to load, or an array of templates to use.
392
     * - `context` Additional options for the context class. For example the EntityContext accepts a 'table'
393
     *   option that allows you to set the specific Table class the form should be based on.
394
     * - `idPrefix` Prefix for generated ID attributes.
395
     * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources()
396
     * - `templateVars` Provide template variables for the formStart template.
397
     *
398
     * @param mixed $context The context for which the form is being defined.
399
     *   Can be a ContextInterface instance, ORM entity, ORM resultset, or an
400
     *   array of meta data. You can use false or null to make a context-less form.
401
     * @param array $options An array of html attributes and options.
402
     * @return string An formatted opening FORM tag.
403
     * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create
404
     */
405
    public function create($context = null, array $options = [])
406
    {
407
        $append = '';
408
409
        if ($context instanceof ContextInterface) {
410
            $this->context($context);
411
        } else {
412
            if (empty($options['context'])) {
413
                $options['context'] = [];
414
            }
415
            $options['context']['entity'] = $context;
416
            $context = $this->_getContext($options['context']);
417
            unset($options['context']);
418
        }
419
420
        $isCreate = $context->isCreate();
421
422
        $options += [
423
            'type' => $isCreate ? 'post' : 'put',
424
            'action' => null,
425
            'url' => null,
426
            'encoding' => strtolower(Configure::read('App.encoding')),
427
            'templates' => null,
428
            'idPrefix' => null,
429
            'valueSources' => null,
430
        ];
431
432
        if (isset($options['action'])) {
433
            trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED);
434
        }
435
436
        if (isset($options['valueSources'])) {
437
            $this->setValueSources($options['valueSources']);
438
            unset($options['valueSources']);
439
        }
440
441
        if ($options['idPrefix'] !== null) {
442
            $this->_idPrefix = $options['idPrefix'];
443
        }
444
        $templater = $this->templater();
445
446 View Code Duplication
        if (!empty($options['templates'])) {
447
            $templater->push();
448
            $method = is_string($options['templates']) ? 'load' : 'add';
449
            $templater->{$method}($options['templates']);
450
        }
451
        unset($options['templates']);
452
453
        if ($options['action'] === false || $options['url'] === false) {
454
            $url = $this->_View->getRequest()->getRequestTarget();
455
            $action = null;
456
        } else {
457
            $url = $this->_formUrl($context, $options);
458
            $action = $this->Url->build($url);
459
        }
460
461
        $this->_lastAction($url);
462
        unset($options['url'], $options['action'], $options['idPrefix']);
463
464
        $htmlAttributes = [];
465
        switch (strtolower($options['type'])) {
466
            case 'get':
467
                $htmlAttributes['method'] = 'get';
468
                break;
469
            // Set enctype for form
470
            case 'file':
471
                $htmlAttributes['enctype'] = 'multipart/form-data';
472
                $options['type'] = $isCreate ? 'post' : 'put';
473
            // Move on
474
            case 'post':
475
            // Move on
476
            case 'put':
477
            // Move on
478
            case 'delete':
479
            // Set patch method
480
            case 'patch':
481
                $append .= $this->hidden('_method', [
482
                    'name' => '_method',
483
                    'value' => strtoupper($options['type']),
484
                    'secure' => static::SECURE_SKIP
485
                ]);
486
            // Default to post method
487
            default:
488
                $htmlAttributes['method'] = 'post';
489
        }
490
        if (isset($options['method'])) {
491
            $htmlAttributes['method'] = strtolower($options['method']);
492
        }
493
        if (isset($options['enctype'])) {
494
            $htmlAttributes['enctype'] = strtolower($options['enctype']);
495
        }
496
497
        $this->requestType = strtolower($options['type']);
498
499
        if (!empty($options['encoding'])) {
500
            $htmlAttributes['accept-charset'] = $options['encoding'];
501
        }
502
        unset($options['type'], $options['encoding']);
503
504
        $htmlAttributes += $options;
505
506
        $this->fields = [];
507
        if ($this->requestType !== 'get') {
508
            $append .= $this->_csrfField();
509
        }
510
511
        if (!empty($append)) {
512
            $append = $templater->format('hiddenBlock', ['content' => $append]);
513
        }
514
515
        $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]);
516
517
        return $this->formatTemplate('formStart', [
518
            'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr,
519
            'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : []
520
        ]) . $append;
521
    }
522
523
    /**
524
     * Create the URL for a form based on the options.
525
     *
526
     * @param \Cake\View\Form\ContextInterface $context The context object to use.
527
     * @param array $options An array of options from create()
528
     * @return string|array The action attribute for the form.
529
     */
530
    protected function _formUrl($context, $options)
531
    {
532
        $request = $this->_View->getRequest();
533
534
        if ($options['action'] === null && $options['url'] === null) {
535
            return $request->getRequestTarget();
536
        }
537
538
        if (is_string($options['url']) ||
539
            (is_array($options['url']) && isset($options['url']['_name']))
540
        ) {
541
            return $options['url'];
542
        }
543
544 View Code Duplication
        if (isset($options['action']) && empty($options['url']['action'])) {
545
            $options['url']['action'] = $options['action'];
546
        }
547
548
        $actionDefaults = [
549
            'plugin' => $this->_View->getPlugin(),
550
            'controller' => $request->getParam('controller'),
551
            'action' => $request->getParam('action'),
552
        ];
553
554
        $action = (array)$options['url'] + $actionDefaults;
555
556
        $pk = $context->primaryKey();
557
        if (count($pk)) {
558
            $id = $this->getSourceValue($pk[0]);
559
        }
560
        if (empty($action[0]) && isset($id)) {
561
            $action[0] = $id;
562
        }
563
564
        return $action;
565
    }
566
567
    /**
568
     * Correctly store the last created form action URL.
569
     *
570
     * @param string|array $url The URL of the last form.
571
     * @return void
572
     */
573
    protected function _lastAction($url)
574
    {
575
        $action = Router::url($url, true);
576
        $query = parse_url($action, PHP_URL_QUERY);
577
        $query = $query ? '?' . $query : '';
578
        $this->_lastAction = parse_url($action, PHP_URL_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)) {
710
            $this->_unlockedFields[] = $name;
711
        }
712
        $index = array_search($name, $this->fields);
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)) {
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 View Code Duplication
        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 View Code Duplication
            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 View Code Duplication
            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 View Code Duplication
        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 View Code Duplication
            case 'select':
1317
                $opts = $options['options'];
1318
                unset($options['options']);
1319
1320
                return $this->select($fieldName, $opts, $options + ['label' => $label]);
1321 View Code Duplication
            case 'radio':
1322
                $opts = $options['options'];
1323
                unset($options['options']);
1324
1325
                return $this->radio($fieldName, $opts, $options + ['label' => $label]);
1326 View Code Duplication
            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('$message')";
1471
                    $options['onvalid'] = "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 View Code Duplication
    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 View Code Duplication
    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);
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1924 can also be of type null; however, Cake\View\Helper\FormHelper::_lastAction() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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 View Code Duplication
    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 View Code Duplication
    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 empty options to each type.
2528 View Code Duplication
            if (!empty($options['empty']) &&
2529
                is_array($options[$type])
2530
            ) {
2531
                $options[$type]['empty'] = $options['empty'];
2532
            }
2533
2534
            // Move empty options into each type array.
2535 View Code Duplication
            if (isset($options['empty'][$type])) {
2536
                $options[$type]['empty'] = $options['empty'][$type];
2537
            }
2538
            if (isset($options['required']) && is_array($options[$type])) {
2539
                $options[$type]['required'] = $options['required'];
2540
            }
2541
        }
2542
2543
        $hasYear = is_array($options['year']);
2544
        if ($hasYear && isset($options['minYear'])) {
2545
            $options['year']['start'] = $options['minYear'];
2546
        }
2547
        if ($hasYear && isset($options['maxYear'])) {
2548
            $options['year']['end'] = $options['maxYear'];
2549
        }
2550
        if ($hasYear && isset($options['orderYear'])) {
2551
            $options['year']['order'] = $options['orderYear'];
2552
        }
2553
        unset($options['minYear'], $options['maxYear'], $options['orderYear']);
2554
2555
        if (is_array($options['month'])) {
2556
            $options['month']['names'] = $options['monthNames'];
2557
        }
2558
        unset($options['monthNames']);
2559
2560
        if (is_array($options['hour']) && isset($options['timeFormat'])) {
2561
            $options['hour']['format'] = $options['timeFormat'];
2562
        }
2563
        unset($options['timeFormat']);
2564
2565
        if (is_array($options['minute'])) {
2566
            $options['minute']['interval'] = $options['interval'];
2567
            $options['minute']['round'] = $options['round'];
2568
        }
2569
        unset($options['interval'], $options['round']);
2570
2571
        if ($options['val'] === true || $options['val'] === null && isset($options['empty']) && $options['empty'] === false) {
2572
            $val = new DateTime();
2573
            $currentYear = $val->format('Y');
2574
            if (isset($options['year']['end']) && $options['year']['end'] < $currentYear) {
2575
                $val->setDate($options['year']['end'], $val->format('n'), $val->format('j'));
2576
            }
2577
            $options['val'] = $val;
2578
        }
2579
2580
        unset($options['empty']);
2581
2582
        return $options;
2583
    }
2584
2585
    /**
2586
     * Generate time inputs.
2587
     *
2588
     * ### Options:
2589
     *
2590
     * See dateTime() for time options.
2591
     *
2592
     * @param string $fieldName Prefix name for the SELECT element
2593
     * @param array $options Array of Options
2594
     * @return string Generated set of select boxes for time formats chosen.
2595
     * @see \Cake\View\Helper\FormHelper::dateTime() for templating options.
2596
     */
2597
    public function time($fieldName, array $options = [])
2598
    {
2599
        $options += [
2600
            'empty' => true,
2601
            'value' => null,
2602
            'interval' => 1,
2603
            'round' => null,
2604
            'timeFormat' => 24,
2605
            'second' => false,
2606
        ];
2607
        $options['year'] = $options['month'] = $options['day'] = false;
2608
        $options = $this->_initInputField($fieldName, $options);
2609
        $options = $this->_datetimeOptions($options);
2610
2611
        return $this->widget('datetime', $options);
2612
    }
2613
2614
    /**
2615
     * Generate date inputs.
2616
     *
2617
     * ### Options:
2618
     *
2619
     * See dateTime() for date options.
2620
     *
2621
     * @param string $fieldName Prefix name for the SELECT element
2622
     * @param array $options Array of Options
2623
     * @return string Generated set of select boxes for time formats chosen.
2624
     * @see \Cake\View\Helper\FormHelper::dateTime() for templating options.
2625
     */
2626
    public function date($fieldName, array $options = [])
2627
    {
2628
        $options += [
2629
            'empty' => true,
2630
            'value' => null,
2631
            'monthNames' => true,
2632
            'minYear' => null,
2633
            'maxYear' => null,
2634
            'orderYear' => 'desc',
2635
        ];
2636
        $options['hour'] = $options['minute'] = false;
2637
        $options['meridian'] = $options['second'] = false;
2638
2639
        $options = $this->_initInputField($fieldName, $options);
2640
        $options = $this->_datetimeOptions($options);
2641
2642
        return $this->widget('datetime', $options);
2643
    }
2644
2645
    /**
2646
     * Sets field defaults and adds field to form security input hash.
2647
     * Will also add the error class if the field contains validation errors.
2648
     *
2649
     * ### Options
2650
     *
2651
     * - `secure` - boolean whether or not the field should be added to the security fields.
2652
     *   Disabling the field using the `disabled` option, will also omit the field from being
2653
     *   part of the hashed key.
2654
     * - `default` - mixed - The value to use if there is no value in the form's context.
2655
     * - `disabled` - mixed - Either a boolean indicating disabled state, or the string in
2656
     *   a numerically indexed value.
2657
     * - `id` - mixed - If `true` it will be auto generated based on field name.
2658
     *
2659
     * This method will convert a numerically indexed 'disabled' into an associative
2660
     * array value. FormHelper's internals expect associative options.
2661
     *
2662
     * The output of this function is a more complete set of input attributes that
2663
     * can be passed to a form widget to generate the actual input.
2664
     *
2665
     * @param string $field Name of the field to initialize options for.
2666
     * @param array $options Array of options to append options into.
2667
     * @return array Array of options for the input.
2668
     */
2669
    protected function _initInputField($field, $options = [])
2670
    {
2671
        if (!isset($options['secure'])) {
2672
            $options['secure'] = (bool)$this->_View->getRequest()->getParam('_Token');
2673
        }
2674
        $context = $this->_getContext();
2675
2676
        if (isset($options['id']) && $options['id'] === true) {
2677
            $options['id'] = $this->_domId($field);
2678
        }
2679
2680
        $disabledIndex = array_search('disabled', $options, true);
2681
        if (is_int($disabledIndex)) {
2682
            unset($options[$disabledIndex]);
2683
            $options['disabled'] = true;
2684
        }
2685
2686
        if (!isset($options['name'])) {
2687
            $endsWithBrackets = '';
2688
            if (substr($field, -2) === '[]') {
2689
                $field = substr($field, 0, -2);
2690
                $endsWithBrackets = '[]';
2691
            }
2692
            $parts = explode('.', $field);
2693
            $first = array_shift($parts);
2694
            $options['name'] = $first . (!empty($parts) ? '[' . implode('][', $parts) . ']' : '') . $endsWithBrackets;
2695
        }
2696
2697 View Code Duplication
        if (isset($options['value']) && !isset($options['val'])) {
2698
            $options['val'] = $options['value'];
2699
            unset($options['value']);
2700
        }
2701
        if (!isset($options['val'])) {
2702
            $valOptions = [
2703
                'default' => isset($options['default']) ? $options['default'] : null,
2704
                'schemaDefault' => isset($options['schemaDefault']) ? $options['schemaDefault'] : true,
2705
            ];
2706
            $options['val'] = $this->getSourceValue($field, $valOptions);
2707
        }
2708
        if (!isset($options['val']) && isset($options['default'])) {
2709
            $options['val'] = $options['default'];
2710
        }
2711
        unset($options['value'], $options['default']);
2712
2713
        if ($context->hasError($field)) {
2714
            $options = $this->addClass($options, $this->_config['errorClass']);
2715
        }
2716
        $isDisabled = $this->_isDisabled($options);
2717
        if ($isDisabled) {
2718
            $options['secure'] = self::SECURE_SKIP;
2719
        }
2720
        if ($options['secure'] === self::SECURE_SKIP) {
2721
            return $options;
2722
        }
2723
        if (!isset($options['required']) && empty($options['disabled']) && $context->isRequired($field)) {
2724
            $options['required'] = true;
2725
        }
2726
2727
        return $options;
2728
    }
2729
2730
    /**
2731
     * Determine if a field is disabled.
2732
     *
2733
     * @param array $options The option set.
2734
     * @return bool Whether or not the field is disabled.
2735
     */
2736
    protected function _isDisabled(array $options)
2737
    {
2738
        if (!isset($options['disabled'])) {
2739
            return false;
2740
        }
2741
        if (is_scalar($options['disabled'])) {
2742
            return ($options['disabled'] === true || $options['disabled'] === 'disabled');
2743
        }
2744
        if (!isset($options['options'])) {
2745
            return false;
2746
        }
2747
        if (is_array($options['options'])) {
2748
            // Simple list options
2749
            $first = $options['options'][array_keys($options['options'])[0]];
2750
            if (is_scalar($first)) {
2751
                return array_diff($options['options'], $options['disabled']) === [];
2752
            }
2753
            // Complex option types
2754
            if (is_array($first)) {
2755
                $disabled = array_filter($options['options'], function ($i) use ($options) {
2756
                    return in_array($i['value'], $options['disabled']);
2757
                });
2758
2759
                return count($disabled) > 0;
2760
            }
2761
        }
2762
2763
        return false;
2764
    }
2765
2766
    /**
2767
     * Get the field name for use with _secure().
2768
     *
2769
     * Parses the name attribute to create a dot separated name value for use
2770
     * in secured field hash. If filename is of form Model[field] an array of
2771
     * fieldname parts like ['Model', 'field'] is returned.
2772
     *
2773
     * @param string $name The form inputs name attribute.
2774
     * @return array Array of field name params like ['Model.field'] or
2775
     *   ['Model', 'field'] for array fields or empty array if $name is empty.
2776
     */
2777
    protected function _secureFieldName($name)
2778
    {
2779
        if (empty($name) && $name !== '0') {
2780
            return [];
2781
        }
2782
2783
        if (strpos($name, '[') === false) {
2784
            return [$name];
2785
        }
2786
        $parts = explode('[', $name);
2787
        $parts = array_map(function ($el) {
2788
            return trim($el, ']');
2789
        }, $parts);
2790
2791
        return array_filter($parts, 'strlen');
2792
    }
2793
2794
    /**
2795
     * Add a new context type.
2796
     *
2797
     * Form context types allow FormHelper to interact with
2798
     * data providers that come from outside CakePHP. For example
2799
     * if you wanted to use an alternative ORM like Doctrine you could
2800
     * create and connect a new context class to allow FormHelper to
2801
     * read metadata from doctrine.
2802
     *
2803
     * @param string $type The type of context. This key
2804
     *   can be used to overwrite existing providers.
2805
     * @param callable $check A callable that returns an object
2806
     *   when the form context is the correct type.
2807
     * @return void
2808
     */
2809
    public function addContextProvider($type, callable $check)
2810
    {
2811
        $this->contextFactory()->addProvider($type, $check);
2812
    }
2813
2814
    /**
2815
     * Get the context instance for the current form set.
2816
     *
2817
     * If there is no active form null will be returned.
2818
     *
2819
     * @param \Cake\View\Form\ContextInterface|null $context Either the new context when setting, or null to get.
2820
     * @return \Cake\View\Form\ContextInterface The context for the form.
2821
     */
2822
    public function context($context = null)
2823
    {
2824
        if ($context instanceof ContextInterface) {
2825
            $this->_context = $context;
2826
        }
2827
2828
        return $this->_getContext();
2829
    }
2830
2831
    /**
2832
     * Find the matching context provider for the data.
2833
     *
2834
     * If no type can be matched a NullContext will be returned.
2835
     *
2836
     * @param mixed $data The data to get a context provider for.
2837
     * @return \Cake\View\Form\ContextInterface Context provider.
2838
     * @throws \RuntimeException when the context class does not implement the
2839
     *   ContextInterface.
2840
     */
2841
    protected function _getContext($data = [])
2842
    {
2843
        if (isset($this->_context) && empty($data)) {
2844
            return $this->_context;
2845
        }
2846
        $data += ['entity' => null];
2847
2848
        return $this->_context = $this->contextFactory()
2849
            ->get($this->_View->getRequest(), $data);
2850
    }
2851
2852
    /**
2853
     * Add a new widget to FormHelper.
2854
     *
2855
     * Allows you to add or replace widget instances with custom code.
2856
     *
2857
     * @param string $name The name of the widget. e.g. 'text'.
2858
     * @param array|\Cake\View\Widget\WidgetInterface $spec Either a string class
2859
     *   name or an object implementing the WidgetInterface.
2860
     * @return void
2861
     */
2862
    public function addWidget($name, $spec)
2863
    {
2864
        $this->_locator->add([$name => $spec]);
2865
    }
2866
2867
    /**
2868
     * Render a named widget.
2869
     *
2870
     * This is a lower level method. For built-in widgets, you should be using
2871
     * methods like `text`, `hidden`, and `radio`. If you are using additional
2872
     * widgets you should use this method render the widget without the label
2873
     * or wrapping div.
2874
     *
2875
     * @param string $name The name of the widget. e.g. 'text'.
2876
     * @param array $data The data to render.
2877
     * @return string
2878
     */
2879
    public function widget($name, array $data = [])
2880
    {
2881
        $secure = null;
2882
        if (isset($data['secure'])) {
2883
            $secure = $data['secure'];
2884
            unset($data['secure']);
2885
        }
2886
        $widget = $this->_locator->get($name);
2887
        $out = $widget->render($data, $this->context());
2888
        if (isset($data['name']) && $secure !== null && $secure !== self::SECURE_SKIP) {
2889
            foreach ($widget->secureFields($data) as $field) {
2890
                $this->_secure($secure, $this->_secureFieldName($field));
2891
            }
2892
        }
2893
2894
        return $out;
2895
    }
2896
2897
    /**
2898
     * Restores the default values built into FormHelper.
2899
     *
2900
     * This method will not reset any templates set in custom widgets.
2901
     *
2902
     * @return void
2903
     */
2904
    public function resetTemplates()
2905
    {
2906
        $this->setTemplates($this->_defaultConfig['templates']);
2907
    }
2908
2909
    /**
2910
     * Event listeners.
2911
     *
2912
     * @return array
2913
     */
2914
    public function implementedEvents()
2915
    {
2916
        return [];
2917
    }
2918
2919
    /**
2920
     * Gets the value sources.
2921
     *
2922
     * Returns a list, but at least one item, of valid sources, such as: `'context'`, `'data'` and `'query'`.
2923
     *
2924
     * @return array List of value sources.
2925
     */
2926
    public function getValueSources()
2927
    {
2928
        return $this->_valueSources;
2929
    }
2930
2931
    /**
2932
     * Sets the value sources.
2933
     *
2934
     * Valid values are `'context'`, `'data'` and `'query'`.
2935
     * You need to supply one valid context or multiple, as a list of strings. Order sets priority.
2936
     *
2937
     * @param string|array $sources A string or a list of strings identifying a source.
2938
     * @return $this
2939
     */
2940
    public function setValueSources($sources)
2941
    {
2942
        $this->_valueSources = array_values(array_intersect((array)$sources, ['context', 'data', 'query']));
2943
2944
        return $this;
2945
    }
2946
2947
    /**
2948
     * Gets a single field value from the sources available.
2949
     *
2950
     * @param string $fieldname The fieldname to fetch the value for.
2951
     * @param array|null $options The options containing default values.
2952
     * @return string|null Field value derived from sources or defaults.
2953
     */
2954
    public function getSourceValue($fieldname, $options = [])
2955
    {
2956
        $valueMap = [
2957
            'data' => 'getData',
2958
            'query' => 'getQuery'
2959
        ];
2960
        foreach ($this->getValueSources() as $valuesSource) {
2961
            if ($valuesSource === 'context') {
2962
                $val = $this->_getContext()->val($fieldname, $options);
2963
                if ($val !== null) {
2964
                    return $val;
2965
                }
2966
            }
2967
            if (isset($valueMap[$valuesSource])) {
2968
                $method = $valueMap[$valuesSource];
2969
                $value = $this->_View->getRequest()->{$method}($fieldname);
2970
                if ($value !== null) {
2971
                    return $value;
2972
                }
2973
            }
2974
        }
2975
2976
        return null;
2977
    }
2978
}
2979