Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

Form   F

Complexity

Total Complexity 204

Size/Duplication

Total Lines 1754
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1754
rs 0.6314
c 0
b 0
f 0
wmc 204

89 Methods

Rating   Name   Duplication   Size   Complexity  
A getRedirectToFormOnValidationError() 0 3 1
A setSessionData() 0 4 1
A FieldMap() 0 3 1
A getTemplate() 0 3 1
A setEncType() 0 4 1
A setNotifyUnsavedChanges() 0 3 1
A setFormMethod() 0 7 2
A getSecurityToken() 0 3 1
A clearMessage() 0 5 1
A getValidator() 0 3 1
B getRequest() 0 12 5
A setHTMLID() 0 5 1
A setActions() 0 4 1
A getSessionData() 0 3 1
B setSessionValidationResult() 0 18 5
A getController() 0 3 1
A Actions() 0 3 1
A setTarget() 0 5 1
A getRecord() 0 3 1
A enableSecurityToken() 0 5 1
A setRedirectToFormOnValidationError() 0 4 1
A getRequestHandler() 0 6 2
A getTemplateHelper() 0 11 3
A setRequestHandler() 0 4 1
A getLegend() 0 3 1
A validationResult() 0 15 4
A setAttribute() 0 4 1
A setFormAction() 0 5 1
C saveInto() 0 23 10
A FormAction() 0 8 2
A setName() 0 5 1
A FormMethod() 0 6 2
A castingHelper() 0 7 3
A getName() 0 3 1
A unsetValidator() 0 4 1
A transform() 0 18 4
A formHtmlContent() 0 14 1
A getValidationExemptActions() 0 3 1
A FormName() 0 3 1
F loadDataFrom() 0 110 31
A setValidationResponseCallback() 0 5 1
A getSessionValidationResult() 0 7 2
A buildRequestHandler() 0 3 1
A loadMessagesFrom() 0 13 4
A removeExtraClass() 0 9 2
A setupDefaultClasses() 0 6 3
A sessionMessage() 0 6 2
A extraClass() 0 3 1
A getSession() 0 7 2
F getAttributesHTML() 0 47 12
A actionIsValidationExempt() 0 13 4
A getStrictFormMethodCheck() 0 3 1
A setValidator() 0 7 2
A renderWithoutActionButton() 0 11 2
A sessionError() 0 6 2
A unsetAllActions() 0 4 1
A setLegend() 0 4 1
A FormHttpMethod() 0 3 1
A disableSecurityToken() 0 5 1
A FormAttributes() 0 3 1
A VisibleFields() 0 3 1
A getNotifyUnsavedChanges() 0 3 1
A getTemplates() 0 8 2
A Fields() 0 9 3
A debug() 0 15 3
A setFieldMessage() 0 11 2
A setStrictFormMethodCheck() 0 4 1
A disableDefaultAction() 0 5 1
A HiddenFields() 0 3 1
B __construct() 0 35 4
A forAjaxTemplate() 0 10 1
A setValidationExemptActions() 0 4 1
A getExtraFields() 0 21 4
A clearFormState() 0 7 1
A getHTMLID() 0 3 1
A defaultAction() 0 6 3
A addExtraClass() 0 9 2
A getAttribute() 0 6 2
A forTemplate() 0 8 1
A setController() 0 4 1
A setFields() 0 4 1
A getValidationResponseCallback() 0 3 1
A makeReadonly() 0 3 1
A getData() 0 15 4
A setTemplate() 0 4 1
B getEncType() 0 15 5
A restoreFormState() 0 14 3
A setTemplateHelper() 0 3 1
A getAttributes() 0 21 4

How to fix   Complexity   

Complex Class

Complex classes like Form often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Form, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use BadMethodCallException;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\HasRequestHandler;
8
use SilverStripe\Control\HTTP;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\NullHTTPRequest;
11
use SilverStripe\Control\RequestHandler;
12
use SilverStripe\Control\Session;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Convert;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Dev\Deprecation;
17
use SilverStripe\ORM\DataObject;
18
use SilverStripe\ORM\DataObjectInterface;
19
use SilverStripe\ORM\FieldType\DBHTMLText;
20
use SilverStripe\ORM\ValidationResult;
21
use SilverStripe\Security\NullSecurityToken;
22
use SilverStripe\Security\SecurityToken;
23
use SilverStripe\View\SSViewer;
24
use SilverStripe\View\ViewableData;
25
26
/**
27
 * Base class for all forms.
28
 * The form class is an extensible base for all forms on a SilverStripe application.  It can be used
29
 * either by extending it, and creating processor methods on the subclass, or by creating instances
30
 * of form whose actions are handled by the parent controller.
31
 *
32
 * In either case, if you want to get a form to do anything, it must be inextricably tied to a
33
 * controller.  The constructor is passed a controller and a method on that controller.  This method
34
 * should return the form object, and it shouldn't require any arguments.  Parameters, if necessary,
35
 * can be passed using the URL or get variables.  These restrictions are in place so that we can
36
 * recreate the form object upon form submission, without the use of a session, which would be too
37
 * resource-intensive.
38
 *
39
 * You will need to create at least one method for processing the submission (through {@link FormAction}).
40
 * This method will be passed two parameters: the raw request data, and the form object.
41
 * Usually you want to save data into a {@link DataObject} by using {@link saveInto()}.
42
 * If you want to process the submitted data in any way, please use {@link getData()} rather than
43
 * the raw request data.
44
 *
45
 * <h2>Validation</h2>
46
 * Each form needs some form of {@link Validator} to trigger the {@link FormField->validate()} methods for each field.
47
 * You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads
48
 * depend on it.
49
 * The default validator is an instance of {@link RequiredFields}.
50
 * If you want to enforce serverside-validation to be ignored for a specific {@link FormField},
51
 * you need to subclass it.
52
 *
53
 * <h2>URL Handling</h2>
54
 * The form class extends {@link RequestHandler}, which means it can
55
 * be accessed directly through a URL. This can be handy for refreshing
56
 * a form by ajax, or even just displaying a single form field.
57
 * You can find out the base URL for your form by looking at the
58
 * <form action="..."> value. For example, the edit form in the CMS would be located at
59
 * "admin/EditForm". This URL will render the form without its surrounding
60
 * template when called through GET instead of POST.
61
 *
62
 * By appending to this URL, you can render individual form elements
63
 * through the {@link FormField->FieldHolder()} method.
64
 * For example, the "URLSegment" field in a standard CMS form would be
65
 * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
66
 */
67
class Form extends ViewableData implements HasRequestHandler
68
{
69
    use FormMessage;
70
71
    /**
72
     * Default form Name property
73
     */
74
    const DEFAULT_NAME = 'Form';
75
76
    /**
77
     * Form submission data is URL encoded
78
     */
79
    const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
80
81
    /**
82
     * Form submission data is multipart form
83
     */
84
    const ENC_TYPE_MULTIPART  = 'multipart/form-data';
85
86
    /**
87
     * Accessed by Form.ss; modified by {@link formHtmlContent()}.
88
     * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
89
     *
90
     * @var bool
91
     */
92
    public $IncludeFormTag = true;
93
94
    /**
95
     * @var FieldList
96
     */
97
    protected $fields;
98
99
    /**
100
     * @var FieldList
101
     */
102
    protected $actions;
103
104
    /**
105
     * Parent (optional) request handler
106
     *
107
     * @var RequestHandler
108
     */
109
    protected $controller;
110
111
    /**
112
     * @var string
113
     */
114
    protected $name;
115
116
    /**
117
     * @var Validator
118
     */
119
    protected $validator;
120
121
    /**
122
     * @see setValidationResponseCallback()
123
     * @var callable
124
     */
125
    protected $validationResponseCallback;
126
127
    /**
128
     * @var string
129
     */
130
    protected $formMethod = "POST";
131
132
    /**
133
     * @var boolean
134
     */
135
    protected $strictFormMethodCheck = true;
136
137
    /**
138
     * Populated by {@link loadDataFrom()}.
139
     *
140
     * @var DataObject|null
141
     */
142
    protected $record;
143
144
    /**
145
     * Keeps track of whether this form has a default action or not.
146
     * Set to false by $this->disableDefaultAction();
147
     *
148
     * @var bool
149
     */
150
    protected $hasDefaultAction = true;
151
152
    /**
153
     * Target attribute of form-tag.
154
     * Useful to open a new window upon
155
     * form submission.
156
     *
157
     * @var string|null
158
     */
159
    protected $target;
160
161
    /**
162
     * Legend value, to be inserted into the
163
     * <legend> element before the <fieldset>
164
     * in Form.ss template.
165
     *
166
     * @var string|null
167
     */
168
    protected $legend;
169
170
    /**
171
     * The SS template to render this form HTML into.
172
     * Default is "Form", but this can be changed to
173
     * another template for customisation.
174
     *
175
     * @see Form->setTemplate()
176
     * @var string|array|null
177
     */
178
    protected $template;
179
180
    /**
181
     * Should we redirect the user back down to the
182
     * the form on validation errors rather then just the page
183
     *
184
     * @var bool
185
     */
186
    protected $redirectToFormOnValidationError = false;
187
188
    /**
189
     * @var bool
190
     */
191
    protected $security = true;
192
193
    /**
194
     * @var SecurityToken|null
195
     */
196
    protected $securityToken = null;
197
198
    /**
199
     * List of additional CSS classes for the form tag.
200
     *
201
     * @var array
202
     */
203
    protected $extraClasses = array();
204
205
    /**
206
     * @config
207
     * @var array $default_classes The default classes to apply to the Form
208
     */
209
    private static $default_classes = array();
210
211
    /**
212
     * @var string|null
213
     */
214
    protected $encType;
215
216
    /**
217
     * Any custom form attributes set through {@link setAttributes()}.
218
     * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
219
     *
220
     * @var array
221
     */
222
    protected $attributes = array();
223
224
    /**
225
     * @var array
226
     */
227
    protected $validationExemptActions = array();
228
229
    /**
230
     * @config
231
     * @var array
232
     */
233
    private static $casting = array(
234
        'AttributesHTML' => 'HTMLFragment',
235
        'FormAttributes' => 'HTMLFragment',
236
        'FormName' => 'Text',
237
        'Legend' => 'HTMLFragment',
238
    );
239
240
    /**
241
     * @var FormTemplateHelper
242
     */
243
    private $templateHelper = null;
244
245
    /**
246
     * HTML ID for this form.
247
     *
248
     * @var string
249
     */
250
    private $htmlID = null;
251
252
    /**
253
     * Custom form action path, if not linking to itself.
254
     * E.g. could be used to post to an external link
255
     *
256
     * @var string
257
     */
258
    protected $formActionPath = false;
259
260
    /**
261
     * @var bool
262
     */
263
    protected $securityTokenAdded = false;
264
265
    /**
266
     * @var bool
267
     */
268
    protected $notifyUnsavedChanges = false;
269
270
    /**
271
     * Create a new form, with the given fields an action buttons.
272
     *
273
     * @param RequestHandler $controller Optional parent request handler
274
     * @param string $name The method on the controller that will return this form object.
275
     * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
276
     * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
277
     *                           {@link FormAction} objects
278
     * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
279
     */
280
    public function __construct(
281
        RequestHandler $controller = null,
282
        $name = self::DEFAULT_NAME,
283
        FieldList $fields = null,
284
        FieldList $actions = null,
285
        Validator $validator = null
286
    ) {
287
        parent::__construct();
288
289
        $fields->setForm($this);
0 ignored issues
show
Bug introduced by
The method setForm() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

289
        $fields->/** @scrutinizer ignore-call */ 
290
                 setForm($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
290
        $actions->setForm($this);
0 ignored issues
show
Bug introduced by
The method setForm() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
        $actions->/** @scrutinizer ignore-call */ 
291
                  setForm($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
291
292
        $this->fields = $fields;
293
        $this->actions = $actions;
294
        $this->setController($controller);
295
        $this->setName($name);
296
297
        // Form validation
298
        $this->validator = ($validator) ? $validator : new RequiredFields();
299
        $this->validator->setForm($this);
300
301
        // Form error controls
302
        $this->restoreFormState();
303
304
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
305
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
306
        if (ClassInfo::hasMethod($controller, 'securityTokenEnabled')) {
307
            $securityEnabled = $controller->securityTokenEnabled();
0 ignored issues
show
Bug introduced by
The method securityTokenEnabled() does not exist on SilverStripe\Control\RequestHandler. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
            /** @scrutinizer ignore-call */ 
308
            $securityEnabled = $controller->securityTokenEnabled();
Loading history...
Bug introduced by
The method securityTokenEnabled() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
            /** @scrutinizer ignore-call */ 
308
            $securityEnabled = $controller->securityTokenEnabled();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
308
        } else {
309
            $securityEnabled = SecurityToken::is_enabled();
310
        }
311
312
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
313
314
        $this->setupDefaultClasses();
315
    }
316
317
    /**
318
     * @return bool
319
     */
320
    public function getNotifyUnsavedChanges()
321
    {
322
        return $this->notifyUnsavedChanges;
323
    }
324
325
    /**
326
     * @param bool
327
     */
328
    public function setNotifyUnsavedChanges($flag)
329
    {
330
        $this->notifyUnsavedChanges = $flag;
331
    }
332
333
    /**
334
     * Load form state from session state
335
     *
336
     * @return $this
337
     */
338
    public function restoreFormState()
339
    {
340
        // Restore messages
341
        $result = $this->getSessionValidationResult();
342
        if (isset($result)) {
343
            $this->loadMessagesFrom($result);
344
        }
345
346
        // load data in from previous submission upon error
347
        $data = $this->getSessionData();
348
        if (isset($data)) {
349
            $this->loadDataFrom($data);
350
        }
351
        return $this;
352
    }
353
354
    /**
355
     * Flush persistant form state details
356
     *
357
     * @return $this
358
     */
359
    public function clearFormState()
360
    {
361
        $this
362
            ->getSession()
363
            ->clear("FormInfo.{$this->FormName()}.result")
364
            ->clear("FormInfo.{$this->FormName()}.data");
365
        return $this;
366
    }
367
368
    /**
369
     * Helper to get current request for this form
370
     *
371
     * @return HTTPRequest
372
     */
373
    protected function getRequest()
374
    {
375
        // Check if current request handler has a request object
376
        $controller = $this->getController();
377
        if ($controller && !($controller->getRequest() instanceof NullHTTPRequest)) {
378
            return $controller->getRequest();
379
        }
380
        // Fall back to current controller
381
        if (Controller::has_curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
382
            return Controller::curr()->getRequest();
383
        }
384
        return null;
385
    }
386
387
    /**
388
     * Get session for this form
389
     *
390
     * @return Session
391
     */
392
    protected function getSession()
393
    {
394
        $request = $this->getRequest();
395
        if ($request) {
396
            return $request->getSession();
397
        }
398
        throw new BadMethodCallException("Session not available in the current context");
399
    }
400
401
    /**
402
     * Return any form data stored in the session
403
     *
404
     * @return array
405
     */
406
    public function getSessionData()
407
    {
408
        return $this->getSession()->get("FormInfo.{$this->FormName()}.data");
409
    }
410
411
    /**
412
     * Store the given form data in the session
413
     *
414
     * @param array $data
415
     * @return $this
416
     */
417
    public function setSessionData($data)
418
    {
419
        $this->getSession()->set("FormInfo.{$this->FormName()}.data", $data);
420
        return $this;
421
    }
422
423
    /**
424
     * Return any ValidationResult instance stored for this object
425
     *
426
     * @return ValidationResult The ValidationResult object stored in the session
427
     */
428
    public function getSessionValidationResult()
429
    {
430
        $resultData = $this->getSession()->get("FormInfo.{$this->FormName()}.result");
431
        if (isset($resultData)) {
432
            return unserialize($resultData);
433
        }
434
        return null;
435
    }
436
437
    /**
438
     * Sets the ValidationResult in the session to be used with the next view of this form.
439
     * @param ValidationResult $result The result to save
440
     * @param bool $combineWithExisting If true, then this will be added to the existing result.
441
     * @return $this
442
     */
443
    public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
444
    {
445
        // Combine with existing result
446
        if ($combineWithExisting) {
447
            $existingResult = $this->getSessionValidationResult();
448
            if ($existingResult) {
449
                if ($result) {
450
                    $existingResult->combineAnd($result);
451
                } else {
452
                    $result = $existingResult;
453
                }
454
            }
455
        }
456
457
        // Serialise
458
        $resultData = $result ? serialize($result) : null;
459
        $this->getSession()->set("FormInfo.{$this->FormName()}.result", $resultData);
460
        return $this;
461
    }
462
463
    /**
464
     * Clear form message (and in session)
465
     *
466
     * @return $this
467
     */
468
    public function clearMessage()
469
    {
470
        $this->setMessage(null);
471
        $this->clearFormState();
472
        return $this;
473
    }
474
475
    /**
476
     * Populate this form with messages from the given ValidationResult.
477
     * Note: This will not clear any pre-existing messages
478
     *
479
     * @param ValidationResult $result
480
     * @return $this
481
     */
482
    public function loadMessagesFrom($result)
483
    {
484
        // Set message on either a field or the parent form
485
        foreach ($result->getMessages() as $message) {
486
            $fieldName = $message['fieldName'];
487
            if ($fieldName) {
488
                $owner = $this->fields->dataFieldByName($fieldName) ?: $this;
489
            } else {
490
                $owner = $this;
491
            }
492
            $owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
493
        }
494
        return $this;
495
    }
496
497
    /**
498
     * Set message on a given field name. This message will not persist via redirect.
499
     *
500
     * @param string $fieldName
501
     * @param string $message
502
     * @param string $messageType
503
     * @param string $messageCast
504
     * @return $this
505
     */
506
    public function setFieldMessage(
507
        $fieldName,
508
        $message,
509
        $messageType = ValidationResult::TYPE_ERROR,
510
        $messageCast = ValidationResult::CAST_TEXT
511
    ) {
512
        $field = $this->fields->dataFieldByName($fieldName);
513
        if ($field) {
514
            $field->setMessage($message, $messageType, $messageCast);
515
        }
516
        return $this;
517
    }
518
519
    public function castingHelper($field)
520
    {
521
        // Override casting for field message
522
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
523
            return $helper;
524
        }
525
        return parent::castingHelper($field);
526
    }
527
528
    /**
529
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
530
     * after instantiation
531
     */
532
    protected function setupDefaultClasses()
533
    {
534
        $defaultClasses = self::config()->get('default_classes');
535
        if ($defaultClasses) {
536
            foreach ($defaultClasses as $class) {
537
                $this->addExtraClass($class);
538
            }
539
        }
540
    }
541
542
    /**
543
     * @return callable
544
     */
545
    public function getValidationResponseCallback()
546
    {
547
        return $this->validationResponseCallback;
548
    }
549
550
    /**
551
     * Overrules validation error behaviour in {@link httpSubmission()}
552
     * when validation has failed. Useful for optional handling of a certain accepted content type.
553
     *
554
     * The callback can opt out of handling specific responses by returning NULL,
555
     * in which case the default form behaviour will kick in.
556
     *
557
     * @param $callback
558
     * @return self
559
     */
560
    public function setValidationResponseCallback($callback)
561
    {
562
        $this->validationResponseCallback = $callback;
563
564
        return $this;
565
    }
566
    /**
567
     * Convert this form into a readonly form
568
     */
569
    public function makeReadonly()
570
    {
571
        $this->transform(new ReadonlyTransformation());
572
    }
573
574
    /**
575
     * Set whether the user should be redirected back down to the
576
     * form on the page upon validation errors in the form or if
577
     * they just need to redirect back to the page
578
     *
579
     * @param bool $bool Redirect to form on error?
580
     * @return $this
581
     */
582
    public function setRedirectToFormOnValidationError($bool)
583
    {
584
        $this->redirectToFormOnValidationError = $bool;
585
        return $this;
586
    }
587
588
    /**
589
     * Get whether the user should be redirected back down to the
590
     * form on the page upon validation errors
591
     *
592
     * @return bool
593
     */
594
    public function getRedirectToFormOnValidationError()
595
    {
596
        return $this->redirectToFormOnValidationError;
597
    }
598
599
    /**
600
     * @param FormTransformation $trans
601
     */
602
    public function transform(FormTransformation $trans)
603
    {
604
        $newFields = new FieldList();
605
        foreach ($this->fields as $field) {
606
            $newFields->push($field->transform($trans));
607
        }
608
        $this->fields = $newFields;
609
610
        $newActions = new FieldList();
611
        foreach ($this->actions as $action) {
612
            $newActions->push($action->transform($trans));
613
        }
614
        $this->actions = $newActions;
615
616
617
        // We have to remove validation, if the fields are not editable ;-)
618
        if ($this->validator) {
619
            $this->validator->removeValidation();
620
        }
621
    }
622
623
    /**
624
     * Get the {@link Validator} attached to this form.
625
     * @return Validator
626
     */
627
    public function getValidator()
628
    {
629
        return $this->validator;
630
    }
631
632
    /**
633
     * Set the {@link Validator} on this form.
634
     * @param Validator $validator
635
     * @return $this
636
     */
637
    public function setValidator(Validator $validator)
638
    {
639
        if ($validator) {
640
            $this->validator = $validator;
641
            $this->validator->setForm($this);
642
        }
643
        return $this;
644
    }
645
646
    /**
647
     * Remove the {@link Validator} from this from.
648
     */
649
    public function unsetValidator()
650
    {
651
        $this->validator = null;
652
        return $this;
653
    }
654
655
    /**
656
     * Set actions that are exempt from validation
657
     *
658
     * @param array
659
     * @return $this
660
     */
661
    public function setValidationExemptActions($actions)
662
    {
663
        $this->validationExemptActions = $actions;
664
        return $this;
665
    }
666
667
    /**
668
     * Get a list of actions that are exempt from validation
669
     *
670
     * @return array
671
     */
672
    public function getValidationExemptActions()
673
    {
674
        return $this->validationExemptActions;
675
    }
676
677
    /**
678
     * Passed a FormAction, returns true if that action is exempt from Form validation
679
     *
680
     * @param FormAction $action
681
     * @return bool
682
     */
683
    public function actionIsValidationExempt($action)
684
    {
685
        // Non-actions don't bypass validation
686
        if (!$action) {
687
            return false;
688
        }
689
        if ($action->getValidationExempt()) {
690
            return true;
691
        }
692
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
693
            return true;
694
        }
695
        return false;
696
    }
697
698
    /**
699
     * Generate extra special fields - namely the security token field (if required).
700
     *
701
     * @return FieldList
702
     */
703
    public function getExtraFields()
704
    {
705
        $extraFields = new FieldList();
706
707
        $token = $this->getSecurityToken();
708
        if ($token) {
709
            $tokenField = $token->updateFieldSet($this->fields);
710
            if ($tokenField) {
711
                $tokenField->setForm($this);
712
            }
713
        }
714
        $this->securityTokenAdded = true;
715
716
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
717
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
718
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
719
            $methodField->setForm($this);
720
            $extraFields->push($methodField);
721
        }
722
723
        return $extraFields;
724
    }
725
726
    /**
727
     * Return the form's fields - used by the templates
728
     *
729
     * @return FieldList The form fields
730
     */
731
    public function Fields()
732
    {
733
        foreach ($this->getExtraFields() as $field) {
734
            if (!$this->fields->fieldByName($field->getName())) {
735
                $this->fields->push($field);
736
            }
737
        }
738
739
        return $this->fields;
740
    }
741
742
    /**
743
     * Return all <input type="hidden"> fields
744
     * in a form - including fields nested in {@link CompositeFields}.
745
     * Useful when doing custom field layouts.
746
     *
747
     * @return FieldList
748
     */
749
    public function HiddenFields()
750
    {
751
        return $this->Fields()->HiddenFields();
752
    }
753
754
    /**
755
     * Return all fields except for the hidden fields.
756
     * Useful when making your own simplified form layouts.
757
     */
758
    public function VisibleFields()
759
    {
760
        return $this->Fields()->VisibleFields();
761
    }
762
763
    /**
764
     * Setter for the form fields.
765
     *
766
     * @param FieldList $fields
767
     * @return $this
768
     */
769
    public function setFields($fields)
770
    {
771
        $this->fields = $fields;
772
        return $this;
773
    }
774
775
    /**
776
     * Return the form's action buttons - used by the templates
777
     *
778
     * @return FieldList The action list
779
     */
780
    public function Actions()
781
    {
782
        return $this->actions;
783
    }
784
785
    /**
786
     * Setter for the form actions.
787
     *
788
     * @param FieldList $actions
789
     * @return $this
790
     */
791
    public function setActions($actions)
792
    {
793
        $this->actions = $actions;
794
        return $this;
795
    }
796
797
    /**
798
     * Unset all form actions
799
     */
800
    public function unsetAllActions()
801
    {
802
        $this->actions = new FieldList();
803
        return $this;
804
    }
805
806
    /**
807
     * @param string $name
808
     * @param string $value
809
     * @return $this
810
     */
811
    public function setAttribute($name, $value)
812
    {
813
        $this->attributes[$name] = $value;
814
        return $this;
815
    }
816
817
    /**
818
     * @param string $name
819
     * @return string
820
     */
821
    public function getAttribute($name)
822
    {
823
        if (isset($this->attributes[$name])) {
824
            return $this->attributes[$name];
825
        }
826
        return null;
827
    }
828
829
    /**
830
     * @return array
831
     */
832
    public function getAttributes()
833
    {
834
        $attrs = array(
835
            'id' => $this->FormName(),
836
            'action' => $this->FormAction(),
837
            'method' => $this->FormMethod(),
838
            'enctype' => $this->getEncType(),
839
            'target' => $this->target,
840
            'class' => $this->extraClass(),
841
        );
842
843
        if ($this->validator && $this->validator->getErrors()) {
844
            if (!isset($attrs['class'])) {
845
                $attrs['class'] = '';
846
            }
847
            $attrs['class'] .= ' validationerror';
848
        }
849
850
        $attrs = array_merge($attrs, $this->attributes);
851
852
        return $attrs;
853
    }
854
855
    /**
856
     * Return the attributes of the form tag - used by the templates.
857
     *
858
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
859
     * If at least one argument is passed as a string, all arguments act as excludes by name.
860
     *
861
     * @return string HTML attributes, ready for insertion into an HTML tag
862
     */
863
    public function getAttributesHTML($attrs = null)
864
    {
865
        $exclude = (is_string($attrs)) ? func_get_args() : null;
866
867
        // Figure out if we can cache this form
868
        // - forms with validation shouldn't be cached, cos their error messages won't be shown
869
        // - forms with security tokens shouldn't be cached because security tokens expire
870
        $needsCacheDisabled = false;
871
        if ($this->getSecurityToken()->isEnabled()) {
872
            $needsCacheDisabled = true;
873
        }
874
        if ($this->FormMethod() != 'GET') {
875
            $needsCacheDisabled = true;
876
        }
877
        if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
878
            $needsCacheDisabled = true;
879
        }
880
881
        // If we need to disable cache, do it
882
        if ($needsCacheDisabled) {
883
            HTTP::set_cache_age(0);
884
        }
885
886
        $attrs = $this->getAttributes();
887
888
        // Remove empty
889
        $attrs = array_filter((array)$attrs, function ($value) {
890
            return ($value || $value === 0);
891
        });
892
893
        // Remove excluded
894
        if ($exclude) {
895
            $attrs = array_diff_key($attrs, array_flip($exclude));
896
        }
897
898
        // Prepare HTML-friendly 'method' attribute (lower-case)
899
        if (isset($attrs['method'])) {
900
            $attrs['method'] = strtolower($attrs['method']);
901
        }
902
903
        // Create markup
904
        $parts = array();
905
        foreach ($attrs as $name => $value) {
906
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
0 ignored issues
show
Bug introduced by
Are you sure SilverStripe\Core\Convert::raw2att($value) of type string|array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

906
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . /** @scrutinizer ignore-type */ Convert::raw2att($value) . "\"";
Loading history...
907
        }
908
909
        return implode(' ', $parts);
910
    }
911
912
    public function FormAttributes()
913
    {
914
        return $this->getAttributesHTML();
915
    }
916
917
    /**
918
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
919
     * another frame
920
    *
921
     * @param string|FormTemplateHelper
922
    */
923
    public function setTemplateHelper($helper)
924
    {
925
        $this->templateHelper = $helper;
926
    }
927
928
    /**
929
     * Return a {@link FormTemplateHelper} for this form. If one has not been
930
     * set, return the default helper.
931
     *
932
     * @return FormTemplateHelper
933
     */
934
    public function getTemplateHelper()
935
    {
936
        if ($this->templateHelper) {
937
            if (is_string($this->templateHelper)) {
938
                return Injector::inst()->get($this->templateHelper);
939
            }
940
941
            return $this->templateHelper;
942
        }
943
944
        return FormTemplateHelper::singleton();
945
    }
946
947
    /**
948
     * Set the target of this form to any value - useful for opening the form
949
     * contents in a new window or refreshing another frame.
950
     *
951
     * @param string $target The value of the target
952
     * @return $this
953
     */
954
    public function setTarget($target)
955
    {
956
        $this->target = $target;
957
958
        return $this;
959
    }
960
961
    /**
962
     * Set the legend value to be inserted into
963
     * the <legend> element in the Form.ss template.
964
     * @param string $legend
965
     * @return $this
966
     */
967
    public function setLegend($legend)
968
    {
969
        $this->legend = $legend;
970
        return $this;
971
    }
972
973
    /**
974
     * Set the SS template that this form should use
975
     * to render with. The default is "Form".
976
     *
977
     * @param string|array $template The name of the template (without the .ss extension) or array form
978
     * @return $this
979
     */
980
    public function setTemplate($template)
981
    {
982
        $this->template = $template;
983
        return $this;
984
    }
985
986
    /**
987
     * Return the template to render this form with.
988
     *
989
     * @return string|array
990
     */
991
    public function getTemplate()
992
    {
993
        return $this->template;
994
    }
995
996
    /**
997
     * Returs the ordered list of preferred templates for rendering this form
998
     * If the template isn't set, then default to the
999
     * form class name e.g "Form".
1000
     *
1001
     * @return array
1002
     */
1003
    public function getTemplates()
1004
    {
1005
        $templates = SSViewer::get_templates_by_class(static::class, '', __CLASS__);
1006
        // Prefer any custom template
1007
        if ($this->getTemplate()) {
1008
            array_unshift($templates, $this->getTemplate());
1009
        }
1010
        return $templates;
1011
    }
1012
1013
    /**
1014
     * Returns the encoding type for the form.
1015
     *
1016
     * By default this will be URL encoded, unless there is a file field present
1017
     * in which case multipart is used. You can also set the enc type using
1018
     * {@link setEncType}.
1019
     */
1020
    public function getEncType()
1021
    {
1022
        if ($this->encType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->encType of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1023
            return $this->encType;
1024
        }
1025
1026
        if ($fields = $this->fields->dataFields()) {
1027
            foreach ($fields as $field) {
1028
                if ($field instanceof FileField) {
1029
                    return self::ENC_TYPE_MULTIPART;
1030
                }
1031
            }
1032
        }
1033
1034
        return self::ENC_TYPE_URLENCODED;
1035
    }
1036
1037
    /**
1038
     * Sets the form encoding type. The most common encoding types are defined
1039
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1040
     *
1041
     * @param string $encType
1042
     * @return $this
1043
     */
1044
    public function setEncType($encType)
1045
    {
1046
        $this->encType = $encType;
1047
        return $this;
1048
    }
1049
1050
    /**
1051
     * Returns the real HTTP method for the form:
1052
     * GET, POST, PUT, DELETE or HEAD.
1053
     * As most browsers only support GET and POST in
1054
     * form submissions, all other HTTP methods are
1055
     * added as a hidden field "_method" that
1056
     * gets evaluated in {@link HTTPRequest::detect_method()}.
1057
     * See {@link FormMethod()} to get a HTTP method
1058
     * for safe insertion into a <form> tag.
1059
     *
1060
     * @return string HTTP method
1061
     */
1062
    public function FormHttpMethod()
1063
    {
1064
        return $this->formMethod;
1065
    }
1066
1067
    /**
1068
     * Returns the form method to be used in the <form> tag.
1069
     * See {@link FormHttpMethod()} to get the "real" method.
1070
     *
1071
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1072
     */
1073
    public function FormMethod()
1074
    {
1075
        if (in_array($this->formMethod, array('GET','POST'))) {
1076
            return $this->formMethod;
1077
        } else {
1078
            return 'POST';
1079
        }
1080
    }
1081
1082
    /**
1083
     * Set the form method: GET, POST, PUT, DELETE.
1084
     *
1085
     * @param string $method
1086
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1087
     * @return $this
1088
     */
1089
    public function setFormMethod($method, $strict = null)
1090
    {
1091
        $this->formMethod = strtoupper($method);
1092
        if ($strict !== null) {
1093
            $this->setStrictFormMethodCheck($strict);
1094
        }
1095
        return $this;
1096
    }
1097
1098
    /**
1099
     * If set to true (the default), enforces the matching of the form method.
1100
     *
1101
     * This will mean two things:
1102
     *  - GET vars will be ignored by a POST form, and vice versa
1103
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1104
     *
1105
     * If set to false then the form method is only used to construct the default
1106
     * form.
1107
     *
1108
     * @param $bool boolean
1109
     * @return $this
1110
     */
1111
    public function setStrictFormMethodCheck($bool)
1112
    {
1113
        $this->strictFormMethodCheck = (bool)$bool;
1114
        return $this;
1115
    }
1116
1117
    /**
1118
     * @return boolean
1119
     */
1120
    public function getStrictFormMethodCheck()
1121
    {
1122
        return $this->strictFormMethodCheck;
1123
    }
1124
1125
    /**
1126
     * Return the form's action attribute.
1127
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1128
     *
1129
     * @return string
1130
     */
1131
    public function FormAction()
1132
    {
1133
        if ($this->formActionPath) {
1134
            return $this->formActionPath;
1135
        }
1136
1137
        // Get action from request handler link
1138
        return $this->getRequestHandler()->Link();
1139
    }
1140
1141
    /**
1142
     * Set the form action attribute to a custom URL.
1143
     *
1144
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1145
     * recommended only for situations where you have two relatively distinct
1146
     * parts of the system trying to communicate via a form post.
1147
     *
1148
     * @param string $path
1149
     * @return $this
1150
     */
1151
    public function setFormAction($path)
1152
    {
1153
        $this->formActionPath = $path;
1154
1155
        return $this;
1156
    }
1157
1158
    /**
1159
     * Returns the name of the form.
1160
     *
1161
     * @return string
1162
     */
1163
    public function FormName()
1164
    {
1165
        return $this->getTemplateHelper()->generateFormID($this);
1166
    }
1167
1168
    /**
1169
     * Set the HTML ID attribute of the form.
1170
     *
1171
     * @param string $id
1172
     * @return $this
1173
     */
1174
    public function setHTMLID($id)
1175
    {
1176
        $this->htmlID = $id;
1177
1178
        return $this;
1179
    }
1180
1181
    /**
1182
     * @return string
1183
     */
1184
    public function getHTMLID()
1185
    {
1186
        return $this->htmlID;
1187
    }
1188
1189
    /**
1190
     * Get the controller or parent request handler.
1191
     *
1192
     * @return RequestHandler
1193
     */
1194
    public function getController()
1195
    {
1196
        return $this->controller;
1197
    }
1198
1199
    /**
1200
     * Set the controller or parent request handler.
1201
     *
1202
     * @param RequestHandler $controller
1203
     * @return $this
1204
     */
1205
    public function setController(RequestHandler $controller = null)
1206
    {
1207
        $this->controller = $controller;
1208
        return $this;
1209
    }
1210
1211
    /**
1212
     * Get the name of the form.
1213
     *
1214
     * @return string
1215
     */
1216
    public function getName()
1217
    {
1218
        return $this->name;
1219
    }
1220
1221
    /**
1222
     * Set the name of the form.
1223
     *
1224
     * @param string $name
1225
     * @return Form
1226
     */
1227
    public function setName($name)
1228
    {
1229
        $this->name = $name;
1230
1231
        return $this;
1232
    }
1233
1234
    /**
1235
     * Returns an object where there is a method with the same name as each data
1236
     * field on the form.
1237
     *
1238
     * That method will return the field itself.
1239
     *
1240
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1241
     */
1242
    public function FieldMap()
1243
    {
1244
        return new Form_FieldMap($this);
1245
    }
1246
1247
    /**
1248
     * Set a message to the session, for display next time this form is shown.
1249
     *
1250
     * @param string $message the text of the message
1251
     * @param string $type Should be set to good, bad, or warning.
1252
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1253
     * Bool values will be treated as plain text flag.
1254
     */
1255
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1256
    {
1257
        $this->setMessage($message, $type, $cast);
1258
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1259
        $result->addMessage($message, $type, null, $cast);
1260
        $this->setSessionValidationResult($result);
1261
    }
1262
1263
    /**
1264
     * Set an error to the session, for display next time this form is shown.
1265
     *
1266
     * @param string $message the text of the message
1267
     * @param string $type Should be set to good, bad, or warning.
1268
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1269
     * Bool values will be treated as plain text flag.
1270
     */
1271
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1272
    {
1273
        $this->setMessage($message, $type, $cast);
1274
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1275
        $result->addError($message, $type, null, $cast);
1276
        $this->setSessionValidationResult($result);
1277
    }
1278
1279
    /**
1280
     * Returns the DataObject that has given this form its data
1281
     * through {@link loadDataFrom()}.
1282
     *
1283
     * @return DataObject
1284
     */
1285
    public function getRecord()
1286
    {
1287
        return $this->record;
1288
    }
1289
1290
    /**
1291
     * Get the legend value to be inserted into the
1292
     * <legend> element in Form.ss
1293
     *
1294
     * @return string
1295
     */
1296
    public function getLegend()
1297
    {
1298
        return $this->legend;
1299
    }
1300
1301
    /**
1302
     * Processing that occurs before a form is executed.
1303
     *
1304
     * This includes form validation, if it fails, we throw a ValidationException
1305
     *
1306
     * This includes form validation, if it fails, we redirect back
1307
     * to the form with appropriate error messages.
1308
     * Always return true if the current form action is exempt from validation
1309
     *
1310
     * Triggered through {@link httpSubmission()}.
1311
     *
1312
     *
1313
     * Note that CSRF protection takes place in {@link httpSubmission()},
1314
     * if it fails the form data will never reach this method.
1315
     *
1316
     * @return ValidationResult
1317
     */
1318
    public function validationResult()
1319
    {
1320
        // Automatically pass if there is no validator, or the clicked button is exempt
1321
        // Note: Soft support here for validation with absent request handler
1322
        $handler = $this->getRequestHandler();
1323
        $action = $handler ? $handler->buttonClicked() : null;
1324
        $validator = $this->getValidator();
1325
        if (!$validator || $this->actionIsValidationExempt($action)) {
1326
            return ValidationResult::create();
1327
        }
1328
1329
        // Invoke validator
1330
        $result = $validator->validate();
1331
        $this->loadMessagesFrom($result);
1332
        return $result;
1333
    }
1334
1335
    const MERGE_DEFAULT = 0;
1336
    const MERGE_CLEAR_MISSING = 1;
1337
    const MERGE_IGNORE_FALSEISH = 2;
1338
1339
    /**
1340
     * Load data from the given DataObject or array.
1341
     *
1342
     * It will call $object->MyField to get the value of MyField.
1343
     * If you passed an array, it will call $object[MyField].
1344
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1345
     * as determined by {@link FieldList->dataFields()}.
1346
     *
1347
     * By default, if a field isn't set (as determined by isset()),
1348
     * its value will not be saved to the field, retaining
1349
     * potential existing values.
1350
     *
1351
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1352
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1353
     *
1354
     * Escaping happens automatically on saving the data through
1355
     * {@link saveInto()}.
1356
     *
1357
     * @uses FieldList->dataFields()
1358
     * @uses FormField->setValue()
1359
     *
1360
     * @param array|DataObject $data
1361
     * @param int $mergeStrategy
1362
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1363
     *  what that property/key's value is.
1364
     *
1365
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1366
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1367
     *  "left alone", meaning they retain any previous value.
1368
     *
1369
     *  You can pass a bitmask here to change this behaviour.
1370
     *
1371
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1372
     *  {@link $data} are cleared.
1373
     *
1374
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1375
     *  a field's value.
1376
     *
1377
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1378
     *  CLEAR_MISSING
1379
     *
1380
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1381
     * form that has some fields that save to one object, and some that save to another.
1382
     * @return $this
1383
     */
1384
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1385
    {
1386
        if (!is_object($data) && !is_array($data)) {
1387
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1388
            return $this;
1389
        }
1390
1391
        // Handle the backwards compatible case of passing "true" as the second argument
1392
        if ($mergeStrategy === true) {
1393
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1394
        } elseif ($mergeStrategy === false) {
1395
            $mergeStrategy = 0;
1396
        }
1397
1398
        // If an object is passed, save it for historical reference through {@link getRecord()}
1399
        // Also use this to determine if we are loading a submitted form, or loading
1400
        // from a dataobject
1401
        $submitted = true;
1402
        if (is_object($data)) {
1403
            $this->record = $data;
1404
            $submitted = false;
1405
        }
1406
1407
        // dont include fields without data
1408
        $dataFields = $this->Fields()->dataFields();
1409
        if (!$dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1410
            return $this;
1411
        }
1412
1413
        /** @var FormField $field */
1414
        foreach ($dataFields as $field) {
1415
            $name = $field->getName();
1416
1417
            // Skip fields that have been excluded
1418
            if ($fieldList && !in_array($name, $fieldList)) {
1419
                continue;
1420
            }
1421
1422
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1423
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1424
                continue;
1425
            }
1426
1427
            // Does this property exist on $data?
1428
            $exists = false;
1429
            // The value from $data for this field
1430
            $val = null;
1431
1432
            if (is_object($data)) {
1433
                $exists = (
1434
                    isset($data->$name) ||
1435
                    $data->hasMethod($name) ||
1436
                    ($data->hasMethod('hasField') && $data->hasField($name))
1437
                );
1438
1439
                if ($exists) {
1440
                    $val = $data->__get($name);
1441
                }
1442
            } elseif (is_array($data)) {
1443
                if (array_key_exists($name, $data)) {
1444
                    $exists = true;
1445
                    $val = $data[$name];
1446
                } elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1447
                    // If field is in array-notation we need to access nested data
1448
                    //discard first match which is just the whole string
1449
                    array_shift($matches);
1450
                    $keys = array_pop($matches);
1451
                    $name = array_shift($matches);
1452
                    $name = array_shift($name);
1453
                    if (array_key_exists($name, $data)) {
1454
                        $tmpData = &$data[$name];
1455
                        // drill down into the data array looking for the corresponding value
1456
                        foreach ($keys as $arrayKey) {
1457
                            if ($arrayKey !== '') {
1458
                                $tmpData = &$tmpData[$arrayKey];
1459
                            } else {
1460
                                //empty square brackets means new array
1461
                                if (is_array($tmpData)) {
1462
                                    $tmpData = array_shift($tmpData);
1463
                                }
1464
                            }
1465
                        }
1466
                        if ($tmpData) {
1467
                            $val = $tmpData;
1468
                            $exists = true;
1469
                        }
1470
                    }
1471
                }
1472
            }
1473
1474
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1475
            $setValue = false;
1476
            if ($exists) {
1477
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1478
                    $setValue = true;
1479
                }
1480
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1481
                $setValue = true;
1482
            }
1483
1484
            // pass original data as well so composite fields can act on the additional information
1485
            if ($setValue) {
1486
                if ($submitted) {
1487
                    $field->setSubmittedValue($val, $data);
1488
                } else {
1489
                    $field->setValue($val, $data);
1490
                }
1491
            }
1492
        }
1493
        return $this;
1494
    }
1495
1496
    /**
1497
     * Save the contents of this form into the given data object.
1498
     * It will make use of setCastedField() to do this.
1499
     *
1500
     * @param DataObjectInterface $dataObject The object to save data into
1501
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1502
     * form that has some fields that save to one object, and some that save to another.
1503
     */
1504
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1505
    {
1506
        $dataFields = $this->fields->saveableFields();
1507
        $lastField = null;
1508
        if ($dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1509
            foreach ($dataFields as $field) {
1510
            // Skip fields that have been excluded
1511
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1512
                    continue;
1513
                }
1514
1515
                $saveMethod = "save{$field->getName()}";
1516
                if ($field->getName() == "ClassName") {
1517
                    $lastField = $field;
1518
                } elseif ($dataObject->hasMethod($saveMethod)) {
1519
                    $dataObject->$saveMethod($field->dataValue());
1520
                } elseif ($field->getName() !== "ID") {
1521
                    $field->saveInto($dataObject);
1522
                }
1523
            }
1524
        }
1525
        if ($lastField) {
1526
            $lastField->saveInto($dataObject);
1527
        }
1528
    }
1529
1530
    /**
1531
     * Get the submitted data from this form through
1532
     * {@link FieldList->dataFields()}, which filters out
1533
     * any form-specific data like form-actions.
1534
     * Calls {@link FormField->dataValue()} on each field,
1535
     * which returns a value suitable for insertion into a DataObject
1536
     * property.
1537
     *
1538
     * @return array
1539
     */
1540
    public function getData()
1541
    {
1542
        $dataFields = $this->fields->dataFields();
1543
        $data = array();
1544
1545
        if ($dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1546
            /** @var FormField $field */
1547
            foreach ($dataFields as $field) {
1548
                if ($field->getName()) {
1549
                    $data[$field->getName()] = $field->dataValue();
1550
                }
1551
            }
1552
        }
1553
1554
        return $data;
1555
    }
1556
1557
    /**
1558
     * Return a rendered version of this form.
1559
     *
1560
     * This is returned when you access a form as $FormObject rather
1561
     * than <% with FormObject %>
1562
     *
1563
     * @return DBHTMLText
1564
     */
1565
    public function forTemplate()
1566
    {
1567
        $return = $this->renderWith($this->getTemplates());
1568
1569
        // Now that we're rendered, clear message
1570
        $this->clearMessage();
1571
1572
        return $return;
1573
    }
1574
1575
    /**
1576
     * Return a rendered version of this form, suitable for ajax post-back.
1577
     *
1578
     * It triggers slightly different behaviour, such as disabling the rewriting
1579
     * of # links.
1580
     *
1581
     * @return DBHTMLText
1582
     */
1583
    public function forAjaxTemplate()
1584
    {
1585
        $view = SSViewer::create($this->getTemplates());
1586
1587
        $return = $view->dontRewriteHashlinks()->process($this);
1588
1589
        // Now that we're rendered, clear message
1590
        $this->clearMessage();
1591
1592
        return $return;
1593
    }
1594
1595
    /**
1596
     * Returns an HTML rendition of this form, without the <form> tag itself.
1597
     *
1598
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1599
     * and _form_enctype.  These are the attributes of the form.  These fields
1600
     * can be used to send the form to Ajax.
1601
     *
1602
     * @deprecated 5.0
1603
     * @return string
1604
     */
1605
    public function formHtmlContent()
1606
    {
1607
        Deprecation::notice('5.0');
1608
        $this->IncludeFormTag = false;
1609
        $content = $this->forTemplate();
1610
        $this->IncludeFormTag = true;
1611
1612
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
1613
            . " value=\"" . $this->FormAction() . "\" />\n";
1614
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1615
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1616
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1617
1618
        return $content;
1619
    }
1620
1621
    /**
1622
     * Render this form using the given template, and return the result as a string
1623
     * You can pass either an SSViewer or a template name
1624
     * @param string|array $template
1625
     * @return DBHTMLText
1626
     */
1627
    public function renderWithoutActionButton($template)
1628
    {
1629
        $custom = $this->customise(array(
1630
            "Actions" => "",
1631
        ));
1632
1633
        if (is_string($template)) {
1634
            $template = SSViewer::create($template);
0 ignored issues
show
Bug introduced by
$template of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\SSViewer::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1634
            $template = SSViewer::create(/** @scrutinizer ignore-type */ $template);
Loading history...
1635
        }
1636
1637
        return $template->process($custom);
1638
    }
1639
1640
    /**
1641
     * Return the default button that should be clicked when another one isn't
1642
     * available.
1643
     *
1644
     * @return FormAction
1645
     */
1646
    public function defaultAction()
1647
    {
1648
        if ($this->hasDefaultAction && $this->actions) {
1649
            return $this->actions->first();
1650
        }
1651
        return null;
1652
    }
1653
1654
    /**
1655
     * Disable the default button.
1656
     *
1657
     * Ordinarily, when a form is processed and no action_XXX button is
1658
     * available, then the first button in the actions list will be pressed.
1659
     * However, if this is "delete", for example, this isn't such a good idea.
1660
     *
1661
     * @return Form
1662
     */
1663
    public function disableDefaultAction()
1664
    {
1665
        $this->hasDefaultAction = false;
1666
1667
        return $this;
1668
    }
1669
1670
    /**
1671
     * Disable the requirement of a security token on this form instance. This
1672
     * security protects against CSRF attacks, but you should disable this if
1673
     * you don't want to tie a form to a session - eg a search form.
1674
     *
1675
     * Check for token state with {@link getSecurityToken()} and
1676
     * {@link SecurityToken->isEnabled()}.
1677
     *
1678
     * @return Form
1679
     */
1680
    public function disableSecurityToken()
1681
    {
1682
        $this->securityToken = new NullSecurityToken();
1683
1684
        return $this;
1685
    }
1686
1687
    /**
1688
     * Enable {@link SecurityToken} protection for this form instance.
1689
     *
1690
     * Check for token state with {@link getSecurityToken()} and
1691
     * {@link SecurityToken->isEnabled()}.
1692
     *
1693
     * @return Form
1694
     */
1695
    public function enableSecurityToken()
1696
    {
1697
        $this->securityToken = new SecurityToken();
1698
1699
        return $this;
1700
    }
1701
1702
    /**
1703
     * Returns the security token for this form (if any exists).
1704
     *
1705
     * Doesn't check for {@link securityTokenEnabled()}.
1706
     *
1707
     * Use {@link SecurityToken::inst()} to get a global token.
1708
     *
1709
     * @return SecurityToken|null
1710
     */
1711
    public function getSecurityToken()
1712
    {
1713
        return $this->securityToken;
1714
    }
1715
1716
    /**
1717
     * Compiles all CSS-classes.
1718
     *
1719
     * @return string
1720
     */
1721
    public function extraClass()
1722
    {
1723
        return implode(array_unique($this->extraClasses), ' ');
0 ignored issues
show
Bug introduced by
' ' of type string is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1723
        return implode(array_unique($this->extraClasses), /** @scrutinizer ignore-type */ ' ');
Loading history...
Bug introduced by
array_unique($this->extraClasses) of type array is incompatible with the type string expected by parameter $glue of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1723
        return implode(/** @scrutinizer ignore-type */ array_unique($this->extraClasses), ' ');
Loading history...
1724
    }
1725
1726
    /**
1727
     * Add a CSS-class to the form-container. If needed, multiple classes can
1728
     * be added by delimiting a string with spaces.
1729
     *
1730
     * @param string $class A string containing a classname or several class
1731
     *              names delimited by a single space.
1732
     * @return $this
1733
     */
1734
    public function addExtraClass($class)
1735
    {
1736
        //split at white space
1737
        $classes = preg_split('/\s+/', $class);
1738
        foreach ($classes as $class) {
1739
            //add classes one by one
1740
            $this->extraClasses[$class] = $class;
1741
        }
1742
        return $this;
1743
    }
1744
1745
    /**
1746
     * Remove a CSS-class from the form-container. Multiple class names can
1747
     * be passed through as a space delimited string
1748
     *
1749
     * @param string $class
1750
     * @return $this
1751
     */
1752
    public function removeExtraClass($class)
1753
    {
1754
        //split at white space
1755
        $classes = preg_split('/\s+/', $class);
1756
        foreach ($classes as $class) {
1757
            //unset one by one
1758
            unset($this->extraClasses[$class]);
1759
        }
1760
        return $this;
1761
    }
1762
1763
    public function debug()
1764
    {
1765
        $class = static::class;
1766
        $result = "<h3>$class</h3><ul>";
1767
        foreach ($this->fields as $field) {
1768
            $result .= "<li>$field" . $field->debug() . "</li>";
1769
        }
1770
        $result .= "</ul>";
1771
1772
        if ($this->validator) {
1773
            /** @skipUpgrade */
1774
            $result .= '<h3>'._t(__CLASS__.'.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
0 ignored issues
show
Bug introduced by
The method debug() does not exist on SilverStripe\Forms\Validator. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1774
            $result .= '<h3>'._t(__CLASS__.'.VALIDATOR', 'Validator').'</h3>' . $this->validator->/** @scrutinizer ignore-call */ debug();
Loading history...
1775
        }
1776
1777
        return $result;
1778
    }
1779
1780
    /**
1781
     * Current request handler, build by buildRequestHandler(),
1782
     * accessed by getRequestHandler()
1783
     *
1784
     * @var FormRequestHandler
1785
     */
1786
    protected $requestHandler = null;
1787
1788
    /**
1789
     * Get request handler for this form
1790
     *
1791
     * @return FormRequestHandler
1792
     */
1793
    public function getRequestHandler()
1794
    {
1795
        if (!$this->requestHandler) {
1796
            $this->requestHandler = $this->buildRequestHandler();
1797
        }
1798
        return $this->requestHandler;
1799
    }
1800
1801
    /**
1802
     * Assign a specific request handler for this form
1803
     *
1804
     * @param FormRequestHandler $handler
1805
     * @return $this
1806
     */
1807
    public function setRequestHandler(FormRequestHandler $handler)
1808
    {
1809
        $this->requestHandler = $handler;
1810
        return $this;
1811
    }
1812
1813
    /**
1814
     * Scaffold new request handler for this form
1815
     *
1816
     * @return FormRequestHandler
1817
     */
1818
    protected function buildRequestHandler()
1819
    {
1820
        return FormRequestHandler::create($this);
0 ignored issues
show
Bug introduced by
$this of type SilverStripe\Forms\Form is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1820
        return FormRequestHandler::create(/** @scrutinizer ignore-type */ $this);
Loading history...
1821
    }
1822
}
1823