Passed
Pull Request — 4 (#10033)
by Maxime
06:42
created

Form::Fields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

311
            /** @scrutinizer ignore-call */ 
312
            $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...
312
        } else {
313
            $securityEnabled = SecurityToken::is_enabled();
314
        }
315
316
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
317
318
        $this->setupDefaultClasses();
319
    }
320
321
    /**
322
     * @return bool
323
     */
324
    public function getNotifyUnsavedChanges()
325
    {
326
        return $this->notifyUnsavedChanges;
327
    }
328
329
    /**
330
     * @param bool $flag
331
     */
332
    public function setNotifyUnsavedChanges($flag)
333
    {
334
        $this->notifyUnsavedChanges = $flag;
335
    }
336
337
    /**
338
     * Load form state from session state
339
     *
340
     * @return $this
341
     */
342
    public function restoreFormState()
343
    {
344
        // Restore messages
345
        $result = $this->getSessionValidationResult();
346
        if (isset($result)) {
347
            $this->loadMessagesFrom($result);
348
        }
349
350
        // load data in from previous submission upon error
351
        $data = $this->getSessionData();
352
        if (isset($data)) {
353
            $this->loadDataFrom($data, self::MERGE_AS_INTERNAL_VALUE);
354
        }
355
        return $this;
356
    }
357
358
    /**
359
     * Flush persistant form state details
360
     *
361
     * @return $this
362
     */
363
    public function clearFormState()
364
    {
365
        $this
366
            ->getSession()
367
            ->clear("FormInfo.{$this->FormName()}.result")
368
            ->clear("FormInfo.{$this->FormName()}.data");
369
        return $this;
370
    }
371
372
    /**
373
     * Helper to get current request for this form
374
     *
375
     * @return HTTPRequest
376
     */
377
    protected function getRequest()
378
    {
379
        // Check if current request handler has a request object
380
        $controller = $this->getController();
381
        if ($controller && !($controller->getRequest() instanceof NullHTTPRequest)) {
382
            return $controller->getRequest();
383
        }
384
        // Fall back to current controller
385
        if (Controller::has_curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
386
            return Controller::curr()->getRequest();
387
        }
388
        return null;
389
    }
390
391
    /**
392
     * Get session for this form
393
     *
394
     * @return Session
395
     */
396
    protected function getSession()
397
    {
398
        $request = $this->getRequest();
399
        if ($request) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
400
            return $request->getSession();
401
        }
402
        throw new BadMethodCallException("Session not available in the current context");
403
    }
404
405
    /**
406
     * Return any form data stored in the session
407
     *
408
     * @return array
409
     */
410
    public function getSessionData()
411
    {
412
        return $this->getSession()->get("FormInfo.{$this->FormName()}.data");
413
    }
414
415
    /**
416
     * Store the given form data in the session
417
     *
418
     * @param array $data
419
     * @return $this
420
     */
421
    public function setSessionData($data)
422
    {
423
        $this->getSession()->set("FormInfo.{$this->FormName()}.data", $data);
424
        return $this;
425
    }
426
427
    /**
428
     * Return any ValidationResult instance stored for this object
429
     *
430
     * @return ValidationResult The ValidationResult object stored in the session
431
     */
432
    public function getSessionValidationResult()
433
    {
434
        $resultData = $this->getSession()->get("FormInfo.{$this->FormName()}.result");
435
        if (isset($resultData)) {
436
            return unserialize($resultData);
437
        }
438
        return null;
439
    }
440
441
    /**
442
     * Sets the ValidationResult in the session to be used with the next view of this form.
443
     * @param ValidationResult $result The result to save
444
     * @param bool $combineWithExisting If true, then this will be added to the existing result.
445
     * @return $this
446
     */
447
    public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
448
    {
449
        // Combine with existing result
450
        if ($combineWithExisting) {
451
            $existingResult = $this->getSessionValidationResult();
452
            if ($existingResult) {
0 ignored issues
show
introduced by
$existingResult is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
453
                if ($result) {
0 ignored issues
show
introduced by
$result is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
454
                    $existingResult->combineAnd($result);
455
                } else {
456
                    $result = $existingResult;
457
                }
458
            }
459
        }
460
461
        // Serialise
462
        $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...
463
        $this->getSession()->set("FormInfo.{$this->FormName()}.result", $resultData);
464
        return $this;
465
    }
466
467
    /**
468
     * Clear form message (and in session)
469
     *
470
     * @return $this
471
     */
472
    public function clearMessage()
473
    {
474
        $this->setMessage(null);
475
        $this->clearFormState();
476
        return $this;
477
    }
478
479
    /**
480
     * Populate this form with messages from the given ValidationResult.
481
     * Note: This will not clear any pre-existing messages
482
     *
483
     * @param ValidationResult $result
484
     * @return $this
485
     */
486
    public function loadMessagesFrom($result)
487
    {
488
        // Set message on either a field or the parent form
489
        foreach ($result->getMessages() as $message) {
490
            $fieldName = $message['fieldName'];
491
492
            if ($fieldName) {
493
                $owner = $this->fields->dataFieldByName($fieldName) ?: $this;
494
            } else {
495
                $owner = $this;
496
            }
497
498
            $owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
499
        }
500
        return $this;
501
    }
502
503
    /**
504
     * Set message on a given field name. This message will not persist via redirect.
505
     *
506
     * @param string $fieldName
507
     * @param string $message
508
     * @param string $messageType
509
     * @param string $messageCast
510
     * @return $this
511
     */
512
    public function setFieldMessage(
513
        $fieldName,
514
        $message,
515
        $messageType = ValidationResult::TYPE_ERROR,
516
        $messageCast = ValidationResult::CAST_TEXT
517
    ) {
518
        $field = $this->fields->dataFieldByName($fieldName);
519
        if ($field) {
520
            $field->setMessage($message, $messageType, $messageCast);
521
        }
522
        return $this;
523
    }
524
525
    public function castingHelper($field)
526
    {
527
        // Override casting for field message
528
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
529
            return $helper;
530
        }
531
        return parent::castingHelper($field);
532
    }
533
534
    /**
535
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
536
     * after instantiation
537
     */
538
    protected function setupDefaultClasses()
539
    {
540
        $defaultClasses = self::config()->get('default_classes');
541
        if ($defaultClasses) {
542
            foreach ($defaultClasses as $class) {
543
                $this->addExtraClass($class);
544
            }
545
        }
546
    }
547
548
    /**
549
     * @return callable
550
     */
551
    public function getValidationResponseCallback()
552
    {
553
        return $this->validationResponseCallback;
554
    }
555
556
    /**
557
     * Overrules validation error behaviour in {@link httpSubmission()}
558
     * when validation has failed. Useful for optional handling of a certain accepted content type.
559
     *
560
     * The callback can opt out of handling specific responses by returning NULL,
561
     * in which case the default form behaviour will kick in.
562
     *
563
     * @param $callback
564
     * @return self
565
     */
566
    public function setValidationResponseCallback($callback)
567
    {
568
        $this->validationResponseCallback = $callback;
569
570
        return $this;
571
    }
572
573
    /**
574
     * Convert this form into a readonly form
575
     *
576
     * @return $this
577
     */
578
    public function makeReadonly()
579
    {
580
        $this->transform(new ReadonlyTransformation());
581
        return $this;
582
    }
583
584
    /**
585
     * Set whether the user should be redirected back down to the
586
     * form on the page upon validation errors in the form or if
587
     * they just need to redirect back to the page
588
     *
589
     * @param bool $bool Redirect to form on error?
590
     * @return $this
591
     */
592
    public function setRedirectToFormOnValidationError($bool)
593
    {
594
        $this->redirectToFormOnValidationError = $bool;
595
        return $this;
596
    }
597
598
    /**
599
     * Get whether the user should be redirected back down to the
600
     * form on the page upon validation errors
601
     *
602
     * @return bool
603
     */
604
    public function getRedirectToFormOnValidationError()
605
    {
606
        return $this->redirectToFormOnValidationError;
607
    }
608
609
    /**
610
     * @param FormTransformation $trans
611
     */
612
    public function transform(FormTransformation $trans)
613
    {
614
        $newFields = new FieldList();
615
        foreach ($this->fields as $field) {
616
            $newFields->push($field->transform($trans));
617
        }
618
        $this->fields = $newFields;
619
620
        $newActions = new FieldList();
621
        foreach ($this->actions as $action) {
622
            $newActions->push($action->transform($trans));
623
        }
624
        $this->actions = $newActions;
625
626
627
        // We have to remove validation, if the fields are not editable ;-)
628
        if ($this->validator) {
629
            $this->validator->removeValidation();
630
        }
631
    }
632
633
    /**
634
     * Get the {@link Validator} attached to this form.
635
     * @return Validator
636
     */
637
    public function getValidator()
638
    {
639
        return $this->validator;
640
    }
641
642
    /**
643
     * Set the {@link Validator} on this form.
644
     * @param Validator $validator
645
     * @return $this
646
     */
647
    public function setValidator(Validator $validator)
648
    {
649
        if ($validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
650
            $this->validator = $validator;
651
            $this->validator->setForm($this);
652
        }
653
        return $this;
654
    }
655
656
    /**
657
     * Remove the {@link Validator} from this from.
658
     */
659
    public function unsetValidator()
660
    {
661
        $this->validator = null;
662
        return $this;
663
    }
664
665
    /**
666
     * Set actions that are exempt from validation
667
     *
668
     * @param array $actions
669
     * @return $this
670
     */
671
    public function setValidationExemptActions($actions)
672
    {
673
        $this->validationExemptActions = $actions;
674
        return $this;
675
    }
676
677
    /**
678
     * Get a list of actions that are exempt from validation
679
     *
680
     * @return array
681
     */
682
    public function getValidationExemptActions()
683
    {
684
        return $this->validationExemptActions;
685
    }
686
687
    /**
688
     * Passed a FormAction, returns true if that action is exempt from Form validation
689
     *
690
     * @param FormAction $action
691
     * @return bool
692
     */
693
    public function actionIsValidationExempt($action)
694
    {
695
        // Non-actions don't bypass validation
696
        if (!$action) {
0 ignored issues
show
introduced by
$action is of type SilverStripe\Forms\FormAction, thus it always evaluated to true.
Loading history...
697
            return false;
698
        }
699
        if ($action->getValidationExempt()) {
700
            return true;
701
        }
702
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
703
            return true;
704
        }
705
        return false;
706
    }
707
708
    /**
709
     * Generate extra special fields - namely the security token field (if required).
710
     *
711
     * @return FieldList
712
     */
713
    public function getExtraFields()
714
    {
715
        $extraFields = new FieldList();
716
717
        $token = $this->getSecurityToken();
718
        if ($token) {
719
            $tokenField = $token->updateFieldSet($this->fields);
720
            if ($tokenField) {
721
                $tokenField->setForm($this);
722
            }
723
        }
724
        $this->securityTokenAdded = true;
725
726
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
727
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
728
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
729
            $methodField->setForm($this);
730
            $extraFields->push($methodField);
731
        }
732
733
        return $extraFields;
734
    }
735
736
    /**
737
     * Return the form's fields - used by the templates
738
     *
739
     * @return FieldList The form fields
740
     */
741
    public function Fields()
742
    {
743
        foreach ($this->getExtraFields() as $field) {
744
            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...
745
                $this->fields->push($field);
746
            }
747
        }
748
749
        return $this->fields;
750
    }
751
752
    /**
753
     * Return all <input type="hidden"> fields
754
     * in a form - including fields nested in {@link CompositeFields}.
755
     * Useful when doing custom field layouts.
756
     *
757
     * @return FieldList
758
     */
759
    public function HiddenFields()
760
    {
761
        return $this->Fields()->HiddenFields();
762
    }
763
764
    /**
765
     * Return all fields except for the hidden fields.
766
     * Useful when making your own simplified form layouts.
767
     */
768
    public function VisibleFields()
769
    {
770
        return $this->Fields()->VisibleFields();
771
    }
772
773
    /**
774
     * Setter for the form fields.
775
     *
776
     * @param FieldList $fields
777
     * @return $this
778
     */
779
    public function setFields($fields)
780
    {
781
        $fields->setForm($this);
782
        $this->fields = $fields;
783
784
        return $this;
785
    }
786
787
    /**
788
     * Return the form's action buttons - used by the templates
789
     *
790
     * @return FieldList The action list
791
     */
792
    public function Actions()
793
    {
794
        return $this->actions;
795
    }
796
797
    /**
798
     * Setter for the form actions.
799
     *
800
     * @param FieldList $actions
801
     * @return $this
802
     */
803
    public function setActions($actions)
804
    {
805
        $actions->setForm($this);
806
        $this->actions = $actions;
807
808
        return $this;
809
    }
810
811
    /**
812
     * Unset all form actions
813
     */
814
    public function unsetAllActions()
815
    {
816
        $this->actions = new FieldList();
817
        return $this;
818
    }
819
820
    protected function getDefaultAttributes(): array
821
    {
822
        $attrs = [
823
            'id' => $this->FormName(),
824
            'action' => $this->FormAction(),
825
            'method' => $this->FormMethod(),
826
            'enctype' => $this->getEncType(),
827
            'target' => $this->target,
828
            'class' => $this->extraClass(),
829
        ];
830
831
        if ($this->validator && $this->validator->getErrors()) {
832
            if (!isset($attrs['class'])) {
833
                $attrs['class'] = '';
834
            }
835
            $attrs['class'] .= ' validationerror';
836
        }
837
838
        return $attrs;
839
    }
840
841
    public function FormAttributes()
842
    {
843
        return $this->getAttributesHTML();
844
    }
845
846
    /**
847
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
848
     * another frame
849
    *
850
     * @param string|FormTemplateHelper $helper
851
    */
852
    public function setTemplateHelper($helper)
853
    {
854
        $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...
855
    }
856
857
    /**
858
     * Return a {@link FormTemplateHelper} for this form. If one has not been
859
     * set, return the default helper.
860
     *
861
     * @return FormTemplateHelper
862
     */
863
    public function getTemplateHelper()
864
    {
865
        if ($this->templateHelper) {
866
            if (is_string($this->templateHelper)) {
0 ignored issues
show
introduced by
The condition is_string($this->templateHelper) is always false.
Loading history...
867
                return Injector::inst()->get($this->templateHelper);
868
            }
869
870
            return $this->templateHelper;
871
        }
872
873
        return FormTemplateHelper::singleton();
874
    }
875
876
    /**
877
     * Set the target of this form to any value - useful for opening the form
878
     * contents in a new window or refreshing another frame.
879
     *
880
     * @param string $target The value of the target
881
     * @return $this
882
     */
883
    public function setTarget($target)
884
    {
885
        $this->target = $target;
886
887
        return $this;
888
    }
889
890
    /**
891
     * Set the legend value to be inserted into
892
     * the <legend> element in the Form.ss template.
893
     * @param string $legend
894
     * @return $this
895
     */
896
    public function setLegend($legend)
897
    {
898
        $this->legend = $legend;
899
        return $this;
900
    }
901
902
    /**
903
     * Set the SS template that this form should use
904
     * to render with. The default is "Form".
905
     *
906
     * @param string|array $template The name of the template (without the .ss extension) or array form
907
     * @return $this
908
     */
909
    public function setTemplate($template)
910
    {
911
        $this->template = $template;
912
        return $this;
913
    }
914
915
    /**
916
     * Return the template to render this form with.
917
     *
918
     * @return string|array
919
     */
920
    public function getTemplate()
921
    {
922
        return $this->template;
923
    }
924
925
    /**
926
     * Returs the ordered list of preferred templates for rendering this form
927
     * If the template isn't set, then default to the
928
     * form class name e.g "Form".
929
     *
930
     * @return array
931
     */
932
    public function getTemplates()
933
    {
934
        $templates = SSViewer::get_templates_by_class(static::class, '', __CLASS__);
935
        // Prefer any custom template
936
        if ($this->getTemplate()) {
937
            array_unshift($templates, $this->getTemplate());
938
        }
939
        return $templates;
940
    }
941
942
    /**
943
     * Returns the encoding type for the form.
944
     *
945
     * By default this will be URL encoded, unless there is a file field present
946
     * in which case multipart is used. You can also set the enc type using
947
     * {@link setEncType}.
948
     */
949
    public function getEncType()
950
    {
951
        if ($this->encType) {
952
            return $this->encType;
953
        }
954
955
        if ($fields = $this->fields->dataFields()) {
956
            foreach ($fields as $field) {
957
                if ($field instanceof FileField) {
958
                    return self::ENC_TYPE_MULTIPART;
959
                }
960
            }
961
        }
962
963
        return self::ENC_TYPE_URLENCODED;
964
    }
965
966
    /**
967
     * Sets the form encoding type. The most common encoding types are defined
968
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
969
     *
970
     * @param string $encType
971
     * @return $this
972
     */
973
    public function setEncType($encType)
974
    {
975
        $this->encType = $encType;
976
        return $this;
977
    }
978
979
    /**
980
     * Returns the real HTTP method for the form:
981
     * GET, POST, PUT, DELETE or HEAD.
982
     * As most browsers only support GET and POST in
983
     * form submissions, all other HTTP methods are
984
     * added as a hidden field "_method" that
985
     * gets evaluated in {@link HTTPRequest::detect_method()}.
986
     * See {@link FormMethod()} to get a HTTP method
987
     * for safe insertion into a <form> tag.
988
     *
989
     * @return string HTTP method
990
     */
991
    public function FormHttpMethod()
992
    {
993
        return $this->formMethod;
994
    }
995
996
    /**
997
     * Returns the form method to be used in the <form> tag.
998
     * See {@link FormHttpMethod()} to get the "real" method.
999
     *
1000
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1001
     */
1002
    public function FormMethod()
1003
    {
1004
        if (in_array($this->formMethod, ['GET','POST'])) {
1005
            return $this->formMethod;
1006
        } else {
1007
            return 'POST';
1008
        }
1009
    }
1010
1011
    /**
1012
     * Set the form method: GET, POST, PUT, DELETE.
1013
     *
1014
     * @param string $method
1015
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1016
     * @return $this
1017
     */
1018
    public function setFormMethod($method, $strict = null)
1019
    {
1020
        $this->formMethod = strtoupper($method);
1021
        if ($strict !== null) {
1022
            $this->setStrictFormMethodCheck($strict);
1023
        }
1024
        return $this;
1025
    }
1026
1027
    /**
1028
     * If set to true (the default), enforces the matching of the form method.
1029
     *
1030
     * This will mean two things:
1031
     *  - GET vars will be ignored by a POST form, and vice versa
1032
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1033
     *
1034
     * If set to false then the form method is only used to construct the default
1035
     * form.
1036
     *
1037
     * @param $bool boolean
1038
     * @return $this
1039
     */
1040
    public function setStrictFormMethodCheck($bool)
1041
    {
1042
        $this->strictFormMethodCheck = (bool)$bool;
1043
        return $this;
1044
    }
1045
1046
    /**
1047
     * @return boolean
1048
     */
1049
    public function getStrictFormMethodCheck()
1050
    {
1051
        return $this->strictFormMethodCheck;
1052
    }
1053
1054
    /**
1055
     * Return the form's action attribute.
1056
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1057
     *
1058
     * @return string
1059
     */
1060
    public function FormAction()
1061
    {
1062
        if ($this->formActionPath) {
1063
            return $this->formActionPath;
1064
        }
1065
1066
        // Get action from request handler link
1067
        return $this->getRequestHandler()->Link();
1068
    }
1069
1070
    /**
1071
     * Set the form action attribute to a custom URL.
1072
     *
1073
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1074
     * recommended only for situations where you have two relatively distinct
1075
     * parts of the system trying to communicate via a form post.
1076
     *
1077
     * @param string $path
1078
     * @return $this
1079
     */
1080
    public function setFormAction($path)
1081
    {
1082
        $this->formActionPath = $path;
1083
1084
        return $this;
1085
    }
1086
1087
    /**
1088
     * Returns the name of the form.
1089
     *
1090
     * @return string
1091
     */
1092
    public function FormName()
1093
    {
1094
        return $this->getTemplateHelper()->generateFormID($this);
1095
    }
1096
1097
    /**
1098
     * Set the HTML ID attribute of the form.
1099
     *
1100
     * @param string $id
1101
     * @return $this
1102
     */
1103
    public function setHTMLID($id)
1104
    {
1105
        $this->htmlID = $id;
1106
1107
        return $this;
1108
    }
1109
1110
    /**
1111
     * @return string
1112
     */
1113
    public function getHTMLID()
1114
    {
1115
        return $this->htmlID;
1116
    }
1117
1118
    /**
1119
     * Get the controller or parent request handler.
1120
     *
1121
     * @return RequestHandler
1122
     */
1123
    public function getController()
1124
    {
1125
        return $this->controller;
1126
    }
1127
1128
    /**
1129
     * Set the controller or parent request handler.
1130
     *
1131
     * @param RequestHandler $controller
1132
     * @return $this
1133
     */
1134
    public function setController(RequestHandler $controller = null)
1135
    {
1136
        $this->controller = $controller;
1137
        return $this;
1138
    }
1139
1140
    /**
1141
     * Get the name of the form.
1142
     *
1143
     * @return string
1144
     */
1145
    public function getName()
1146
    {
1147
        return $this->name;
1148
    }
1149
1150
    /**
1151
     * Set the name of the form.
1152
     *
1153
     * @param string $name
1154
     * @return Form
1155
     */
1156
    public function setName($name)
1157
    {
1158
        $this->name = $name;
1159
1160
        return $this;
1161
    }
1162
1163
    /**
1164
     * Returns an object where there is a method with the same name as each data
1165
     * field on the form.
1166
     *
1167
     * That method will return the field itself.
1168
     *
1169
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1170
     */
1171
    public function FieldMap()
1172
    {
1173
        return new Form_FieldMap($this);
1174
    }
1175
1176
    /**
1177
     * Set a message to the session, for display next time this form is shown.
1178
     *
1179
     * @param string $message the text of the message
1180
     * @param string $type Should be set to good, bad, or warning.
1181
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1182
     * Bool values will be treated as plain text flag.
1183
     */
1184
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1185
    {
1186
        $this->setMessage($message, $type, $cast);
1187
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1188
        $result->addMessage($message, $type, null, $cast);
1189
        $this->setSessionValidationResult($result);
1190
    }
1191
1192
    /**
1193
     * Set an error to the session, for display next time this form is shown.
1194
     *
1195
     * @param string $message the text of the message
1196
     * @param string $type Should be set to good, bad, or warning.
1197
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1198
     * Bool values will be treated as plain text flag.
1199
     */
1200
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1201
    {
1202
        $this->setMessage($message, $type, $cast);
1203
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1204
        $result->addError($message, $type, null, $cast);
1205
        $this->setSessionValidationResult($result);
1206
    }
1207
1208
    /**
1209
     * Set an error message for a field in the session, for display next time this form is shown.
1210
     *
1211
     * @param string $message the text of the message
1212
     * @param string $fieldName Name of the field to set the error message on it.
1213
     * @param string $type Should be set to good, bad, or warning.
1214
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1215
     * Bool values will be treated as plain text flag.
1216
     */
1217
    public function sessionFieldError($message, $fieldName, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1218
    {
1219
        $this->setMessage($message, $type, $cast);
1220
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1221
        $result->addFieldMessage($fieldName, $message, $type, null, $cast);
1222
        $this->setSessionValidationResult($result);
1223
    }
1224
1225
    /**
1226
     * Returns the DataObject that has given this form its data
1227
     * through {@link loadDataFrom()}.
1228
     *
1229
     * @return DataObject
1230
     */
1231
    public function getRecord()
1232
    {
1233
        return $this->record;
1234
    }
1235
1236
    /**
1237
     * Get the legend value to be inserted into the
1238
     * <legend> element in Form.ss
1239
     *
1240
     * @return string
1241
     */
1242
    public function getLegend()
1243
    {
1244
        return $this->legend;
1245
    }
1246
1247
    /**
1248
     * Processing that occurs before a form is executed.
1249
     *
1250
     * This includes form validation, if it fails, we throw a ValidationException
1251
     *
1252
     * This includes form validation, if it fails, we redirect back
1253
     * to the form with appropriate error messages.
1254
     * Always return true if the current form action is exempt from validation
1255
     *
1256
     * Triggered through {@link httpSubmission()}.
1257
     *
1258
     *
1259
     * Note that CSRF protection takes place in {@link httpSubmission()},
1260
     * if it fails the form data will never reach this method.
1261
     *
1262
     * @return ValidationResult
1263
     */
1264
    public function validationResult()
1265
    {
1266
        // Automatically pass if there is no validator, or the clicked button is exempt
1267
        // Note: Soft support here for validation with absent request handler
1268
        $handler = $this->getRequestHandler();
1269
        $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...
1270
        $validator = $this->getValidator();
1271
        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...
1272
            return ValidationResult::create();
1273
        }
1274
1275
        // Invoke validator
1276
        $result = $validator->validate();
1277
        $this->loadMessagesFrom($result);
1278
        return $result;
1279
    }
1280
1281
    const MERGE_DEFAULT             = 0b0000;
1282
    const MERGE_CLEAR_MISSING       = 0b0001;
1283
    const MERGE_IGNORE_FALSEISH     = 0b0010;
1284
    const MERGE_AS_INTERNAL_VALUE   = 0b0100;
1285
    const MERGE_AS_SUBMITTED_VALUE  = 0b1000;
1286
1287
    /**
1288
     * Load data from the given DataObject or array.
1289
     *
1290
     * It will call $object->MyField to get the value of MyField.
1291
     * If you passed an array, it will call $object[MyField].
1292
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1293
     * as determined by {@link FieldList->dataFields()}.
1294
     *
1295
     * By default, if a field isn't set (as determined by isset()),
1296
     * its value will not be saved to the field, retaining
1297
     * potential existing values.
1298
     *
1299
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1300
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1301
     *
1302
     * Escaping happens automatically on saving the data through
1303
     * {@link saveInto()}.
1304
     *
1305
     * @uses FieldList::dataFields()
1306
     * @uses FormField::setSubmittedValue()
1307
     * @uses FormField::setValue()
1308
     *
1309
     * @param array|DataObject $data
1310
     * @param int $mergeStrategy
1311
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1312
     *  what that property/key's value is.
1313
     *
1314
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1315
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1316
     *  "left alone", meaning they retain any previous value.
1317
     *
1318
     *  You can pass a bitmask here to change this behaviour.
1319
     *
1320
     *  Passing MERGE_CLEAR_MISSING means that any fields that don't match any property/key in
1321
     *  {@link $data} are cleared.
1322
     *
1323
     *  Passing MERGE_IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1324
     *  a field's value.
1325
     *
1326
     *  Passing MERGE_AS_INTERNAL_VALUE forces the data to be parsed using the internal representation of the matching
1327
     *  form field. This is helpful if you are loading an array of values retrieved from `Form::getData()` and you
1328
     *  do not want them parsed as submitted data. MERGE_AS_SUBMITTED_VALUE does the opposite and forces the data to be
1329
     *  parsed as it would be submitted from a form.
1330
     *
1331
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1332
     *  MERGE_CLEAR_MISSING
1333
     *
1334
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1335
     * form that has some fields that save to one object, and some that save to another.
1336
     * @return $this
1337
     */
1338
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1339
    {
1340
        if (!is_object($data) && !is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
1341
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1342
            return $this;
1343
        }
1344
1345
        // Handle the backwards compatible case of passing "true" as the second argument
1346
        if ($mergeStrategy === true) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === true is always false.
Loading history...
1347
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1348
        } elseif ($mergeStrategy === false) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === false is always false.
Loading history...
1349
            $mergeStrategy = 0;
1350
        }
1351
1352
        // If an object is passed, save it for historical reference through {@link getRecord()}
1353
        // Also use this to determine if we are loading a submitted form, or loading
1354
        // from a dataobject
1355
        $submitted = true;
1356
        if (is_object($data)) {
1357
            $this->record = $data;
1358
            $submitted = false;
1359
        }
1360
1361
        // Using the `MERGE_AS_INTERNAL_VALUE` or `MERGE_AS_SUBMITTED_VALUE` flags users can explicitly specify which
1362
        // `setValue` method to use.
1363
        if (($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) == self::MERGE_AS_INTERNAL_VALUE) {
1364
            $submitted = false;
1365
        } elseif (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) {
1366
            $submitted = true;
1367
        }
1368
1369
        // Don't include fields without data
1370
        $dataFields = $this->Fields()->dataFields();
1371
1372
        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...
1373
            return $this;
1374
        }
1375
1376
        /** @var FormField $field */
1377
        foreach ($dataFields as $field) {
1378
            $name = $field->getName();
1379
1380
            // Skip fields that have been excluded
1381
            if ($fieldList && !in_array($name, $fieldList)) {
1382
                continue;
1383
            }
1384
1385
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1386
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1387
                continue;
1388
            }
1389
1390
            // Does this property exist on $data?
1391
            $exists = false;
1392
            // The value from $data for this field
1393
            $val = null;
1394
1395
            if (is_object($data)) {
1396
                // Allow dot-syntax traversal of has-one relations fields
1397
                if (strpos($name, '.') !== false) {
1398
                    $exists = (
1399
                        $data->hasMethod('relField')
1400
                    );
1401
                    try {
1402
                        $val = $data->relField($name);
1403
                    } catch (\LogicException $e) {
1404
                        // There's no other way to tell whether the relation actually exists
1405
                        $exists = false;
1406
                    }
1407
                // Regular ViewableData access
1408
                } else {
1409
                    $exists = (
1410
                        isset($data->$name) ||
1411
                        $data->hasMethod($name) ||
1412
                        ($data->hasMethod('hasField') && $data->hasField($name))
1413
                    );
1414
1415
                    if ($exists) {
1416
                        $val = $data->__get($name);
1417
                    }
1418
                }
1419
1420
            // Regular array access. Note that dot-syntax not supported here
1421
            } elseif (is_array($data)) {
1422
                if (array_key_exists($name, $data)) {
1423
                    $exists = true;
1424
                    $val = $data[$name];
1425
                // PHP turns the '.'s in POST vars into '_'s
1426
                } elseif (array_key_exists($altName = str_replace('.', '_', $name), $data)) {
1427
                    $exists = true;
1428
                    $val = $data[$altName];
1429
                } elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1430
                    // If field is in array-notation we need to access nested data
1431
                    //discard first match which is just the whole string
1432
                    array_shift($matches);
1433
                    $keys = array_pop($matches);
1434
                    $name = array_shift($matches);
1435
                    $name = array_shift($name);
1436
                    if (array_key_exists($name, $data)) {
1437
                        $tmpData = &$data[$name];
1438
                        // drill down into the data array looking for the corresponding value
1439
                        foreach ($keys as $arrayKey) {
1440
                            if ($tmpData && $arrayKey !== '') {
1441
                                $tmpData = &$tmpData[$arrayKey];
1442
                            } else {
1443
                                //empty square brackets means new array
1444
                                if (is_array($tmpData)) {
1445
                                    $tmpData = array_shift($tmpData);
1446
                                }
1447
                            }
1448
                        }
1449
                        if ($tmpData) {
1450
                            $val = $tmpData;
1451
                            $exists = true;
1452
                        }
1453
                    }
1454
                }
1455
            }
1456
1457
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1458
            $setValue = false;
1459
            if ($exists) {
1460
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1461
                    $setValue = true;
1462
                }
1463
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1464
                $setValue = true;
1465
            }
1466
1467
            // pass original data as well so composite fields can act on the additional information
1468
            if ($setValue) {
1469
                if ($submitted) {
1470
                    $field->setSubmittedValue($val, $data);
1471
                } else {
1472
                    $field->setValue($val, $data);
1473
                }
1474
            }
1475
        }
1476
        return $this;
1477
    }
1478
1479
    /**
1480
     * Save the contents of this form into the given data object.
1481
     * It will make use of setCastedField() to do this.
1482
     *
1483
     * @param DataObjectInterface $dataObject The object to save data into
1484
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1485
     * form that has some fields that save to one object, and some that save to another.
1486
     */
1487
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1488
    {
1489
        $dataFields = $this->fields->saveableFields();
1490
        $lastField = null;
1491
        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...
1492
            foreach ($dataFields as $field) {
1493
            // Skip fields that have been excluded
1494
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1495
                    continue;
1496
                }
1497
1498
                $saveMethod = "save{$field->getName()}";
1499
                if ($field->getName() == "ClassName") {
1500
                    $lastField = $field;
1501
                } 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

1501
                } elseif ($dataObject->/** @scrutinizer ignore-call */ hasMethod($saveMethod)) {
Loading history...
1502
                    $dataObject->$saveMethod($field->dataValue());
1503
                } elseif ($field->getName() !== "ID") {
1504
                    $field->saveInto($dataObject);
1505
                }
1506
            }
1507
        }
1508
        if ($lastField) {
1509
            $lastField->saveInto($dataObject);
1510
        }
1511
    }
1512
1513
    /**
1514
     * Get the submitted data from this form through
1515
     * {@link FieldList->dataFields()}, which filters out
1516
     * any form-specific data like form-actions.
1517
     * Calls {@link FormField->dataValue()} on each field,
1518
     * which returns a value suitable for insertion into a DataObject
1519
     * property.
1520
     *
1521
     * @return array
1522
     */
1523
    public function getData()
1524
    {
1525
        $dataFields = $this->fields->dataFields();
1526
        $data = [];
1527
1528
        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...
1529
            /** @var FormField $field */
1530
            foreach ($dataFields as $field) {
1531
                if ($field->getName()) {
1532
                    $data[$field->getName()] = $field->dataValue();
1533
                }
1534
            }
1535
        }
1536
1537
        return $data;
1538
    }
1539
1540
    /**
1541
     * Return a rendered version of this form.
1542
     *
1543
     * This is returned when you access a form as $FormObject rather
1544
     * than <% with FormObject %>
1545
     *
1546
     * @return DBHTMLText
1547
     */
1548
    public function forTemplate()
1549
    {
1550
        if (!$this->canBeCached()) {
1551
            HTTPCacheControlMiddleware::singleton()->disableCache();
1552
        }
1553
1554
        $return = $this->renderWith($this->getTemplates());
1555
1556
        // Now that we're rendered, clear message
1557
        $this->clearMessage();
1558
1559
        return $return;
1560
    }
1561
1562
    /**
1563
     * Return a rendered version of this form, suitable for ajax post-back.
1564
     *
1565
     * It triggers slightly different behaviour, such as disabling the rewriting
1566
     * of # links.
1567
     *
1568
     * @return DBHTMLText
1569
     */
1570
    public function forAjaxTemplate()
1571
    {
1572
        $view = SSViewer::create($this->getTemplates());
1573
1574
        $return = $view->dontRewriteHashlinks()->process($this);
1575
1576
        // Now that we're rendered, clear message
1577
        $this->clearMessage();
1578
1579
        return $return;
1580
    }
1581
1582
    /**
1583
     * Returns an HTML rendition of this form, without the <form> tag itself.
1584
     *
1585
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1586
     * and _form_enctype.  These are the attributes of the form.  These fields
1587
     * can be used to send the form to Ajax.
1588
     *
1589
     * @deprecated 5.0
1590
     * @return string
1591
     */
1592
    public function formHtmlContent()
1593
    {
1594
        Deprecation::notice('5.0');
1595
        $this->IncludeFormTag = false;
1596
        $content = $this->forTemplate();
1597
        $this->IncludeFormTag = true;
1598
1599
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
1600
            . " value=\"" . $this->FormAction() . "\" />\n";
1601
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1602
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1603
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1604
1605
        return $content;
1606
    }
1607
1608
    /**
1609
     * Render this form using the given template, and return the result as a string
1610
     * You can pass either an SSViewer or a template name
1611
     * @param string|array $template
1612
     * @return DBHTMLText
1613
     */
1614
    public function renderWithoutActionButton($template)
1615
    {
1616
        $custom = $this->customise([
1617
            "Actions" => "",
1618
        ]);
1619
1620
        if (is_string($template)) {
1621
            $template = SSViewer::create($template);
1622
        }
1623
1624
        return $template->process($custom);
1625
    }
1626
1627
    /**
1628
     * Return the default button that should be clicked when another one isn't
1629
     * available.
1630
     *
1631
     * @return FormAction
1632
     */
1633
    public function defaultAction()
1634
    {
1635
        if ($this->hasDefaultAction && $this->actions) {
1636
            return $this->actions->first();
1637
        }
1638
        return null;
1639
    }
1640
1641
    /**
1642
     * Disable the default button.
1643
     *
1644
     * Ordinarily, when a form is processed and no action_XXX button is
1645
     * available, then the first button in the actions list will be pressed.
1646
     * However, if this is "delete", for example, this isn't such a good idea.
1647
     *
1648
     * @return Form
1649
     */
1650
    public function disableDefaultAction()
1651
    {
1652
        $this->hasDefaultAction = false;
1653
1654
        return $this;
1655
    }
1656
1657
    /**
1658
     * Disable the requirement of a security token on this form instance. This
1659
     * security protects against CSRF attacks, but you should disable this if
1660
     * you don't want to tie a form to a session - eg a search form.
1661
     *
1662
     * Check for token state with {@link getSecurityToken()} and
1663
     * {@link SecurityToken->isEnabled()}.
1664
     *
1665
     * @return Form
1666
     */
1667
    public function disableSecurityToken()
1668
    {
1669
        $this->securityToken = new NullSecurityToken();
1670
1671
        return $this;
1672
    }
1673
1674
    /**
1675
     * Enable {@link SecurityToken} protection for this form instance.
1676
     *
1677
     * Check for token state with {@link getSecurityToken()} and
1678
     * {@link SecurityToken->isEnabled()}.
1679
     *
1680
     * @return Form
1681
     */
1682
    public function enableSecurityToken()
1683
    {
1684
        $this->securityToken = new SecurityToken();
1685
1686
        return $this;
1687
    }
1688
1689
    /**
1690
     * Returns the security token for this form (if any exists).
1691
     *
1692
     * Doesn't check for {@link securityTokenEnabled()}.
1693
     *
1694
     * Use {@link SecurityToken::inst()} to get a global token.
1695
     *
1696
     * @return SecurityToken|null
1697
     */
1698
    public function getSecurityToken()
1699
    {
1700
        return $this->securityToken;
1701
    }
1702
1703
    /**
1704
     * Compiles all CSS-classes.
1705
     *
1706
     * @return string
1707
     */
1708
    public function extraClass()
1709
    {
1710
        return implode(' ', array_unique($this->extraClasses));
1711
    }
1712
1713
    /**
1714
     * Check if a CSS-class has been added to the form container.
1715
     *
1716
     * @param string $class A string containing a classname or several class
1717
     * names delimited by a single space.
1718
     * @return boolean True if all of the classnames passed in have been added.
1719
     */
1720
    public function hasExtraClass($class)
1721
    {
1722
        //split at white space
1723
        $classes = preg_split('/\s+/', $class);
1724
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1725
            if (!isset($this->extraClasses[$class])) {
1726
                return false;
1727
            }
1728
        }
1729
        return true;
1730
    }
1731
1732
    /**
1733
     * Add a CSS-class to the form-container. If needed, multiple classes can
1734
     * be added by delimiting a string with spaces.
1735
     *
1736
     * @param string $class A string containing a classname or several class
1737
     *              names delimited by a single space.
1738
     * @return $this
1739
     */
1740
    public function addExtraClass($class)
1741
    {
1742
        //split at white space
1743
        $classes = preg_split('/\s+/', $class);
1744
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1745
            //add classes one by one
1746
            $this->extraClasses[$class] = $class;
1747
        }
1748
        return $this;
1749
    }
1750
1751
    /**
1752
     * Remove a CSS-class from the form-container. Multiple class names can
1753
     * be passed through as a space delimited string
1754
     *
1755
     * @param string $class
1756
     * @return $this
1757
     */
1758
    public function removeExtraClass($class)
1759
    {
1760
        //split at white space
1761
        $classes = preg_split('/\s+/', $class);
1762
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1763
            //unset one by one
1764
            unset($this->extraClasses[$class]);
1765
        }
1766
        return $this;
1767
    }
1768
1769
    public function debug()
1770
    {
1771
        $class = static::class;
1772
        $result = "<h3>$class</h3><ul>";
1773
        foreach ($this->fields as $field) {
1774
            $result .= "<li>$field" . $field->debug() . "</li>";
1775
        }
1776
        $result .= "</ul>";
1777
1778
        if ($this->validator) {
1779
            /** @skipUpgrade */
1780
            $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

1780
            $result .= '<h3>' . _t(__CLASS__ . '.VALIDATOR', 'Validator') . '</h3>' . $this->validator->/** @scrutinizer ignore-call */ debug();
Loading history...
1781
        }
1782
1783
        return $result;
1784
    }
1785
1786
    /**
1787
     * Current request handler, build by buildRequestHandler(),
1788
     * accessed by getRequestHandler()
1789
     *
1790
     * @var FormRequestHandler
1791
     */
1792
    protected $requestHandler = null;
1793
1794
    /**
1795
     * Get request handler for this form
1796
     *
1797
     * @return FormRequestHandler
1798
     */
1799
    public function getRequestHandler()
1800
    {
1801
        if (!$this->requestHandler) {
1802
            $this->requestHandler = $this->buildRequestHandler();
1803
        }
1804
        return $this->requestHandler;
1805
    }
1806
1807
    /**
1808
     * Assign a specific request handler for this form
1809
     *
1810
     * @param FormRequestHandler $handler
1811
     * @return $this
1812
     */
1813
    public function setRequestHandler(FormRequestHandler $handler)
1814
    {
1815
        $this->requestHandler = $handler;
1816
        return $this;
1817
    }
1818
1819
    /**
1820
     * Scaffold new request handler for this form
1821
     *
1822
     * @return FormRequestHandler
1823
     */
1824
    protected function buildRequestHandler()
1825
    {
1826
        return FormRequestHandler::create($this);
1827
    }
1828
1829
    /**
1830
     * Can the body of this form be cached?
1831
     *
1832
     * @return bool
1833
     */
1834
    protected function canBeCached()
1835
    {
1836
        if ($this->getSecurityToken()->isEnabled()) {
1837
            return false;
1838
        }
1839
1840
        if ($this->FormMethod() !== 'GET') {
1841
            return false;
1842
        }
1843
1844
        $validator = $this->getValidator();
1845
1846
        if (!$validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
1847
            return true;
1848
        }
1849
1850
        return $validator->canBeCached();
1851
    }
1852
}
1853