Completed
Push — master ( 2bdb72...51e4e4 )
by
unknown
30s
created

Form::setNotifyUnsavedChanges()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
290
        $actions->setForm($this);
0 ignored issues
show
Bug introduced by
It seems like $actions is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
291
292
        $this->fields = $fields;
293
        $this->actions = $actions;
294
        $this->setController($controller);
295
        $this->setName($name);
296
297
        // Form validation
298
        $this->validator = ($validator) ? $validator : new RequiredFields();
299
        $this->validator->setForm($this);
300
301
        // Form error controls
302
        $this->restoreFormState();
303
304
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
305
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
306
        if (ClassInfo::hasMethod($controller, 'securityTokenEnabled')) {
0 ignored issues
show
Bug introduced by
It seems like $controller defined by parameter $controller on line 281 can be null; however, SilverStripe\Core\ClassInfo::hasMethod() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1008
            array_unshift($templates, $this->getTemplate());
1009
        }
1010
        return $templates;
1011
    }
1012
1013
    /**
1014
     * Returns the encoding type for the form.
1015
     *
1016
     * By default this will be URL encoded, unless there is a file field present
1017
     * in which case multipart is used. You can also set the enc type using
1018
     * {@link setEncType}.
1019
     */
1020
    public function getEncType()
1021
    {
1022
        if ($this->encType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->encType of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1023
            return $this->encType;
1024
        }
1025
1026
        if ($fields = $this->fields->dataFields()) {
1027
            foreach ($fields as $field) {
1028
                if ($field instanceof FileField) {
1029
                    return self::ENC_TYPE_MULTIPART;
1030
                }
1031
            }
1032
        }
1033
1034
        return self::ENC_TYPE_URLENCODED;
1035
    }
1036
1037
    /**
1038
     * Sets the form encoding type. The most common encoding types are defined
1039
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
1040
     *
1041
     * @param string $encType
1042
     * @return $this
1043
     */
1044
    public function setEncType($encType)
1045
    {
1046
        $this->encType = $encType;
1047
        return $this;
1048
    }
1049
1050
    /**
1051
     * Returns the real HTTP method for the form:
1052
     * GET, POST, PUT, DELETE or HEAD.
1053
     * As most browsers only support GET and POST in
1054
     * form submissions, all other HTTP methods are
1055
     * added as a hidden field "_method" that
1056
     * gets evaluated in {@link HTTPRequest::detect_method()}.
1057
     * See {@link FormMethod()} to get a HTTP method
1058
     * for safe insertion into a <form> tag.
1059
     *
1060
     * @return string HTTP method
1061
     */
1062
    public function FormHttpMethod()
1063
    {
1064
        return $this->formMethod;
1065
    }
1066
1067
    /**
1068
     * Returns the form method to be used in the <form> tag.
1069
     * See {@link FormHttpMethod()} to get the "real" method.
1070
     *
1071
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1072
     */
1073
    public function FormMethod()
1074
    {
1075
        if (in_array($this->formMethod, array('GET','POST'))) {
1076
            return $this->formMethod;
1077
        } else {
1078
            return 'POST';
1079
        }
1080
    }
1081
1082
    /**
1083
     * Set the form method: GET, POST, PUT, DELETE.
1084
     *
1085
     * @param string $method
1086
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1087
     * @return $this
1088
     */
1089
    public function setFormMethod($method, $strict = null)
1090
    {
1091
        $this->formMethod = strtoupper($method);
1092
        if ($strict !== null) {
1093
            $this->setStrictFormMethodCheck($strict);
1094
        }
1095
        return $this;
1096
    }
1097
1098
    /**
1099
     * If set to true (the default), enforces the matching of the form method.
1100
     *
1101
     * This will mean two things:
1102
     *  - GET vars will be ignored by a POST form, and vice versa
1103
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1104
     *
1105
     * If set to false then the form method is only used to construct the default
1106
     * form.
1107
     *
1108
     * @param $bool boolean
1109
     * @return $this
1110
     */
1111
    public function setStrictFormMethodCheck($bool)
1112
    {
1113
        $this->strictFormMethodCheck = (bool)$bool;
1114
        return $this;
1115
    }
1116
1117
    /**
1118
     * @return boolean
1119
     */
1120
    public function getStrictFormMethodCheck()
1121
    {
1122
        return $this->strictFormMethodCheck;
1123
    }
1124
1125
    /**
1126
     * Return the form's action attribute.
1127
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1128
     *
1129
     * @return string
1130
     */
1131
    public function FormAction()
1132
    {
1133
        if ($this->formActionPath) {
1134
            return $this->formActionPath;
1135
        }
1136
1137
        // Get action from request handler link
1138
        return $this->getRequestHandler()->Link();
1139
    }
1140
1141
    /**
1142
     * Set the form action attribute to a custom URL.
1143
     *
1144
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1145
     * recommended only for situations where you have two relatively distinct
1146
     * parts of the system trying to communicate via a form post.
1147
     *
1148
     * @param string $path
1149
     * @return $this
1150
     */
1151
    public function setFormAction($path)
1152
    {
1153
        $this->formActionPath = $path;
1154
1155
        return $this;
1156
    }
1157
1158
    /**
1159
     * Returns the name of the form.
1160
     *
1161
     * @return string
1162
     */
1163
    public function FormName()
1164
    {
1165
        return $this->getTemplateHelper()->generateFormID($this);
1166
    }
1167
1168
    /**
1169
     * Set the HTML ID attribute of the form.
1170
     *
1171
     * @param string $id
1172
     * @return $this
1173
     */
1174
    public function setHTMLID($id)
1175
    {
1176
        $this->htmlID = $id;
1177
1178
        return $this;
1179
    }
1180
1181
    /**
1182
     * @return string
1183
     */
1184
    public function getHTMLID()
1185
    {
1186
        return $this->htmlID;
1187
    }
1188
1189
    /**
1190
     * Get the controller or parent request handler.
1191
     *
1192
     * @return RequestHandler
1193
     */
1194
    public function getController()
1195
    {
1196
        return $this->controller;
1197
    }
1198
1199
    /**
1200
     * Set the controller or parent request handler.
1201
     *
1202
     * @param RequestHandler $controller
1203
     * @return $this
1204
     */
1205
    public function setController(RequestHandler $controller = null)
1206
    {
1207
        $this->controller = $controller;
1208
        return $this;
1209
    }
1210
1211
    /**
1212
     * Get the name of the form.
1213
     *
1214
     * @return string
1215
     */
1216
    public function getName()
1217
    {
1218
        return $this->name;
1219
    }
1220
1221
    /**
1222
     * Set the name of the form.
1223
     *
1224
     * @param string $name
1225
     * @return Form
1226
     */
1227
    public function setName($name)
1228
    {
1229
        $this->name = $name;
1230
1231
        return $this;
1232
    }
1233
1234
    /**
1235
     * Returns an object where there is a method with the same name as each data
1236
     * field on the form.
1237
     *
1238
     * That method will return the field itself.
1239
     *
1240
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1241
     */
1242
    public function FieldMap()
1243
    {
1244
        return new Form_FieldMap($this);
1245
    }
1246
1247
    /**
1248
     * Set a message to the session, for display next time this form is shown.
1249
     *
1250
     * @param string $message the text of the message
1251
     * @param string $type Should be set to good, bad, or warning.
1252
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1253
     * Bool values will be treated as plain text flag.
1254
     */
1255
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1256
    {
1257
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1255 can also be of type boolean; however, SilverStripe\Forms\FormMessage::setMessage() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1258
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1259
        $result->addMessage($message, $type, null, $cast);
1260
        $this->setSessionValidationResult($result);
1261
    }
1262
1263
    /**
1264
     * Set an error to the session, for display next time this form is shown.
1265
     *
1266
     * @param string $message the text of the message
1267
     * @param string $type Should be set to good, bad, or warning.
1268
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1269
     * Bool values will be treated as plain text flag.
1270
     */
1271
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1272
    {
1273
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1271 can also be of type boolean; however, SilverStripe\Forms\FormMessage::setMessage() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1274
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1275
        $result->addError($message, $type, null, $cast);
1276
        $this->setSessionValidationResult($result);
1277
    }
1278
1279
    /**
1280
     * Returns the DataObject that has given this form its data
1281
     * through {@link loadDataFrom()}.
1282
     *
1283
     * @return DataObject
1284
     */
1285
    public function getRecord()
1286
    {
1287
        return $this->record;
1288
    }
1289
1290
    /**
1291
     * Get the legend value to be inserted into the
1292
     * <legend> element in Form.ss
1293
     *
1294
     * @return string
1295
     */
1296
    public function getLegend()
1297
    {
1298
        return $this->legend;
1299
    }
1300
1301
    /**
1302
     * Processing that occurs before a form is executed.
1303
     *
1304
     * This includes form validation, if it fails, we throw a ValidationException
1305
     *
1306
     * This includes form validation, if it fails, we redirect back
1307
     * to the form with appropriate error messages.
1308
     * Always return true if the current form action is exempt from validation
1309
     *
1310
     * Triggered through {@link httpSubmission()}.
1311
     *
1312
     *
1313
     * Note that CSRF protection takes place in {@link httpSubmission()},
1314
     * if it fails the form data will never reach this method.
1315
     *
1316
     * @return ValidationResult
1317
     */
1318
    public function validationResult()
1319
    {
1320
        // Automatically pass if there is no validator, or the clicked button is exempt
1321
        // Note: Soft support here for validation with absent request handler
1322
        $handler = $this->getRequestHandler();
1323
        $action = $handler ? $handler->buttonClicked() : null;
1324
        $validator = $this->getValidator();
1325
        if (!$validator || $this->actionIsValidationExempt($action)) {
0 ignored issues
show
Bug introduced by
It seems like $action defined by $handler ? $handler->buttonClicked() : null on line 1323 can be null; however, SilverStripe\Forms\Form:...ionIsValidationExempt() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1326
            return ValidationResult::create();
1327
        }
1328
1329
        // Invoke validator
1330
        $result = $validator->validate();
1331
        $this->loadMessagesFrom($result);
1332
        return $result;
1333
    }
1334
1335
    const MERGE_DEFAULT = 0;
1336
    const MERGE_CLEAR_MISSING = 1;
1337
    const MERGE_IGNORE_FALSEISH = 2;
1338
1339
    /**
1340
     * Load data from the given DataObject or array.
1341
     *
1342
     * It will call $object->MyField to get the value of MyField.
1343
     * If you passed an array, it will call $object[MyField].
1344
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1345
     * as determined by {@link FieldList->dataFields()}.
1346
     *
1347
     * By default, if a field isn't set (as determined by isset()),
1348
     * its value will not be saved to the field, retaining
1349
     * potential existing values.
1350
     *
1351
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1352
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1353
     *
1354
     * Escaping happens automatically on saving the data through
1355
     * {@link saveInto()}.
1356
     *
1357
     * @uses FieldList->dataFields()
1358
     * @uses FormField->setValue()
1359
     *
1360
     * @param array|DataObject $data
1361
     * @param int $mergeStrategy
1362
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1363
     *  what that property/key's value is.
1364
     *
1365
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1366
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1367
     *  "left alone", meaning they retain any previous value.
1368
     *
1369
     *  You can pass a bitmask here to change this behaviour.
1370
     *
1371
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1372
     *  {@link $data} are cleared.
1373
     *
1374
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1375
     *  a field's value.
1376
     *
1377
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1378
     *  CLEAR_MISSING
1379
     *
1380
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1381
     * form that has some fields that save to one object, and some that save to another.
1382
     * @return $this
1383
     */
1384
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1385
    {
1386
        if (!is_object($data) && !is_array($data)) {
1387
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1388
            return $this;
1389
        }
1390
1391
        // Handle the backwards compatible case of passing "true" as the second argument
1392
        if ($mergeStrategy === true) {
1393
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1394
        } elseif ($mergeStrategy === false) {
1395
            $mergeStrategy = 0;
1396
        }
1397
1398
        // If an object is passed, save it for historical reference through {@link getRecord()}
1399
        // Also use this to determine if we are loading a submitted form, or loading
1400
        // from a dataobject
1401
        $submitted = true;
1402
        if (is_object($data)) {
1403
            $this->record = $data;
1404
            $submitted = false;
1405
        }
1406
1407
        // dont include fields without data
1408
        $dataFields = $this->Fields()->dataFields();
1409
        if (!$dataFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataFields of type SilverStripe\Forms\FormField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1638
    }
1639
1640
    /**
1641
     * Return the default button that should be clicked when another one isn't
1642
     * available.
1643
     *
1644
     * @return FormAction
1645
     */
1646
    public function defaultAction()
1647
    {
1648
        if ($this->hasDefaultAction && $this->actions) {
1649
            return $this->actions->first();
1650
        }
1651
        return null;
1652
    }
1653
1654
    /**
1655
     * Disable the default button.
1656
     *
1657
     * Ordinarily, when a form is processed and no action_XXX button is
1658
     * available, then the first button in the actions list will be pressed.
1659
     * However, if this is "delete", for example, this isn't such a good idea.
1660
     *
1661
     * @return Form
1662
     */
1663
    public function disableDefaultAction()
1664
    {
1665
        $this->hasDefaultAction = false;
1666
1667
        return $this;
1668
    }
1669
1670
    /**
1671
     * Disable the requirement of a security token on this form instance. This
1672
     * security protects against CSRF attacks, but you should disable this if
1673
     * you don't want to tie a form to a session - eg a search form.
1674
     *
1675
     * Check for token state with {@link getSecurityToken()} and
1676
     * {@link SecurityToken->isEnabled()}.
1677
     *
1678
     * @return Form
1679
     */
1680
    public function disableSecurityToken()
1681
    {
1682
        $this->securityToken = new NullSecurityToken();
1683
1684
        return $this;
1685
    }
1686
1687
    /**
1688
     * Enable {@link SecurityToken} protection for this form instance.
1689
     *
1690
     * Check for token state with {@link getSecurityToken()} and
1691
     * {@link SecurityToken->isEnabled()}.
1692
     *
1693
     * @return Form
1694
     */
1695
    public function enableSecurityToken()
1696
    {
1697
        $this->securityToken = new SecurityToken();
1698
1699
        return $this;
1700
    }
1701
1702
    /**
1703
     * Returns the security token for this form (if any exists).
1704
     *
1705
     * Doesn't check for {@link securityTokenEnabled()}.
1706
     *
1707
     * Use {@link SecurityToken::inst()} to get a global token.
1708
     *
1709
     * @return SecurityToken|null
1710
     */
1711
    public function getSecurityToken()
1712
    {
1713
        return $this->securityToken;
1714
    }
1715
1716
    /**
1717
     * Compiles all CSS-classes.
1718
     *
1719
     * @return string
1720
     */
1721
    public function extraClass()
1722
    {
1723
        return implode(array_unique($this->extraClasses), ' ');
1724
    }
1725
1726
    /**
1727
     * Add a CSS-class to the form-container. If needed, multiple classes can
1728
     * be added by delimiting a string with spaces.
1729
     *
1730
     * @param string $class A string containing a classname or several class
1731
     *              names delimited by a single space.
1732
     * @return $this
1733
     */
1734
    public function addExtraClass($class)
1735
    {
1736
        //split at white space
1737
        $classes = preg_split('/\s+/', $class);
1738
        foreach ($classes as $class) {
1739
            //add classes one by one
1740
            $this->extraClasses[$class] = $class;
1741
        }
1742
        return $this;
1743
    }
1744
1745
    /**
1746
     * Remove a CSS-class from the form-container. Multiple class names can
1747
     * be passed through as a space delimited string
1748
     *
1749
     * @param string $class
1750
     * @return $this
1751
     */
1752
    public function removeExtraClass($class)
1753
    {
1754
        //split at white space
1755
        $classes = preg_split('/\s+/', $class);
1756
        foreach ($classes as $class) {
1757
            //unset one by one
1758
            unset($this->extraClasses[$class]);
1759
        }
1760
        return $this;
1761
    }
1762
1763
    public function debug()
1764
    {
1765
        $class = static::class;
1766
        $result = "<h3>$class</h3><ul>";
1767
        foreach ($this->fields as $field) {
1768
            $result .= "<li>$field" . $field->debug() . "</li>";
1769
        }
1770
        $result .= "</ul>";
1771
1772
        if ($this->validator) {
1773
            /** @skipUpgrade */
1774
            $result .= '<h3>'._t(__CLASS__.'.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1775
        }
1776
1777
        return $result;
1778
    }
1779
1780
    /**
1781
     * Current request handler, build by buildRequestHandler(),
1782
     * accessed by getRequestHandler()
1783
     *
1784
     * @var FormRequestHandler
1785
     */
1786
    protected $requestHandler = null;
1787
1788
    /**
1789
     * Get request handler for this form
1790
     *
1791
     * @return FormRequestHandler
1792
     */
1793
    public function getRequestHandler()
1794
    {
1795
        if (!$this->requestHandler) {
1796
            $this->requestHandler = $this->buildRequestHandler();
1797
        }
1798
        return $this->requestHandler;
1799
    }
1800
1801
    /**
1802
     * Assign a specific request handler for this form
1803
     *
1804
     * @param FormRequestHandler $handler
1805
     * @return $this
1806
     */
1807
    public function setRequestHandler(FormRequestHandler $handler)
1808
    {
1809
        $this->requestHandler = $handler;
1810
        return $this;
1811
    }
1812
1813
    /**
1814
     * Scaffold new request handler for this form
1815
     *
1816
     * @return FormRequestHandler
1817
     */
1818
    protected function buildRequestHandler()
1819
    {
1820
        return FormRequestHandler::create($this);
1821
    }
1822
}
1823