Passed
Push — 4 ( cd0765...fc349d )
by
unknown
08:35
created

Form::setValidationResponseCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
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\HTTPRequest;
9
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
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 customization.
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 = [];
204
205
    /**
206
     * @config
207
     * @var array $default_classes The default classes to apply to the Form
208
     */
209
    private static $default_classes = [];
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 = [];
223
224
    /**
225
     * @var array
226
     */
227
    protected $validationExemptActions = [];
228
229
    /**
230
     * @config
231
     * @var array
232
     */
233
    private static $casting = [
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 = $fields ? $fields : FieldList::create();
290
        $actions = $actions ? $actions : FieldList::create();
291
292
        $fields->setForm($this);
293
        $actions->setForm($this);
294
295
        $this->fields = $fields;
296
        $this->actions = $actions;
297
        $this->setController($controller);
298
        $this->setName($name);
299
300
        // Form validation
301
        $this->validator = ($validator) ? $validator : new RequiredFields();
302
        $this->validator->setForm($this);
303
304
        // Form error controls
305
        $this->restoreFormState();
306
307
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
308
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
309
        if (ClassInfo::hasMethod($controller, 'securityTokenEnabled')) {
310
            $securityEnabled = $controller->securityTokenEnabled();
0 ignored issues
show
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

310
            /** @scrutinizer ignore-call */ 
311
            $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...
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

310
            /** @scrutinizer ignore-call */ 
311
            $securityEnabled = $controller->securityTokenEnabled();
Loading history...
311
        } else {
312
            $securityEnabled = SecurityToken::is_enabled();
313
        }
314
315
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
316
317
        $this->setupDefaultClasses();
318
    }
319
320
    /**
321
     * @return bool
322
     */
323
    public function getNotifyUnsavedChanges()
324
    {
325
        return $this->notifyUnsavedChanges;
326
    }
327
328
    /**
329
     * @param bool $flag
330
     */
331
    public function setNotifyUnsavedChanges($flag)
332
    {
333
        $this->notifyUnsavedChanges = $flag;
334
    }
335
336
    /**
337
     * Load form state from session state
338
     *
339
     * @return $this
340
     */
341
    public function restoreFormState()
342
    {
343
        // Restore messages
344
        $result = $this->getSessionValidationResult();
345
        if (isset($result)) {
346
            $this->loadMessagesFrom($result);
347
        }
348
349
        // load data in from previous submission upon error
350
        $data = $this->getSessionData();
351
        if (isset($data)) {
352
            $this->loadDataFrom($data, self::MERGE_AS_INTERNAL_VALUE);
353
        }
354
        return $this;
355
    }
356
357
    /**
358
     * Flush persistant form state details
359
     *
360
     * @return $this
361
     */
362
    public function clearFormState()
363
    {
364
        $this
365
            ->getSession()
366
            ->clear("FormInfo.{$this->FormName()}.result")
367
            ->clear("FormInfo.{$this->FormName()}.data");
368
        return $this;
369
    }
370
371
    /**
372
     * Helper to get current request for this form
373
     *
374
     * @return HTTPRequest
375
     */
376
    protected function getRequest()
377
    {
378
        // Check if current request handler has a request object
379
        $controller = $this->getController();
380
        if ($controller && !($controller->getRequest() instanceof NullHTTPRequest)) {
381
            return $controller->getRequest();
382
        }
383
        // Fall back to current controller
384
        if (Controller::has_curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
385
            return Controller::curr()->getRequest();
386
        }
387
        return null;
388
    }
389
390
    /**
391
     * Get session for this form
392
     *
393
     * @return Session
394
     */
395
    protected function getSession()
396
    {
397
        $request = $this->getRequest();
398
        if ($request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
399
            return $request->getSession();
400
        }
401
        throw new BadMethodCallException("Session not available in the current context");
402
    }
403
404
    /**
405
     * Return any form data stored in the session
406
     *
407
     * @return array
408
     */
409
    public function getSessionData()
410
    {
411
        return $this->getSession()->get("FormInfo.{$this->FormName()}.data");
412
    }
413
414
    /**
415
     * Store the given form data in the session
416
     *
417
     * @param array $data
418
     * @return $this
419
     */
420
    public function setSessionData($data)
421
    {
422
        $this->getSession()->set("FormInfo.{$this->FormName()}.data", $data);
423
        return $this;
424
    }
425
426
    /**
427
     * Return any ValidationResult instance stored for this object
428
     *
429
     * @return ValidationResult The ValidationResult object stored in the session
430
     */
431
    public function getSessionValidationResult()
432
    {
433
        $resultData = $this->getSession()->get("FormInfo.{$this->FormName()}.result");
434
        if (isset($resultData)) {
435
            return unserialize($resultData);
436
        }
437
        return null;
438
    }
439
440
    /**
441
     * Sets the ValidationResult in the session to be used with the next view of this form.
442
     * @param ValidationResult $result The result to save
443
     * @param bool $combineWithExisting If true, then this will be added to the existing result.
444
     * @return $this
445
     */
446
    public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
447
    {
448
        // Combine with existing result
449
        if ($combineWithExisting) {
450
            $existingResult = $this->getSessionValidationResult();
451
            if ($existingResult) {
0 ignored issues
show
introduced by
$existingResult is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
452
                if ($result) {
0 ignored issues
show
introduced by
$result is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
453
                    $existingResult->combineAnd($result);
454
                } else {
455
                    $result = $existingResult;
456
                }
457
            }
458
        }
459
460
        // Serialise
461
        $resultData = $result ? serialize($result) : null;
0 ignored issues
show
introduced by
$result is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
462
        $this->getSession()->set("FormInfo.{$this->FormName()}.result", $resultData);
463
        return $this;
464
    }
465
466
    /**
467
     * Clear form message (and in session)
468
     *
469
     * @return $this
470
     */
471
    public function clearMessage()
472
    {
473
        $this->setMessage(null);
474
        $this->clearFormState();
475
        return $this;
476
    }
477
478
    /**
479
     * Populate this form with messages from the given ValidationResult.
480
     * Note: This will not clear any pre-existing messages
481
     *
482
     * @param ValidationResult $result
483
     * @return $this
484
     */
485
    public function loadMessagesFrom($result)
486
    {
487
        // Set message on either a field or the parent form
488
        foreach ($result->getMessages() as $message) {
489
            $fieldName = $message['fieldName'];
490
491
            if ($fieldName) {
492
                $owner = $this->fields->dataFieldByName($fieldName) ?: $this;
493
            } else {
494
                $owner = $this;
495
            }
496
497
            $owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
498
        }
499
        return $this;
500
    }
501
502
    /**
503
     * Set message on a given field name. This message will not persist via redirect.
504
     *
505
     * @param string $fieldName
506
     * @param string $message
507
     * @param string $messageType
508
     * @param string $messageCast
509
     * @return $this
510
     */
511
    public function setFieldMessage(
512
        $fieldName,
513
        $message,
514
        $messageType = ValidationResult::TYPE_ERROR,
515
        $messageCast = ValidationResult::CAST_TEXT
516
    ) {
517
        $field = $this->fields->dataFieldByName($fieldName);
518
        if ($field) {
519
            $field->setMessage($message, $messageType, $messageCast);
520
        }
521
        return $this;
522
    }
523
524
    public function castingHelper($field)
525
    {
526
        // Override casting for field message
527
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
528
            return $helper;
529
        }
530
        return parent::castingHelper($field);
531
    }
532
533
    /**
534
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
535
     * after instantiation
536
     */
537
    protected function setupDefaultClasses()
538
    {
539
        $defaultClasses = self::config()->get('default_classes');
540
        if ($defaultClasses) {
541
            foreach ($defaultClasses as $class) {
542
                $this->addExtraClass($class);
543
            }
544
        }
545
    }
546
547
    /**
548
     * @return callable
549
     */
550
    public function getValidationResponseCallback()
551
    {
552
        return $this->validationResponseCallback;
553
    }
554
555
    /**
556
     * Overrules validation error behaviour in {@link httpSubmission()}
557
     * when validation has failed. Useful for optional handling of a certain accepted content type.
558
     *
559
     * The callback can opt out of handling specific responses by returning NULL,
560
     * in which case the default form behaviour will kick in.
561
     *
562
     * @param $callback
563
     * @return self
564
     */
565
    public function setValidationResponseCallback($callback)
566
    {
567
        $this->validationResponseCallback = $callback;
568
569
        return $this;
570
    }
571
572
    /**
573
     * Convert this form into a readonly form
574
     *
575
     * @return $this
576
     */
577
    public function makeReadonly()
578
    {
579
        $this->transform(new ReadonlyTransformation());
580
        return $this;
581
    }
582
583
    /**
584
     * Set whether the user should be redirected back down to the
585
     * form on the page upon validation errors in the form or if
586
     * they just need to redirect back to the page
587
     *
588
     * @param bool $bool Redirect to form on error?
589
     * @return $this
590
     */
591
    public function setRedirectToFormOnValidationError($bool)
592
    {
593
        $this->redirectToFormOnValidationError = $bool;
594
        return $this;
595
    }
596
597
    /**
598
     * Get whether the user should be redirected back down to the
599
     * form on the page upon validation errors
600
     *
601
     * @return bool
602
     */
603
    public function getRedirectToFormOnValidationError()
604
    {
605
        return $this->redirectToFormOnValidationError;
606
    }
607
608
    /**
609
     * @param FormTransformation $trans
610
     */
611
    public function transform(FormTransformation $trans)
612
    {
613
        $newFields = new FieldList();
614
        foreach ($this->fields as $field) {
615
            $newFields->push($field->transform($trans));
616
        }
617
        $this->fields = $newFields;
618
619
        $newActions = new FieldList();
620
        foreach ($this->actions as $action) {
621
            $newActions->push($action->transform($trans));
622
        }
623
        $this->actions = $newActions;
624
625
626
        // We have to remove validation, if the fields are not editable ;-)
627
        if ($this->validator) {
628
            $this->validator->removeValidation();
629
        }
630
    }
631
632
    /**
633
     * Get the {@link Validator} attached to this form.
634
     * @return Validator
635
     */
636
    public function getValidator()
637
    {
638
        return $this->validator;
639
    }
640
641
    /**
642
     * Set the {@link Validator} on this form.
643
     * @param Validator $validator
644
     * @return $this
645
     */
646
    public function setValidator(Validator $validator)
647
    {
648
        if ($validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
649
            $this->validator = $validator;
650
            $this->validator->setForm($this);
651
        }
652
        return $this;
653
    }
654
655
    /**
656
     * Remove the {@link Validator} from this from.
657
     */
658
    public function unsetValidator()
659
    {
660
        $this->validator = null;
661
        return $this;
662
    }
663
664
    /**
665
     * Set actions that are exempt from validation
666
     *
667
     * @param array $actions
668
     * @return $this
669
     */
670
    public function setValidationExemptActions($actions)
671
    {
672
        $this->validationExemptActions = $actions;
673
        return $this;
674
    }
675
676
    /**
677
     * Get a list of actions that are exempt from validation
678
     *
679
     * @return array
680
     */
681
    public function getValidationExemptActions()
682
    {
683
        return $this->validationExemptActions;
684
    }
685
686
    /**
687
     * Passed a FormAction, returns true if that action is exempt from Form validation
688
     *
689
     * @param FormAction $action
690
     * @return bool
691
     */
692
    public function actionIsValidationExempt($action)
693
    {
694
        // Non-actions don't bypass validation
695
        if (!$action) {
0 ignored issues
show
introduced by
$action is of type SilverStripe\Forms\FormAction, thus it always evaluated to true.
Loading history...
696
            return false;
697
        }
698
        if ($action->getValidationExempt()) {
699
            return true;
700
        }
701
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
702
            return true;
703
        }
704
        return false;
705
    }
706
707
    /**
708
     * Generate extra special fields - namely the security token field (if required).
709
     *
710
     * @return FieldList
711
     */
712
    public function getExtraFields()
713
    {
714
        $extraFields = new FieldList();
715
716
        $token = $this->getSecurityToken();
717
        if ($token) {
718
            $tokenField = $token->updateFieldSet($this->fields);
719
            if ($tokenField) {
720
                $tokenField->setForm($this);
721
            }
722
        }
723
        $this->securityTokenAdded = true;
724
725
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
726
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
727
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
728
            $methodField->setForm($this);
729
            $extraFields->push($methodField);
730
        }
731
732
        return $extraFields;
733
    }
734
735
    /**
736
     * Return the form's fields - used by the templates
737
     *
738
     * @return FieldList The form fields
739
     */
740
    public function Fields()
741
    {
742
        foreach ($this->getExtraFields() as $field) {
743
            if (!$this->fields->fieldByName($field->getName())) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fields->fieldByName($field->getName()) targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
744
                $this->fields->push($field);
745
            }
746
        }
747
748
        return $this->fields;
749
    }
750
751
    /**
752
     * Return all <input type="hidden"> fields
753
     * in a form - including fields nested in {@link CompositeFields}.
754
     * Useful when doing custom field layouts.
755
     *
756
     * @return FieldList
757
     */
758
    public function HiddenFields()
759
    {
760
        return $this->Fields()->HiddenFields();
761
    }
762
763
    /**
764
     * Return all fields except for the hidden fields.
765
     * Useful when making your own simplified form layouts.
766
     */
767
    public function VisibleFields()
768
    {
769
        return $this->Fields()->VisibleFields();
770
    }
771
772
    /**
773
     * Setter for the form fields.
774
     *
775
     * @param FieldList $fields
776
     * @return $this
777
     */
778
    public function setFields($fields)
779
    {
780
        $fields->setForm($this);
781
        $this->fields = $fields;
782
783
        return $this;
784
    }
785
786
    /**
787
     * Return the form's action buttons - used by the templates
788
     *
789
     * @return FieldList The action list
790
     */
791
    public function Actions()
792
    {
793
        return $this->actions;
794
    }
795
796
    /**
797
     * Setter for the form actions.
798
     *
799
     * @param FieldList $actions
800
     * @return $this
801
     */
802
    public function setActions($actions)
803
    {
804
        $actions->setForm($this);
805
        $this->actions = $actions;
806
807
        return $this;
808
    }
809
810
    /**
811
     * Unset all form actions
812
     */
813
    public function unsetAllActions()
814
    {
815
        $this->actions = new FieldList();
816
        return $this;
817
    }
818
819
    /**
820
     * @param string $name
821
     * @param string $value
822
     * @return $this
823
     */
824
    public function setAttribute($name, $value)
825
    {
826
        $this->attributes[$name] = $value;
827
        return $this;
828
    }
829
830
    /**
831
     * @param string $name
832
     * @return string
833
     */
834
    public function getAttribute($name)
835
    {
836
        if (isset($this->attributes[$name])) {
837
            return $this->attributes[$name];
838
        }
839
        return null;
840
    }
841
842
    /**
843
     * @return array
844
     */
845
    public function getAttributes()
846
    {
847
        $attrs = [
848
            'id' => $this->FormName(),
849
            'action' => $this->FormAction(),
850
            'method' => $this->FormMethod(),
851
            'enctype' => $this->getEncType(),
852
            'target' => $this->target,
853
            'class' => $this->extraClass(),
854
        ];
855
856
        if ($this->validator && $this->validator->getErrors()) {
857
            if (!isset($attrs['class'])) {
858
                $attrs['class'] = '';
859
            }
860
            $attrs['class'] .= ' validationerror';
861
        }
862
863
        $attrs = array_merge($attrs, $this->attributes);
864
865
        return $attrs;
866
    }
867
868
    /**
869
     * Return the attributes of the form tag - used by the templates.
870
     *
871
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
872
     * If at least one argument is passed as a string, all arguments act as excludes by name.
873
     *
874
     * @return string HTML attributes, ready for insertion into an HTML tag
875
     */
876
    public function getAttributesHTML($attrs = null)
877
    {
878
        $exclude = (is_string($attrs)) ? func_get_args() : null;
879
880
        $attrs = $this->getAttributes();
881
882
        // Remove empty
883
        $attrs = array_filter((array)$attrs, function ($value) {
884
            return ($value || $value === 0);
885
        });
886
887
        // Remove excluded
888
        if ($exclude) {
889
            $attrs = array_diff_key($attrs, array_flip($exclude));
890
        }
891
892
        // Prepare HTML-friendly 'method' attribute (lower-case)
893
        if (isset($attrs['method'])) {
894
            $attrs['method'] = strtolower($attrs['method']);
895
        }
896
897
        // Create markup
898
        $parts = [];
899
        foreach ($attrs as $name => $value) {
900
            if ($value === true) {
901
                $value = $name;
902
            }
903
904
            $parts[] = sprintf('%s="%s"', Convert::raw2att($name), Convert::raw2att($value));
0 ignored issues
show
Bug introduced by
It seems like SilverStripe\Core\Convert::raw2att($name) can also be of type array and array; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

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

904
            $parts[] = sprintf('%s="%s"', /** @scrutinizer ignore-type */ Convert::raw2att($name), Convert::raw2att($value));
Loading history...
905
        }
906
907
        return implode(' ', $parts);
908
    }
909
910
    public function FormAttributes()
911
    {
912
        return $this->getAttributesHTML();
913
    }
914
915
    /**
916
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
917
     * another frame
918
    *
919
     * @param string|FormTemplateHelper $helper
920
    */
921
    public function setTemplateHelper($helper)
922
    {
923
        $this->templateHelper = $helper;
0 ignored issues
show
Documentation Bug introduced by
It seems like $helper can also be of type string. However, the property $templateHelper is declared as type SilverStripe\Forms\FormTemplateHelper. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
924
    }
925
926
    /**
927
     * Return a {@link FormTemplateHelper} for this form. If one has not been
928
     * set, return the default helper.
929
     *
930
     * @return FormTemplateHelper
931
     */
932
    public function getTemplateHelper()
933
    {
934
        if ($this->templateHelper) {
935
            if (is_string($this->templateHelper)) {
0 ignored issues
show
introduced by
The condition is_string($this->templateHelper) is always false.
Loading history...
936
                return Injector::inst()->get($this->templateHelper);
937
            }
938
939
            return $this->templateHelper;
940
        }
941
942
        return FormTemplateHelper::singleton();
943
    }
944
945
    /**
946
     * Set the target of this form to any value - useful for opening the form
947
     * contents in a new window or refreshing another frame.
948
     *
949
     * @param string $target The value of the target
950
     * @return $this
951
     */
952
    public function setTarget($target)
953
    {
954
        $this->target = $target;
955
956
        return $this;
957
    }
958
959
    /**
960
     * Set the legend value to be inserted into
961
     * the <legend> element in the Form.ss template.
962
     * @param string $legend
963
     * @return $this
964
     */
965
    public function setLegend($legend)
966
    {
967
        $this->legend = $legend;
968
        return $this;
969
    }
970
971
    /**
972
     * Set the SS template that this form should use
973
     * to render with. The default is "Form".
974
     *
975
     * @param string|array $template The name of the template (without the .ss extension) or array form
976
     * @return $this
977
     */
978
    public function setTemplate($template)
979
    {
980
        $this->template = $template;
981
        return $this;
982
    }
983
984
    /**
985
     * Return the template to render this form with.
986
     *
987
     * @return string|array
988
     */
989
    public function getTemplate()
990
    {
991
        return $this->template;
992
    }
993
994
    /**
995
     * Returs the ordered list of preferred templates for rendering this form
996
     * If the template isn't set, then default to the
997
     * form class name e.g "Form".
998
     *
999
     * @return array
1000
     */
1001
    public function getTemplates()
1002
    {
1003
        $templates = SSViewer::get_templates_by_class(static::class, '', __CLASS__);
1004
        // Prefer any custom template
1005
        if ($this->getTemplate()) {
1006
            array_unshift($templates, $this->getTemplate());
1007
        }
1008
        return $templates;
1009
    }
1010
1011
    /**
1012
     * Returns the encoding type for the form.
1013
     *
1014
     * By default this will be URL encoded, unless there is a file field present
1015
     * in which case multipart is used. You can also set the enc type using
1016
     * {@link setEncType}.
1017
     */
1018
    public function getEncType()
1019
    {
1020
        if ($this->encType) {
1021
            return $this->encType;
1022
        }
1023
1024
        if ($fields = $this->fields->dataFields()) {
1025
            foreach ($fields as $field) {
1026
                if ($field instanceof FileField) {
1027
                    return self::ENC_TYPE_MULTIPART;
1028
                }
1029
            }
1030
        }
1031
1032
        return self::ENC_TYPE_URLENCODED;
1033
    }
1034
1035
    /**
1036
     * Sets the form encoding type. The most common encoding types are defined
1037
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1038
     *
1039
     * @param string $encType
1040
     * @return $this
1041
     */
1042
    public function setEncType($encType)
1043
    {
1044
        $this->encType = $encType;
1045
        return $this;
1046
    }
1047
1048
    /**
1049
     * Returns the real HTTP method for the form:
1050
     * GET, POST, PUT, DELETE or HEAD.
1051
     * As most browsers only support GET and POST in
1052
     * form submissions, all other HTTP methods are
1053
     * added as a hidden field "_method" that
1054
     * gets evaluated in {@link HTTPRequest::detect_method()}.
1055
     * See {@link FormMethod()} to get a HTTP method
1056
     * for safe insertion into a <form> tag.
1057
     *
1058
     * @return string HTTP method
1059
     */
1060
    public function FormHttpMethod()
1061
    {
1062
        return $this->formMethod;
1063
    }
1064
1065
    /**
1066
     * Returns the form method to be used in the <form> tag.
1067
     * See {@link FormHttpMethod()} to get the "real" method.
1068
     *
1069
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1070
     */
1071
    public function FormMethod()
1072
    {
1073
        if (in_array($this->formMethod, ['GET','POST'])) {
1074
            return $this->formMethod;
1075
        } else {
1076
            return 'POST';
1077
        }
1078
    }
1079
1080
    /**
1081
     * Set the form method: GET, POST, PUT, DELETE.
1082
     *
1083
     * @param string $method
1084
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1085
     * @return $this
1086
     */
1087
    public function setFormMethod($method, $strict = null)
1088
    {
1089
        $this->formMethod = strtoupper($method);
1090
        if ($strict !== null) {
1091
            $this->setStrictFormMethodCheck($strict);
1092
        }
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * If set to true (the default), enforces the matching of the form method.
1098
     *
1099
     * This will mean two things:
1100
     *  - GET vars will be ignored by a POST form, and vice versa
1101
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1102
     *
1103
     * If set to false then the form method is only used to construct the default
1104
     * form.
1105
     *
1106
     * @param $bool boolean
1107
     * @return $this
1108
     */
1109
    public function setStrictFormMethodCheck($bool)
1110
    {
1111
        $this->strictFormMethodCheck = (bool)$bool;
1112
        return $this;
1113
    }
1114
1115
    /**
1116
     * @return boolean
1117
     */
1118
    public function getStrictFormMethodCheck()
1119
    {
1120
        return $this->strictFormMethodCheck;
1121
    }
1122
1123
    /**
1124
     * Return the form's action attribute.
1125
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1126
     *
1127
     * @return string
1128
     */
1129
    public function FormAction()
1130
    {
1131
        if ($this->formActionPath) {
1132
            return $this->formActionPath;
1133
        }
1134
1135
        // Get action from request handler link
1136
        return $this->getRequestHandler()->Link();
1137
    }
1138
1139
    /**
1140
     * Set the form action attribute to a custom URL.
1141
     *
1142
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1143
     * recommended only for situations where you have two relatively distinct
1144
     * parts of the system trying to communicate via a form post.
1145
     *
1146
     * @param string $path
1147
     * @return $this
1148
     */
1149
    public function setFormAction($path)
1150
    {
1151
        $this->formActionPath = $path;
1152
1153
        return $this;
1154
    }
1155
1156
    /**
1157
     * Returns the name of the form.
1158
     *
1159
     * @return string
1160
     */
1161
    public function FormName()
1162
    {
1163
        return $this->getTemplateHelper()->generateFormID($this);
1164
    }
1165
1166
    /**
1167
     * Set the HTML ID attribute of the form.
1168
     *
1169
     * @param string $id
1170
     * @return $this
1171
     */
1172
    public function setHTMLID($id)
1173
    {
1174
        $this->htmlID = $id;
1175
1176
        return $this;
1177
    }
1178
1179
    /**
1180
     * @return string
1181
     */
1182
    public function getHTMLID()
1183
    {
1184
        return $this->htmlID;
1185
    }
1186
1187
    /**
1188
     * Get the controller or parent request handler.
1189
     *
1190
     * @return RequestHandler
1191
     */
1192
    public function getController()
1193
    {
1194
        return $this->controller;
1195
    }
1196
1197
    /**
1198
     * Set the controller or parent request handler.
1199
     *
1200
     * @param RequestHandler $controller
1201
     * @return $this
1202
     */
1203
    public function setController(RequestHandler $controller = null)
1204
    {
1205
        $this->controller = $controller;
1206
        return $this;
1207
    }
1208
1209
    /**
1210
     * Get the name of the form.
1211
     *
1212
     * @return string
1213
     */
1214
    public function getName()
1215
    {
1216
        return $this->name;
1217
    }
1218
1219
    /**
1220
     * Set the name of the form.
1221
     *
1222
     * @param string $name
1223
     * @return Form
1224
     */
1225
    public function setName($name)
1226
    {
1227
        $this->name = $name;
1228
1229
        return $this;
1230
    }
1231
1232
    /**
1233
     * Returns an object where there is a method with the same name as each data
1234
     * field on the form.
1235
     *
1236
     * That method will return the field itself.
1237
     *
1238
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1239
     */
1240
    public function FieldMap()
1241
    {
1242
        return new Form_FieldMap($this);
1243
    }
1244
1245
    /**
1246
     * Set a message to the session, for display next time this form is shown.
1247
     *
1248
     * @param string $message the text of the message
1249
     * @param string $type Should be set to good, bad, or warning.
1250
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1251
     * Bool values will be treated as plain text flag.
1252
     */
1253
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1254
    {
1255
        $this->setMessage($message, $type, $cast);
1256
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1257
        $result->addMessage($message, $type, null, $cast);
1258
        $this->setSessionValidationResult($result);
1259
    }
1260
1261
    /**
1262
     * Set an error to the session, for display next time this form is shown.
1263
     *
1264
     * @param string $message the text of the message
1265
     * @param string $type Should be set to good, bad, or warning.
1266
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1267
     * Bool values will be treated as plain text flag.
1268
     */
1269
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1270
    {
1271
        $this->setMessage($message, $type, $cast);
1272
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1273
        $result->addError($message, $type, null, $cast);
1274
        $this->setSessionValidationResult($result);
1275
    }
1276
1277
    /**
1278
     * Set an error message for a field in the session, for display next time this form is shown.
1279
     *
1280
     * @param string $message the text of the message
1281
     * @param string $fieldName Name of the field to set the error message on it.
1282
     * @param string $type Should be set to good, bad, or warning.
1283
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1284
     * Bool values will be treated as plain text flag.
1285
     */
1286
    public function sessionFieldError($message, $fieldName, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1287
    {
1288
        $this->setMessage($message, $type, $cast);
1289
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1290
        $result->addFieldMessage($fieldName, $message, $type, null, $cast);
1291
        $this->setSessionValidationResult($result);
1292
    }
1293
1294
    /**
1295
     * Returns the DataObject that has given this form its data
1296
     * through {@link loadDataFrom()}.
1297
     *
1298
     * @return DataObject
1299
     */
1300
    public function getRecord()
1301
    {
1302
        return $this->record;
1303
    }
1304
1305
    /**
1306
     * Get the legend value to be inserted into the
1307
     * <legend> element in Form.ss
1308
     *
1309
     * @return string
1310
     */
1311
    public function getLegend()
1312
    {
1313
        return $this->legend;
1314
    }
1315
1316
    /**
1317
     * Processing that occurs before a form is executed.
1318
     *
1319
     * This includes form validation, if it fails, we throw a ValidationException
1320
     *
1321
     * This includes form validation, if it fails, we redirect back
1322
     * to the form with appropriate error messages.
1323
     * Always return true if the current form action is exempt from validation
1324
     *
1325
     * Triggered through {@link httpSubmission()}.
1326
     *
1327
     *
1328
     * Note that CSRF protection takes place in {@link httpSubmission()},
1329
     * if it fails the form data will never reach this method.
1330
     *
1331
     * @return ValidationResult
1332
     */
1333
    public function validationResult()
1334
    {
1335
        // Automatically pass if there is no validator, or the clicked button is exempt
1336
        // Note: Soft support here for validation with absent request handler
1337
        $handler = $this->getRequestHandler();
1338
        $action = $handler ? $handler->buttonClicked() : null;
0 ignored issues
show
introduced by
$handler is of type SilverStripe\Forms\FormRequestHandler, thus it always evaluated to true.
Loading history...
1339
        $validator = $this->getValidator();
1340
        if (!$validator || $this->actionIsValidationExempt($action)) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
1341
            return ValidationResult::create();
1342
        }
1343
1344
        // Invoke validator
1345
        $result = $validator->validate();
1346
        $this->loadMessagesFrom($result);
1347
        return $result;
1348
    }
1349
1350
    const MERGE_DEFAULT             = 0b0000;
1351
    const MERGE_CLEAR_MISSING       = 0b0001;
1352
    const MERGE_IGNORE_FALSEISH     = 0b0010;
1353
    const MERGE_AS_INTERNAL_VALUE   = 0b0100;
1354
    const MERGE_AS_SUBMITTED_VALUE  = 0b1000;
1355
1356
    /**
1357
     * Load data from the given DataObject or array.
1358
     *
1359
     * It will call $object->MyField to get the value of MyField.
1360
     * If you passed an array, it will call $object[MyField].
1361
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1362
     * as determined by {@link FieldList->dataFields()}.
1363
     *
1364
     * By default, if a field isn't set (as determined by isset()),
1365
     * its value will not be saved to the field, retaining
1366
     * potential existing values.
1367
     *
1368
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1369
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1370
     *
1371
     * Escaping happens automatically on saving the data through
1372
     * {@link saveInto()}.
1373
     *
1374
     * @uses FieldList::dataFields()
1375
     * @uses FormField::setSubmittedValue()
1376
     * @uses FormField::setValue()
1377
     *
1378
     * @param array|DataObject $data
1379
     * @param int $mergeStrategy
1380
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1381
     *  what that property/key's value is.
1382
     *
1383
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1384
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1385
     *  "left alone", meaning they retain any previous value.
1386
     *
1387
     *  You can pass a bitmask here to change this behaviour.
1388
     *
1389
     *  Passing MERGE_CLEAR_MISSING means that any fields that don't match any property/key in
1390
     *  {@link $data} are cleared.
1391
     *
1392
     *  Passing MERGE_IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1393
     *  a field's value.
1394
     *
1395
     *  Passing MERGE_AS_INTERNAL_VALUE forces the data to be parsed using the internal representation of the matching
1396
     *  form field. This is helpful if you are loading an array of values retrieved from `Form::getData()` and you
1397
     *  do not want them parsed as submitted data. MERGE_AS_SUBMITTED_VALUE does the opposite and forces the data to be
1398
     *  parsed as it would be submitted from a form.
1399
     *
1400
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1401
     *  MERGE_CLEAR_MISSING
1402
     *
1403
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1404
     * form that has some fields that save to one object, and some that save to another.
1405
     * @return $this
1406
     */
1407
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1408
    {
1409
        if (!is_object($data) && !is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
1410
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1411
            return $this;
1412
        }
1413
1414
        // Handle the backwards compatible case of passing "true" as the second argument
1415
        if ($mergeStrategy === true) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === true is always false.
Loading history...
1416
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1417
        } elseif ($mergeStrategy === false) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === false is always false.
Loading history...
1418
            $mergeStrategy = 0;
1419
        }
1420
1421
        // If an object is passed, save it for historical reference through {@link getRecord()}
1422
        // Also use this to determine if we are loading a submitted form, or loading
1423
        // from a dataobject
1424
        $submitted = true;
1425
        if (is_object($data)) {
1426
            $this->record = $data;
1427
            $submitted = false;
1428
        }
1429
1430
        // Using the `MERGE_AS_INTERNAL_VALUE` or `MERGE_AS_SUBMITTED_VALUE` flags users can explicitly specify which
1431
        // `setValue` method to use.
1432
        if (($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) == self::MERGE_AS_INTERNAL_VALUE) {
1433
            $submitted = false;
1434
        } elseif (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) {
1435
            $submitted = true;
1436
        }
1437
1438
        // Don't include fields without data
1439
        $dataFields = $this->Fields()->dataFields();
1440
1441
        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...
1442
            return $this;
1443
        }
1444
1445
        /** @var FormField $field */
1446
        foreach ($dataFields as $field) {
1447
            $name = $field->getName();
1448
1449
            // Skip fields that have been excluded
1450
            if ($fieldList && !in_array($name, $fieldList)) {
1451
                continue;
1452
            }
1453
1454
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1455
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1456
                continue;
1457
            }
1458
1459
            // Does this property exist on $data?
1460
            $exists = false;
1461
            // The value from $data for this field
1462
            $val = null;
1463
1464
            if (is_object($data)) {
1465
                // Allow dot-syntax traversal of has-one relations fields
1466
                if (strpos($name, '.') !== false) {
1467
                    $exists = (
1468
                        $data->hasMethod('relField')
1469
                    );
1470
                    try {
1471
                        $val = $data->relField($name);
1472
                    } catch (\LogicException $e) {
1473
                        // There's no other way to tell whether the relation actually exists
1474
                        $exists = false;
1475
                    }
1476
                // Regular ViewableData access
1477
                } else {
1478
                    $exists = (
1479
                        isset($data->$name) ||
1480
                        $data->hasMethod($name) ||
1481
                        ($data->hasMethod('hasField') && $data->hasField($name))
1482
                    );
1483
1484
                    if ($exists) {
1485
                        $val = $data->__get($name);
1486
                    }
1487
                }
1488
1489
            // Regular array access. Note that dot-syntax not supported here
1490
            } elseif (is_array($data)) {
1491
                if (array_key_exists($name, $data)) {
1492
                    $exists = true;
1493
                    $val = $data[$name];
1494
                // PHP turns the '.'s in POST vars into '_'s
1495
                } elseif (array_key_exists($altName = str_replace('.', '_', $name), $data)) {
1496
                    $exists = true;
1497
                    $val = $data[$altName];
1498
                } elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1499
                    // If field is in array-notation we need to access nested data
1500
                    //discard first match which is just the whole string
1501
                    array_shift($matches);
1502
                    $keys = array_pop($matches);
1503
                    $name = array_shift($matches);
1504
                    $name = array_shift($name);
1505
                    if (array_key_exists($name, $data)) {
1506
                        $tmpData = &$data[$name];
1507
                        // drill down into the data array looking for the corresponding value
1508
                        foreach ($keys as $arrayKey) {
1509
                            if ($tmpData && $arrayKey !== '') {
1510
                                $tmpData = &$tmpData[$arrayKey];
1511
                            } else {
1512
                                //empty square brackets means new array
1513
                                if (is_array($tmpData)) {
1514
                                    $tmpData = array_shift($tmpData);
1515
                                }
1516
                            }
1517
                        }
1518
                        if ($tmpData) {
1519
                            $val = $tmpData;
1520
                            $exists = true;
1521
                        }
1522
                    }
1523
                }
1524
            }
1525
1526
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1527
            $setValue = false;
1528
            if ($exists) {
1529
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1530
                    $setValue = true;
1531
                }
1532
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1533
                $setValue = true;
1534
            }
1535
1536
            // pass original data as well so composite fields can act on the additional information
1537
            if ($setValue) {
1538
                if ($submitted) {
1539
                    $field->setSubmittedValue($val, $data);
1540
                } else {
1541
                    $field->setValue($val, $data);
1542
                }
1543
            }
1544
        }
1545
        return $this;
1546
    }
1547
1548
    /**
1549
     * Save the contents of this form into the given data object.
1550
     * It will make use of setCastedField() to do this.
1551
     *
1552
     * @param DataObjectInterface $dataObject The object to save data into
1553
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1554
     * form that has some fields that save to one object, and some that save to another.
1555
     */
1556
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1557
    {
1558
        $dataFields = $this->fields->saveableFields();
1559
        $lastField = null;
1560
        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...
1561
            foreach ($dataFields as $field) {
1562
            // Skip fields that have been excluded
1563
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1564
                    continue;
1565
                }
1566
1567
                $saveMethod = "save{$field->getName()}";
1568
                if ($field->getName() == "ClassName") {
1569
                    $lastField = $field;
1570
                } elseif ($dataObject->hasMethod($saveMethod)) {
0 ignored issues
show
Bug introduced by
The method hasMethod() does not exist on SilverStripe\ORM\DataObjectInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\DataObjectInterface. ( Ignorable by Annotation )

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

1570
                } elseif ($dataObject->/** @scrutinizer ignore-call */ hasMethod($saveMethod)) {
Loading history...
1571
                    $dataObject->$saveMethod($field->dataValue());
1572
                } elseif ($field->getName() !== "ID") {
1573
                    $field->saveInto($dataObject);
1574
                }
1575
            }
1576
        }
1577
        if ($lastField) {
1578
            $lastField->saveInto($dataObject);
1579
        }
1580
    }
1581
1582
    /**
1583
     * Get the submitted data from this form through
1584
     * {@link FieldList->dataFields()}, which filters out
1585
     * any form-specific data like form-actions.
1586
     * Calls {@link FormField->dataValue()} on each field,
1587
     * which returns a value suitable for insertion into a DataObject
1588
     * property.
1589
     *
1590
     * @return array
1591
     */
1592
    public function getData()
1593
    {
1594
        $dataFields = $this->fields->dataFields();
1595
        $data = [];
1596
1597
        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...
1598
            /** @var FormField $field */
1599
            foreach ($dataFields as $field) {
1600
                if ($field->getName()) {
1601
                    $data[$field->getName()] = $field->dataValue();
1602
                }
1603
            }
1604
        }
1605
1606
        return $data;
1607
    }
1608
1609
    /**
1610
     * Return a rendered version of this form.
1611
     *
1612
     * This is returned when you access a form as $FormObject rather
1613
     * than <% with FormObject %>
1614
     *
1615
     * @return DBHTMLText
1616
     */
1617
    public function forTemplate()
1618
    {
1619
        if (!$this->canBeCached()) {
1620
            HTTPCacheControlMiddleware::singleton()->disableCache();
1621
        }
1622
1623
        $return = $this->renderWith($this->getTemplates());
1624
1625
        // Now that we're rendered, clear message
1626
        $this->clearMessage();
1627
1628
        return $return;
1629
    }
1630
1631
    /**
1632
     * Return a rendered version of this form, suitable for ajax post-back.
1633
     *
1634
     * It triggers slightly different behaviour, such as disabling the rewriting
1635
     * of # links.
1636
     *
1637
     * @return DBHTMLText
1638
     */
1639
    public function forAjaxTemplate()
1640
    {
1641
        $view = SSViewer::create($this->getTemplates());
1642
1643
        $return = $view->dontRewriteHashlinks()->process($this);
1644
1645
        // Now that we're rendered, clear message
1646
        $this->clearMessage();
1647
1648
        return $return;
1649
    }
1650
1651
    /**
1652
     * Returns an HTML rendition of this form, without the <form> tag itself.
1653
     *
1654
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1655
     * and _form_enctype.  These are the attributes of the form.  These fields
1656
     * can be used to send the form to Ajax.
1657
     *
1658
     * @deprecated 5.0
1659
     * @return string
1660
     */
1661
    public function formHtmlContent()
1662
    {
1663
        Deprecation::notice('5.0');
1664
        $this->IncludeFormTag = false;
1665
        $content = $this->forTemplate();
1666
        $this->IncludeFormTag = true;
1667
1668
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
1669
            . " value=\"" . $this->FormAction() . "\" />\n";
1670
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1671
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1672
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1673
1674
        return $content;
1675
    }
1676
1677
    /**
1678
     * Render this form using the given template, and return the result as a string
1679
     * You can pass either an SSViewer or a template name
1680
     * @param string|array $template
1681
     * @return DBHTMLText
1682
     */
1683
    public function renderWithoutActionButton($template)
1684
    {
1685
        $custom = $this->customise([
1686
            "Actions" => "",
1687
        ]);
1688
1689
        if (is_string($template)) {
1690
            $template = SSViewer::create($template);
1691
        }
1692
1693
        return $template->process($custom);
1694
    }
1695
1696
    /**
1697
     * Return the default button that should be clicked when another one isn't
1698
     * available.
1699
     *
1700
     * @return FormAction
1701
     */
1702
    public function defaultAction()
1703
    {
1704
        if ($this->hasDefaultAction && $this->actions) {
1705
            return $this->actions->first();
1706
        }
1707
        return null;
1708
    }
1709
1710
    /**
1711
     * Disable the default button.
1712
     *
1713
     * Ordinarily, when a form is processed and no action_XXX button is
1714
     * available, then the first button in the actions list will be pressed.
1715
     * However, if this is "delete", for example, this isn't such a good idea.
1716
     *
1717
     * @return Form
1718
     */
1719
    public function disableDefaultAction()
1720
    {
1721
        $this->hasDefaultAction = false;
1722
1723
        return $this;
1724
    }
1725
1726
    /**
1727
     * Disable the requirement of a security token on this form instance. This
1728
     * security protects against CSRF attacks, but you should disable this if
1729
     * you don't want to tie a form to a session - eg a search form.
1730
     *
1731
     * Check for token state with {@link getSecurityToken()} and
1732
     * {@link SecurityToken->isEnabled()}.
1733
     *
1734
     * @return Form
1735
     */
1736
    public function disableSecurityToken()
1737
    {
1738
        $this->securityToken = new NullSecurityToken();
1739
1740
        return $this;
1741
    }
1742
1743
    /**
1744
     * Enable {@link SecurityToken} protection for this form instance.
1745
     *
1746
     * Check for token state with {@link getSecurityToken()} and
1747
     * {@link SecurityToken->isEnabled()}.
1748
     *
1749
     * @return Form
1750
     */
1751
    public function enableSecurityToken()
1752
    {
1753
        $this->securityToken = new SecurityToken();
1754
1755
        return $this;
1756
    }
1757
1758
    /**
1759
     * Returns the security token for this form (if any exists).
1760
     *
1761
     * Doesn't check for {@link securityTokenEnabled()}.
1762
     *
1763
     * Use {@link SecurityToken::inst()} to get a global token.
1764
     *
1765
     * @return SecurityToken|null
1766
     */
1767
    public function getSecurityToken()
1768
    {
1769
        return $this->securityToken;
1770
    }
1771
1772
    /**
1773
     * Compiles all CSS-classes.
1774
     *
1775
     * @return string
1776
     */
1777
    public function extraClass()
1778
    {
1779
        return implode(' ', array_unique($this->extraClasses));
1780
    }
1781
1782
    /**
1783
     * Check if a CSS-class has been added to the form container.
1784
     *
1785
     * @param string $class A string containing a classname or several class
1786
     * names delimited by a single space.
1787
     * @return boolean True if all of the classnames passed in have been added.
1788
     */
1789
    public function hasExtraClass($class)
1790
    {
1791
        //split at white space
1792
        $classes = preg_split('/\s+/', $class);
1793
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1794
            if (!isset($this->extraClasses[$class])) {
1795
                return false;
1796
            }
1797
        }
1798
        return true;
1799
    }
1800
1801
    /**
1802
     * Add a CSS-class to the form-container. If needed, multiple classes can
1803
     * be added by delimiting a string with spaces.
1804
     *
1805
     * @param string $class A string containing a classname or several class
1806
     *              names delimited by a single space.
1807
     * @return $this
1808
     */
1809
    public function addExtraClass($class)
1810
    {
1811
        //split at white space
1812
        $classes = preg_split('/\s+/', $class);
1813
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1814
            //add classes one by one
1815
            $this->extraClasses[$class] = $class;
1816
        }
1817
        return $this;
1818
    }
1819
1820
    /**
1821
     * Remove a CSS-class from the form-container. Multiple class names can
1822
     * be passed through as a space delimited string
1823
     *
1824
     * @param string $class
1825
     * @return $this
1826
     */
1827
    public function removeExtraClass($class)
1828
    {
1829
        //split at white space
1830
        $classes = preg_split('/\s+/', $class);
1831
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1832
            //unset one by one
1833
            unset($this->extraClasses[$class]);
1834
        }
1835
        return $this;
1836
    }
1837
1838
    public function debug()
1839
    {
1840
        $class = static::class;
1841
        $result = "<h3>$class</h3><ul>";
1842
        foreach ($this->fields as $field) {
1843
            $result .= "<li>$field" . $field->debug() . "</li>";
1844
        }
1845
        $result .= "</ul>";
1846
1847
        if ($this->validator) {
1848
            /** @skipUpgrade */
1849
            $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

1849
            $result .= '<h3>' . _t(__CLASS__ . '.VALIDATOR', 'Validator') . '</h3>' . $this->validator->/** @scrutinizer ignore-call */ debug();
Loading history...
1850
        }
1851
1852
        return $result;
1853
    }
1854
1855
    /**
1856
     * Current request handler, build by buildRequestHandler(),
1857
     * accessed by getRequestHandler()
1858
     *
1859
     * @var FormRequestHandler
1860
     */
1861
    protected $requestHandler = null;
1862
1863
    /**
1864
     * Get request handler for this form
1865
     *
1866
     * @return FormRequestHandler
1867
     */
1868
    public function getRequestHandler()
1869
    {
1870
        if (!$this->requestHandler) {
1871
            $this->requestHandler = $this->buildRequestHandler();
1872
        }
1873
        return $this->requestHandler;
1874
    }
1875
1876
    /**
1877
     * Assign a specific request handler for this form
1878
     *
1879
     * @param FormRequestHandler $handler
1880
     * @return $this
1881
     */
1882
    public function setRequestHandler(FormRequestHandler $handler)
1883
    {
1884
        $this->requestHandler = $handler;
1885
        return $this;
1886
    }
1887
1888
    /**
1889
     * Scaffold new request handler for this form
1890
     *
1891
     * @return FormRequestHandler
1892
     */
1893
    protected function buildRequestHandler()
1894
    {
1895
        return FormRequestHandler::create($this);
1896
    }
1897
1898
    /**
1899
     * Can the body of this form be cached?
1900
     *
1901
     * @return bool
1902
     */
1903
    protected function canBeCached()
1904
    {
1905
        if ($this->getSecurityToken()->isEnabled()) {
1906
            return false;
1907
        }
1908
1909
        if ($this->FormMethod() !== 'GET') {
1910
            return false;
1911
        }
1912
1913
        $validator = $this->getValidator();
1914
1915
        if (!$validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
1916
            return true;
1917
        }
1918
1919
        return $validator->canBeCached();
1920
    }
1921
}
1922