Completed
Push — master ( cec761...c24643 )
by mark
07:01 queued 03:21
created

src/View/Helper/FormHelper.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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