Form::getAttributesHTML()   B
last analyzed

Complexity

Conditions 7
Paths 24

Size

Total Lines 32
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 24
nop 1
dl 0
loc 32
rs 8.8333
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 customisation.
174
     *
175
     * @see Form->setTemplate()
176
     * @var string|array|null
177
     */
178
    protected $template;
179
180
    /**
181
     * Should we redirect the user back down to the
182
     * the form on validation errors rather then just the page
183
     *
184
     * @var bool
185
     */
186
    protected $redirectToFormOnValidationError = false;
187
188
    /**
189
     * @var bool
190
     */
191
    protected $security = true;
192
193
    /**
194
     * @var SecurityToken|null
195
     */
196
    protected $securityToken = null;
197
198
    /**
199
     * List of additional CSS classes for the form tag.
200
     *
201
     * @var array
202
     */
203
    protected $extraClasses = array();
204
205
    /**
206
     * @config
207
     * @var array $default_classes The default classes to apply to the Form
208
     */
209
    private static $default_classes = array();
210
211
    /**
212
     * @var string|null
213
     */
214
    protected $encType;
215
216
    /**
217
     * Any custom form attributes set through {@link setAttributes()}.
218
     * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
219
     *
220
     * @var array
221
     */
222
    protected $attributes = array();
223
224
    /**
225
     * @var array
226
     */
227
    protected $validationExemptActions = array();
228
229
    /**
230
     * @config
231
     * @var array
232
     */
233
    private static $casting = array(
234
        'AttributesHTML' => 'HTMLFragment',
235
        'FormAttributes' => 'HTMLFragment',
236
        'FormName' => 'Text',
237
        'Legend' => 'HTMLFragment',
238
    );
239
240
    /**
241
     * @var FormTemplateHelper
242
     */
243
    private $templateHelper = null;
244
245
    /**
246
     * HTML ID for this form.
247
     *
248
     * @var string
249
     */
250
    private $htmlID = null;
251
252
    /**
253
     * Custom form action path, if not linking to itself.
254
     * E.g. could be used to post to an external link
255
     *
256
     * @var string
257
     */
258
    protected $formActionPath = false;
259
260
    /**
261
     * @var bool
262
     */
263
    protected $securityTokenAdded = false;
264
265
    /**
266
     * @var bool
267
     */
268
    protected $notifyUnsavedChanges = false;
269
270
    /**
271
     * Create a new form, with the given fields an action buttons.
272
     *
273
     * @param RequestHandler $controller Optional parent request handler
274
     * @param string $name The method on the controller that will return this form object.
275
     * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
276
     * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
277
     *                           {@link FormAction} objects
278
     * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
279
     */
280
    public function __construct(
281
        RequestHandler $controller = null,
282
        $name = self::DEFAULT_NAME,
283
        FieldList $fields = null,
284
        FieldList $actions = null,
285
        Validator $validator = null
286
    ) {
287
        parent::__construct();
288
289
        $fields = $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
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) {
0 ignored issues
show
introduced by
$field is of type SilverStripe\Forms\FormField, thus it always evaluated to true.
Loading history...
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
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())) {
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 = array(
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 = array();
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 $args of sprintf() does only seem to accept 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
920
    */
921
    public function setTemplateHelper($helper)
922
    {
923
        $this->templateHelper = $helper;
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, array('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
     * Returns the DataObject that has given this form its data
1279
     * through {@link loadDataFrom()}.
1280
     *
1281
     * @return DataObject
1282
     */
1283
    public function getRecord()
1284
    {
1285
        return $this->record;
1286
    }
1287
1288
    /**
1289
     * Get the legend value to be inserted into the
1290
     * <legend> element in Form.ss
1291
     *
1292
     * @return string
1293
     */
1294
    public function getLegend()
1295
    {
1296
        return $this->legend;
1297
    }
1298
1299
    /**
1300
     * Processing that occurs before a form is executed.
1301
     *
1302
     * This includes form validation, if it fails, we throw a ValidationException
1303
     *
1304
     * This includes form validation, if it fails, we redirect back
1305
     * to the form with appropriate error messages.
1306
     * Always return true if the current form action is exempt from validation
1307
     *
1308
     * Triggered through {@link httpSubmission()}.
1309
     *
1310
     *
1311
     * Note that CSRF protection takes place in {@link httpSubmission()},
1312
     * if it fails the form data will never reach this method.
1313
     *
1314
     * @return ValidationResult
1315
     */
1316
    public function validationResult()
1317
    {
1318
        // Automatically pass if there is no validator, or the clicked button is exempt
1319
        // Note: Soft support here for validation with absent request handler
1320
        $handler = $this->getRequestHandler();
1321
        $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...
1322
        $validator = $this->getValidator();
1323
        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...
1324
            return ValidationResult::create();
1325
        }
1326
1327
        // Invoke validator
1328
        $result = $validator->validate();
1329
        $this->loadMessagesFrom($result);
1330
        return $result;
1331
    }
1332
1333
    const MERGE_DEFAULT             = 0b0000;
1334
    const MERGE_CLEAR_MISSING       = 0b0001;
1335
    const MERGE_IGNORE_FALSEISH     = 0b0010;
1336
    const MERGE_AS_INTERNAL_VALUE   = 0b0100;
1337
    const MERGE_AS_SUBMITTED_VALUE  = 0b1000;
1338
1339
    /**
1340
     * Load data from the given DataObject or array.
1341
     *
1342
     * It will call $object->MyField to get the value of MyField.
1343
     * If you passed an array, it will call $object[MyField].
1344
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1345
     * as determined by {@link FieldList->dataFields()}.
1346
     *
1347
     * By default, if a field isn't set (as determined by isset()),
1348
     * its value will not be saved to the field, retaining
1349
     * potential existing values.
1350
     *
1351
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1352
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1353
     *
1354
     * Escaping happens automatically on saving the data through
1355
     * {@link saveInto()}.
1356
     *
1357
     * @uses FieldList->dataFields()
1358
     * @uses FormField->setSubmittedValue()
1359
     * @uses FormField->setValue()
1360
     *
1361
     * @param array|DataObject $data
1362
     * @param int $mergeStrategy
1363
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1364
     *  what that property/key's value is.
1365
     *
1366
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1367
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1368
     *  "left alone", meaning they retain any previous value.
1369
     *
1370
     *  You can pass a bitmask here to change this behaviour.
1371
     *
1372
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1373
     *  {@link $data} are cleared.
1374
     *
1375
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1376
     *  a field's value.
1377
     *
1378
     *  Passing MERGE_AS_INTERNAL_VALUE forces the data to be parsed using the internal representation of the matching
1379
     *  form field. This is helpful if you are loading an array of values retrieved from `Form::getData()` and you
1380
     *  do not want them parsed as submitted data. MERGE_AS_SUBMITTED_VALUE does the opposite and forces the data to be
1381
     *  parsed as it would be submitted from a form.
1382
     *
1383
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1384
     *  CLEAR_MISSING
1385
     *
1386
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1387
     * form that has some fields that save to one object, and some that save to another.
1388
     * @return $this
1389
     */
1390
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1391
    {
1392
        if (!is_object($data) && !is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
1393
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1394
            return $this;
1395
        }
1396
1397
        // Handle the backwards compatible case of passing "true" as the second argument
1398
        if ($mergeStrategy === true) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === true is always false.
Loading history...
1399
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1400
        } elseif ($mergeStrategy === false) {
0 ignored issues
show
introduced by
The condition $mergeStrategy === false is always false.
Loading history...
1401
            $mergeStrategy = 0;
1402
        }
1403
1404
        // If an object is passed, save it for historical reference through {@link getRecord()}
1405
        // Also use this to determine if we are loading a submitted form, or loading
1406
        // from a dataobject
1407
        $submitted = true;
1408
        if (is_object($data)) {
1409
            $this->record = $data;
1410
            $submitted = false;
1411
        }
1412
1413
        // Using the `MERGE_AS_INTERNAL_VALUE` or `MERGE_AS_SUBMITTED_VALUE` flags users can explicitly specify which
1414
        // `setValue` method to use.
1415
        if (($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) == self::MERGE_AS_INTERNAL_VALUE) {
1416
            $submitted = false;
1417
        } elseif (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) {
1418
            $submitted = true;
1419
        }
1420
1421
        // Don't include fields without data
1422
        $dataFields = $this->Fields()->dataFields();
1423
1424
        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...
1425
            return $this;
1426
        }
1427
1428
        /** @var FormField $field */
1429
        foreach ($dataFields as $field) {
1430
            $name = $field->getName();
1431
1432
            // Skip fields that have been excluded
1433
            if ($fieldList && !in_array($name, $fieldList)) {
1434
                continue;
1435
            }
1436
1437
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1438
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1439
                continue;
1440
            }
1441
1442
            // Does this property exist on $data?
1443
            $exists = false;
1444
            // The value from $data for this field
1445
            $val = null;
1446
1447
            if (is_object($data)) {
1448
                $exists = (
1449
                    isset($data->$name) ||
1450
                    $data->hasMethod($name) ||
1451
                    ($data->hasMethod('hasField') && $data->hasField($name))
1452
                );
1453
1454
                if ($exists) {
1455
                    $val = $data->__get($name);
1456
                }
1457
            } elseif (is_array($data)) {
1458
                if (array_key_exists($name, $data)) {
1459
                    $exists = true;
1460
                    $val = $data[$name];
1461
                } elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1462
                    // If field is in array-notation we need to access nested data
1463
                    //discard first match which is just the whole string
1464
                    array_shift($matches);
1465
                    $keys = array_pop($matches);
1466
                    $name = array_shift($matches);
1467
                    $name = array_shift($name);
1468
                    if (array_key_exists($name, $data)) {
1469
                        $tmpData = &$data[$name];
1470
                        // drill down into the data array looking for the corresponding value
1471
                        foreach ($keys as $arrayKey) {
1472
                            if ($tmpData && $arrayKey !== '') {
1473
                                $tmpData = &$tmpData[$arrayKey];
1474
                            } else {
1475
                                //empty square brackets means new array
1476
                                if (is_array($tmpData)) {
1477
                                    $tmpData = array_shift($tmpData);
1478
                                }
1479
                            }
1480
                        }
1481
                        if ($tmpData) {
1482
                            $val = $tmpData;
1483
                            $exists = true;
1484
                        }
1485
                    }
1486
                }
1487
            }
1488
1489
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1490
            $setValue = false;
1491
            if ($exists) {
1492
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1493
                    $setValue = true;
1494
                }
1495
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1496
                $setValue = true;
1497
            }
1498
1499
            // pass original data as well so composite fields can act on the additional information
1500
            if ($setValue) {
1501
                if ($submitted) {
1502
                    $field->setSubmittedValue($val, $data);
1503
                } else {
1504
                    $field->setValue($val, $data);
1505
                }
1506
            }
1507
        }
1508
        return $this;
1509
    }
1510
1511
    /**
1512
     * Save the contents of this form into the given data object.
1513
     * It will make use of setCastedField() to do this.
1514
     *
1515
     * @param DataObjectInterface $dataObject The object to save data into
1516
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1517
     * form that has some fields that save to one object, and some that save to another.
1518
     */
1519
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1520
    {
1521
        $dataFields = $this->fields->saveableFields();
1522
        $lastField = null;
1523
        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...
1524
            foreach ($dataFields as $field) {
1525
            // Skip fields that have been excluded
1526
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1527
                    continue;
1528
                }
1529
1530
                $saveMethod = "save{$field->getName()}";
1531
                if ($field->getName() == "ClassName") {
1532
                    $lastField = $field;
1533
                } 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

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

1793
            $result .= '<h3>' . _t(__CLASS__ . '.VALIDATOR', 'Validator') . '</h3>' . $this->validator->/** @scrutinizer ignore-call */ debug();
Loading history...
1794
        }
1795
1796
        return $result;
1797
    }
1798
1799
    /**
1800
     * Current request handler, build by buildRequestHandler(),
1801
     * accessed by getRequestHandler()
1802
     *
1803
     * @var FormRequestHandler
1804
     */
1805
    protected $requestHandler = null;
1806
1807
    /**
1808
     * Get request handler for this form
1809
     *
1810
     * @return FormRequestHandler
1811
     */
1812
    public function getRequestHandler()
1813
    {
1814
        if (!$this->requestHandler) {
1815
            $this->requestHandler = $this->buildRequestHandler();
1816
        }
1817
        return $this->requestHandler;
1818
    }
1819
1820
    /**
1821
     * Assign a specific request handler for this form
1822
     *
1823
     * @param FormRequestHandler $handler
1824
     * @return $this
1825
     */
1826
    public function setRequestHandler(FormRequestHandler $handler)
1827
    {
1828
        $this->requestHandler = $handler;
1829
        return $this;
1830
    }
1831
1832
    /**
1833
     * Scaffold new request handler for this form
1834
     *
1835
     * @return FormRequestHandler
1836
     */
1837
    protected function buildRequestHandler()
1838
    {
1839
        return FormRequestHandler::create($this);
1840
    }
1841
1842
    /**
1843
     * Can the body of this form be cached?
1844
     *
1845
     * @return bool
1846
     */
1847
    protected function canBeCached()
1848
    {
1849
        if ($this->getSecurityToken()->isEnabled()) {
1850
            return false;
1851
        }
1852
        if ($this->FormMethod() !== 'GET') {
1853
            return false;
1854
        }
1855
1856
        // Don't cache if there are required fields, or some other complex validator
1857
        $validator = $this->getValidator();
1858
        if ($validator instanceof RequiredFields) {
1859
            if (count($this->validator->getRequired())) {
0 ignored issues
show
Bug introduced by
The method getRequired() 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

1859
            if (count($this->validator->/** @scrutinizer ignore-call */ getRequired())) {
Loading history...
1860
                return false;
1861
            }
1862
        } else {
1863
            return false;
1864
        }
1865
        return true;
1866
    }
1867
}
1868