Completed
Push — master ( 644ae6...bba86b )
by Daniel
10:38
created

Form::checkAccessAction()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 6
nop 1
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Form::actionIsValidationExempt() 0 14 4
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Control\HasRequestHandler;
6
use SilverStripe\Control\HTTP;
7
use SilverStripe\Control\RequestHandler;
8
use SilverStripe\Control\Session;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Dev\Deprecation;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DataObjectInterface;
15
use SilverStripe\ORM\FieldType\DBHTMLText;
16
use SilverStripe\ORM\ValidationResult;
17
use SilverStripe\Security\NullSecurityToken;
18
use SilverStripe\Security\SecurityToken;
19
use SilverStripe\View\SSViewer;
20
use SilverStripe\View\ViewableData;
21
22
/**
23
 * Base class for all forms.
24
 * The form class is an extensible base for all forms on a SilverStripe application.  It can be used
25
 * either by extending it, and creating processor methods on the subclass, or by creating instances
26
 * of form whose actions are handled by the parent controller.
27
 *
28
 * In either case, if you want to get a form to do anything, it must be inextricably tied to a
29
 * controller.  The constructor is passed a controller and a method on that controller.  This method
30
 * should return the form object, and it shouldn't require any arguments.  Parameters, if necessary,
31
 * can be passed using the URL or get variables.  These restrictions are in place so that we can
32
 * recreate the form object upon form submission, without the use of a session, which would be too
33
 * resource-intensive.
34
 *
35
 * You will need to create at least one method for processing the submission (through {@link FormAction}).
36
 * This method will be passed two parameters: the raw request data, and the form object.
37
 * Usually you want to save data into a {@link DataObject} by using {@link saveInto()}.
38
 * If you want to process the submitted data in any way, please use {@link getData()} rather than
39
 * the raw request data.
40
 *
41
 * <h2>Validation</h2>
42
 * Each form needs some form of {@link Validator} to trigger the {@link FormField->validate()} methods for each field.
43
 * You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads
44
 * depend on it.
45
 * The default validator is an instance of {@link RequiredFields}.
46
 * If you want to enforce serverside-validation to be ignored for a specific {@link FormField},
47
 * you need to subclass it.
48
 *
49
 * <h2>URL Handling</h2>
50
 * The form class extends {@link RequestHandler}, which means it can
51
 * be accessed directly through a URL. This can be handy for refreshing
52
 * a form by ajax, or even just displaying a single form field.
53
 * You can find out the base URL for your form by looking at the
54
 * <form action="..."> value. For example, the edit form in the CMS would be located at
55
 * "admin/EditForm". This URL will render the form without its surrounding
56
 * template when called through GET instead of POST.
57
 *
58
 * By appending to this URL, you can render individual form elements
59
 * through the {@link FormField->FieldHolder()} method.
60
 * For example, the "URLSegment" field in a standard CMS form would be
61
 * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
62
 */
63
class Form extends ViewableData implements HasRequestHandler
64
{
65
    use FormMessage;
66
67
    /**
68
     * Default form Name property
69
     */
70
    const DEFAULT_NAME = 'Form';
71
72
    /**
73
     * Form submission data is URL encoded
74
     */
75
    const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
76
77
    /**
78
     * Form submission data is multipart form
79
     */
80
    const ENC_TYPE_MULTIPART  = 'multipart/form-data';
81
82
    /**
83
     * Accessed by Form.ss; modified by {@link formHtmlContent()}.
84
     * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
85
     *
86
     * @var bool
87
     */
88
    public $IncludeFormTag = true;
89
90
    /**
91
     * @var FieldList
92
     */
93
    protected $fields;
94
95
    /**
96
     * @var FieldList
97
     */
98
    protected $actions;
99
100
    /**
101
     * Parent (optional) request handler
102
     *
103
     * @var RequestHandler
104
     */
105
    protected $controller;
106
107
    /**
108
     * @var string
109
     */
110
    protected $name;
111
112
    /**
113
     * @var Validator
114
     */
115
    protected $validator;
116
117
    /**
118
     * @see setValidationResponseCallback()
119
     * @var callable
120
     */
121
    protected $validationResponseCallback;
122
123
    /**
124
     * @var string
125
     */
126
    protected $formMethod = "POST";
127
128
    /**
129
     * @var boolean
130
     */
131
    protected $strictFormMethodCheck = false;
132
133
    /**
134
     * Populated by {@link loadDataFrom()}.
135
     *
136
     * @var DataObject|null
137
     */
138
    protected $record;
139
140
    /**
141
     * Keeps track of whether this form has a default action or not.
142
     * Set to false by $this->disableDefaultAction();
143
     *
144
     * @var bool
145
     */
146
    protected $hasDefaultAction = true;
147
148
    /**
149
     * Target attribute of form-tag.
150
     * Useful to open a new window upon
151
     * form submission.
152
     *
153
     * @var string|null
154
     */
155
    protected $target;
156
157
    /**
158
     * Legend value, to be inserted into the
159
     * <legend> element before the <fieldset>
160
     * in Form.ss template.
161
     *
162
     * @var string|null
163
     */
164
    protected $legend;
165
166
    /**
167
     * The SS template to render this form HTML into.
168
     * Default is "Form", but this can be changed to
169
     * another template for customisation.
170
     *
171
     * @see Form->setTemplate()
172
     * @var string|null
173
     */
174
    protected $template;
175
176
    /**
177
     * Should we redirect the user back down to the
178
     * the form on validation errors rather then just the page
179
     *
180
     * @var bool
181
     */
182
    protected $redirectToFormOnValidationError = false;
183
184
    /**
185
     * @var bool
186
     */
187
    protected $security = true;
188
189
    /**
190
     * @var SecurityToken|null
191
     */
192
    protected $securityToken = null;
193
194
    /**
195
     * List of additional CSS classes for the form tag.
196
     *
197
     * @var array
198
     */
199
    protected $extraClasses = array();
200
201
    /**
202
     * @config
203
     * @var array $default_classes The default classes to apply to the Form
204
     */
205
    private static $default_classes = array();
206
207
    /**
208
     * @var string|null
209
     */
210
    protected $encType;
211
212
    /**
213
     * Any custom form attributes set through {@link setAttributes()}.
214
     * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
215
     *
216
     * @var array
217
     */
218
    protected $attributes = array();
219
220
    /**
221
     * @var array
222
     */
223
    protected $validationExemptActions = array();
224
225
    /**
226
     * @config
227
     * @var array
228
     */
229
    private static $casting = array(
230
        'AttributesHTML' => 'HTMLFragment',
231
        'FormAttributes' => 'HTMLFragment',
232
        'FormName' => 'Text',
233
        'Legend' => 'HTMLFragment',
234
    );
235
236
    /**
237
     * @var FormTemplateHelper
238
     */
239
    private $templateHelper = null;
240
241
    /**
242
     * HTML ID for this form.
243
     *
244
     * @var string
245
     */
246
    private $htmlID = null;
247
248
    /**
249
     * Custom form action path, if not linking to itself.
250
     * E.g. could be used to post to an external link
251
     *
252
     * @var string
253
     */
254
    protected $formActionPath = false;
255
256
    /**
257
     * @var bool
258
     */
259
    protected $securityTokenAdded = false;
260
261
    /**
262
     * Create a new form, with the given fields an action buttons.
263
     *
264
     * @param RequestHandler $controller Optional parent request handler
265
     * @param string $name The method on the controller that will return this form object.
266
     * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
267
     * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
268
     *                           {@link FormAction} objects
269
     * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
270
     */
271
    public function __construct(
272
        RequestHandler $controller = null,
273
        $name = self::DEFAULT_NAME,
274
        FieldList $fields = null,
275
        FieldList $actions = null,
276
        Validator $validator = null
277
    ) {
278
        parent::__construct();
279
280
        $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...
281
        $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...
282
283
        $this->fields = $fields;
284
        $this->actions = $actions;
285
        $this->setController($controller);
286
        $this->setName($name);
287
288
        // Form validation
289
        $this->validator = ($validator) ? $validator : new RequiredFields();
290
        $this->validator->setForm($this);
291
292
        // Form error controls
293
        $this->restoreFormState();
294
295
        // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
296
        // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
297
        if (ClassInfo::hasMethod($controller, 'securityTokenEnabled')) {
0 ignored issues
show
Bug introduced by
It seems like $controller defined by parameter $controller on line 272 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...
298
            $securityEnabled = $controller->securityTokenEnabled();
299
        } else {
300
            $securityEnabled = SecurityToken::is_enabled();
301
        }
302
303
        $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
304
305
        $this->setupDefaultClasses();
306
    }
307
308
    /**
309
     * Load form state from session state
310
     *
311
     * @return $this
312
     */
313
    public function restoreFormState()
314
    {
315
        // Restore messages
316
        $result = $this->getSessionValidationResult();
317
        if (isset($result)) {
318
            $this->loadMessagesFrom($result);
319
        }
320
321
        // load data in from previous submission upon error
322
        $data = $this->getSessionData();
323
        if (isset($data)) {
324
            $this->loadDataFrom($data);
325
        }
326
        return $this;
327
    }
328
329
    /**
330
     * Flush persistant form state details
331
     *
332
     * @return $this
333
     */
334
    public function clearFormState()
335
    {
336
        Session::clear("FormInfo.{$this->FormName()}.result");
337
        Session::clear("FormInfo.{$this->FormName()}.data");
338
        return $this;
339
    }
340
341
    /**
342
     * Return any form data stored in the session
343
     *
344
     * @return array
345
     */
346
    public function getSessionData()
347
    {
348
        return Session::get("FormInfo.{$this->FormName()}.data");
349
    }
350
351
    /**
352
     * Store the given form data in the session
353
     *
354
     * @param array $data
355
     * @return $this
356
     */
357
    public function setSessionData($data)
358
    {
359
        Session::set("FormInfo.{$this->FormName()}.data", $data);
360
        return $this;
361
    }
362
363
    /**
364
     * Return any ValidationResult instance stored for this object
365
     *
366
     * @return ValidationResult The ValidationResult object stored in the session
367
     */
368
    public function getSessionValidationResult()
369
    {
370
        $resultData = Session::get("FormInfo.{$this->FormName()}.result");
371
        if (isset($resultData)) {
372
            return unserialize($resultData);
373
        }
374
        return null;
375
    }
376
377
    /**
378
     * Sets the ValidationResult in the session to be used with the next view of this form.
379
     * @param ValidationResult $result The result to save
380
     * @param bool $combineWithExisting If true, then this will be added to the existing result.
381
     * @return $this
382
     */
383
    public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
384
    {
385
        // Combine with existing result
386
        if ($combineWithExisting) {
387
            $existingResult = $this->getSessionValidationResult();
388
            if ($existingResult) {
389
                if ($result) {
390
                    $existingResult->combineAnd($result);
391
                } else {
392
                    $result = $existingResult;
393
                }
394
            }
395
        }
396
397
        // Serialise
398
        $resultData = $result ? serialize($result) : null;
399
        Session::set("FormInfo.{$this->FormName()}.result", $resultData);
400
        return $this;
401
    }
402
403
    /**
404
     * Clear form message (and in session)
405
     *
406
     * @return $this
407
     */
408
    public function clearMessage()
409
    {
410
        $this->setMessage(null);
411
        $this->clearFormState();
412
        return $this;
413
    }
414
415
    /**
416
     * Populate this form with messages from the given ValidationResult.
417
     * Note: This will not clear any pre-existing messages
418
     *
419
     * @param ValidationResult $result
420
     * @return $this
421
     */
422
    public function loadMessagesFrom($result)
423
    {
424
        // Set message on either a field or the parent form
425
        foreach ($result->getMessages() as $message) {
426
            $fieldName = $message['fieldName'];
427
            if ($fieldName) {
428
                $owner = $this->fields->dataFieldByName($fieldName) ?: $this;
429
            } else {
430
                $owner = $this;
431
            }
432
            $owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
433
        }
434
        return $this;
435
    }
436
437
    /**
438
     * Set message on a given field name. This message will not persist via redirect.
439
     *
440
     * @param string $fieldName
441
     * @param string $message
442
     * @param string $messageType
443
     * @param string $messageCast
444
     * @return $this
445
     */
446
    public function setFieldMessage(
447
        $fieldName,
448
        $message,
449
        $messageType = ValidationResult::TYPE_ERROR,
450
        $messageCast = ValidationResult::CAST_TEXT
451
    ) {
452
        $field = $this->fields->dataFieldByName($fieldName);
453
        if ($field) {
454
            $field->setMessage($message, $messageType, $messageCast);
455
        }
456
        return $this;
457
    }
458
459
    public function castingHelper($field)
460
    {
461
        // Override casting for field message
462
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
463
            return $helper;
464
        }
465
        return parent::castingHelper($field);
466
    }
467
468
    /**
469
     * set up the default classes for the form. This is done on construct so that the default classes can be removed
470
     * after instantiation
471
     */
472
    protected function setupDefaultClasses()
473
    {
474
        $defaultClasses = self::config()->get('default_classes');
475
        if ($defaultClasses) {
476
            foreach ($defaultClasses as $class) {
477
                $this->addExtraClass($class);
478
            }
479
        }
480
    }
481
482
    /**
483
     * @return callable
484
     */
485
    public function getValidationResponseCallback()
486
    {
487
        return $this->validationResponseCallback;
488
    }
489
490
    /**
491
     * Overrules validation error behaviour in {@link httpSubmission()}
492
     * when validation has failed. Useful for optional handling of a certain accepted content type.
493
     *
494
     * The callback can opt out of handling specific responses by returning NULL,
495
     * in which case the default form behaviour will kick in.
496
     *
497
     * @param $callback
498
     * @return self
499
     */
500
    public function setValidationResponseCallback($callback)
501
    {
502
        $this->validationResponseCallback = $callback;
503
504
        return $this;
505
    }
506
    /**
507
     * Convert this form into a readonly form
508
     */
509
    public function makeReadonly()
510
    {
511
        $this->transform(new ReadonlyTransformation());
512
    }
513
514
    /**
515
     * Set whether the user should be redirected back down to the
516
     * form on the page upon validation errors in the form or if
517
     * they just need to redirect back to the page
518
     *
519
     * @param bool $bool Redirect to form on error?
520
     * @return $this
521
     */
522
    public function setRedirectToFormOnValidationError($bool)
523
    {
524
        $this->redirectToFormOnValidationError = $bool;
525
        return $this;
526
    }
527
528
    /**
529
     * Get whether the user should be redirected back down to the
530
     * form on the page upon validation errors
531
     *
532
     * @return bool
533
     */
534
    public function getRedirectToFormOnValidationError()
535
    {
536
        return $this->redirectToFormOnValidationError;
537
    }
538
539
    /**
540
     * @param FormTransformation $trans
541
     */
542
    public function transform(FormTransformation $trans)
543
    {
544
        $newFields = new FieldList();
545
        foreach ($this->fields as $field) {
546
            $newFields->push($field->transform($trans));
547
        }
548
        $this->fields = $newFields;
549
550
        $newActions = new FieldList();
551
        foreach ($this->actions as $action) {
552
            $newActions->push($action->transform($trans));
553
        }
554
        $this->actions = $newActions;
555
556
557
        // We have to remove validation, if the fields are not editable ;-)
558
        if ($this->validator) {
559
            $this->validator->removeValidation();
560
        }
561
    }
562
563
    /**
564
     * Get the {@link Validator} attached to this form.
565
     * @return Validator
566
     */
567
    public function getValidator()
568
    {
569
        return $this->validator;
570
    }
571
572
    /**
573
     * Set the {@link Validator} on this form.
574
     * @param Validator $validator
575
     * @return $this
576
     */
577
    public function setValidator(Validator $validator)
578
    {
579
        if ($validator) {
580
            $this->validator = $validator;
581
            $this->validator->setForm($this);
582
        }
583
        return $this;
584
    }
585
586
    /**
587
     * Remove the {@link Validator} from this from.
588
     */
589
    public function unsetValidator()
590
    {
591
        $this->validator = null;
592
        return $this;
593
    }
594
595
    /**
596
     * Set actions that are exempt from validation
597
     *
598
     * @param array
599
     * @return $this
600
     */
601
    public function setValidationExemptActions($actions)
602
    {
603
        $this->validationExemptActions = $actions;
604
        return $this;
605
    }
606
607
    /**
608
     * Get a list of actions that are exempt from validation
609
     *
610
     * @return array
611
     */
612
    public function getValidationExemptActions()
613
    {
614
        return $this->validationExemptActions;
615
    }
616
617
    /**
618
     * Passed a FormAction, returns true if that action is exempt from Form validation
619
     *
620
     * @param FormAction $action
621
     * @return bool
622
     */
623
    public function actionIsValidationExempt($action)
624
    {
625
        // Non-actions don't bypass validation
626
        if (!$action) {
627
            return false;
628
        }
629
        if ($action->getValidationExempt()) {
630
            return true;
631
        }
632
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
633
            return true;
634
        }
635
        return false;
636
    }
637
638
    /**
639
     * Generate extra special fields - namely the security token field (if required).
640
     *
641
     * @return FieldList
642
     */
643
    public function getExtraFields()
644
    {
645
        $extraFields = new FieldList();
646
647
        $token = $this->getSecurityToken();
648
        if ($token) {
649
            $tokenField = $token->updateFieldSet($this->fields);
650
            if ($tokenField) {
651
                $tokenField->setForm($this);
652
            }
653
        }
654
        $this->securityTokenAdded = true;
655
656
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
657
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
658
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
659
            $methodField->setForm($this);
660
            $extraFields->push($methodField);
661
        }
662
663
        return $extraFields;
664
    }
665
666
    /**
667
     * Return the form's fields - used by the templates
668
     *
669
     * @return FieldList The form fields
670
     */
671
    public function Fields()
672
    {
673
        foreach ($this->getExtraFields() as $field) {
674
            if (!$this->fields->fieldByName($field->getName())) {
675
                $this->fields->push($field);
676
            }
677
        }
678
679
        return $this->fields;
680
    }
681
682
    /**
683
     * Return all <input type="hidden"> fields
684
     * in a form - including fields nested in {@link CompositeFields}.
685
     * Useful when doing custom field layouts.
686
     *
687
     * @return FieldList
688
     */
689
    public function HiddenFields()
690
    {
691
        return $this->Fields()->HiddenFields();
692
    }
693
694
    /**
695
     * Return all fields except for the hidden fields.
696
     * Useful when making your own simplified form layouts.
697
     */
698
    public function VisibleFields()
699
    {
700
        return $this->Fields()->VisibleFields();
701
    }
702
703
    /**
704
     * Setter for the form fields.
705
     *
706
     * @param FieldList $fields
707
     * @return $this
708
     */
709
    public function setFields($fields)
710
    {
711
        $this->fields = $fields;
712
        return $this;
713
    }
714
715
    /**
716
     * Return the form's action buttons - used by the templates
717
     *
718
     * @return FieldList The action list
719
     */
720
    public function Actions()
721
    {
722
        return $this->actions;
723
    }
724
725
    /**
726
     * Setter for the form actions.
727
     *
728
     * @param FieldList $actions
729
     * @return $this
730
     */
731
    public function setActions($actions)
732
    {
733
        $this->actions = $actions;
734
        return $this;
735
    }
736
737
    /**
738
     * Unset all form actions
739
     */
740
    public function unsetAllActions()
741
    {
742
        $this->actions = new FieldList();
743
        return $this;
744
    }
745
746
    /**
747
     * @param string $name
748
     * @param string $value
749
     * @return $this
750
     */
751
    public function setAttribute($name, $value)
752
    {
753
        $this->attributes[$name] = $value;
754
        return $this;
755
    }
756
757
    /**
758
     * @param string $name
759
     * @return string
760
     */
761
    public function getAttribute($name)
762
    {
763
        if (isset($this->attributes[$name])) {
764
            return $this->attributes[$name];
765
        }
766
        return null;
767
    }
768
769
    /**
770
     * @return array
771
     */
772
    public function getAttributes()
773
    {
774
        $attrs = array(
775
            'id' => $this->FormName(),
776
            'action' => $this->FormAction(),
777
            'method' => $this->FormMethod(),
778
            'enctype' => $this->getEncType(),
779
            'target' => $this->target,
780
            'class' => $this->extraClass(),
781
        );
782
783
        if ($this->validator && $this->validator->getErrors()) {
784
            if (!isset($attrs['class'])) {
785
                $attrs['class'] = '';
786
            }
787
            $attrs['class'] .= ' validationerror';
788
        }
789
790
        $attrs = array_merge($attrs, $this->attributes);
791
792
        return $attrs;
793
    }
794
795
    /**
796
     * Return the attributes of the form tag - used by the templates.
797
     *
798
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
799
     * If at least one argument is passed as a string, all arguments act as excludes by name.
800
     *
801
     * @return string HTML attributes, ready for insertion into an HTML tag
802
     */
803
    public function getAttributesHTML($attrs = null)
804
    {
805
        $exclude = (is_string($attrs)) ? func_get_args() : null;
806
807
        // Figure out if we can cache this form
808
        // - forms with validation shouldn't be cached, cos their error messages won't be shown
809
        // - forms with security tokens shouldn't be cached because security tokens expire
810
        $needsCacheDisabled = false;
811
        if ($this->getSecurityToken()->isEnabled()) {
812
            $needsCacheDisabled = true;
813
        }
814
        if ($this->FormMethod() != 'GET') {
815
            $needsCacheDisabled = true;
816
        }
817
        if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
818
            $needsCacheDisabled = true;
819
        }
820
821
        // If we need to disable cache, do it
822
        if ($needsCacheDisabled) {
823
            HTTP::set_cache_age(0);
824
        }
825
826
        $attrs = $this->getAttributes();
827
828
        // Remove empty
829
        $attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);'));
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
830
831
        // Remove excluded
832
        if ($exclude) {
833
            $attrs = array_diff_key($attrs, array_flip($exclude));
834
        }
835
836
        // Prepare HTML-friendly 'method' attribute (lower-case)
837
        if (isset($attrs['method'])) {
838
            $attrs['method'] = strtolower($attrs['method']);
839
        }
840
841
        // Create markup
842
        $parts = array();
843
        foreach ($attrs as $name => $value) {
844
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
845
        }
846
847
        return implode(' ', $parts);
848
    }
849
850
    public function FormAttributes()
851
    {
852
        return $this->getAttributesHTML();
853
    }
854
855
    /**
856
     * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
857
     * another frame
858
    *
859
     * @param string|FormTemplateHelper
860
    */
861
    public function setTemplateHelper($helper)
862
    {
863
        $this->templateHelper = $helper;
864
    }
865
866
    /**
867
     * Return a {@link FormTemplateHelper} for this form. If one has not been
868
     * set, return the default helper.
869
     *
870
     * @return FormTemplateHelper
871
     */
872
    public function getTemplateHelper()
873
    {
874
        if ($this->templateHelper) {
875
            if (is_string($this->templateHelper)) {
876
                return Injector::inst()->get($this->templateHelper);
877
            }
878
879
            return $this->templateHelper;
880
        }
881
882
        return FormTemplateHelper::singleton();
883
    }
884
885
    /**
886
     * Set the target of this form to any value - useful for opening the form
887
     * contents in a new window or refreshing another frame.
888
     *
889
     * @param string $target The value of the target
890
     * @return $this
891
     */
892
    public function setTarget($target)
893
    {
894
        $this->target = $target;
895
896
        return $this;
897
    }
898
899
    /**
900
     * Set the legend value to be inserted into
901
     * the <legend> element in the Form.ss template.
902
     * @param string $legend
903
     * @return $this
904
     */
905
    public function setLegend($legend)
906
    {
907
        $this->legend = $legend;
908
        return $this;
909
    }
910
911
    /**
912
     * Set the SS template that this form should use
913
     * to render with. The default is "Form".
914
     *
915
     * @param string $template The name of the template (without the .ss extension)
916
     * @return $this
917
     */
918
    public function setTemplate($template)
919
    {
920
        $this->template = $template;
921
        return $this;
922
    }
923
924
    /**
925
     * Return the template to render this form with.
926
     *
927
     * @return string
928
     */
929
    public function getTemplate()
930
    {
931
        return $this->template;
932
    }
933
934
    /**
935
     * Returs the ordered list of preferred templates for rendering this form
936
     * If the template isn't set, then default to the
937
     * form class name e.g "Form".
938
     *
939
     * @return array
940
     */
941
    public function getTemplates()
942
    {
943
        $templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
944
        // Prefer any custom template
945
        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...
946
            array_unshift($templates, $this->getTemplate());
947
        }
948
        return $templates;
949
    }
950
951
    /**
952
     * Returns the encoding type for the form.
953
     *
954
     * By default this will be URL encoded, unless there is a file field present
955
     * in which case multipart is used. You can also set the enc type using
956
     * {@link setEncType}.
957
     */
958
    public function getEncType()
959
    {
960
        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...
961
            return $this->encType;
962
        }
963
964
        if ($fields = $this->fields->dataFields()) {
965
            foreach ($fields as $field) {
966
                if ($field instanceof FileField) {
967
                    return self::ENC_TYPE_MULTIPART;
968
                }
969
            }
970
        }
971
972
        return self::ENC_TYPE_URLENCODED;
973
    }
974
975
    /**
976
     * Sets the form encoding type. The most common encoding types are defined
977
     * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
978
     *
979
     * @param string $encType
980
     * @return $this
981
     */
982
    public function setEncType($encType)
983
    {
984
        $this->encType = $encType;
985
        return $this;
986
    }
987
988
    /**
989
     * Returns the real HTTP method for the form:
990
     * GET, POST, PUT, DELETE or HEAD.
991
     * As most browsers only support GET and POST in
992
     * form submissions, all other HTTP methods are
993
     * added as a hidden field "_method" that
994
     * gets evaluated in {@link Director::direct()}.
995
     * See {@link FormMethod()} to get a HTTP method
996
     * for safe insertion into a <form> tag.
997
     *
998
     * @return string HTTP method
999
     */
1000
    public function FormHttpMethod()
1001
    {
1002
        return $this->formMethod;
1003
    }
1004
1005
    /**
1006
     * Returns the form method to be used in the <form> tag.
1007
     * See {@link FormHttpMethod()} to get the "real" method.
1008
     *
1009
     * @return string Form HTTP method restricted to 'GET' or 'POST'
1010
     */
1011
    public function FormMethod()
1012
    {
1013
        if (in_array($this->formMethod, array('GET','POST'))) {
1014
            return $this->formMethod;
1015
        } else {
1016
            return 'POST';
1017
        }
1018
    }
1019
1020
    /**
1021
     * Set the form method: GET, POST, PUT, DELETE.
1022
     *
1023
     * @param string $method
1024
     * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
1025
     * @return $this
1026
     */
1027
    public function setFormMethod($method, $strict = null)
1028
    {
1029
        $this->formMethod = strtoupper($method);
1030
        if ($strict !== null) {
1031
            $this->setStrictFormMethodCheck($strict);
1032
        }
1033
        return $this;
1034
    }
1035
1036
    /**
1037
     * If set to true, enforce the matching of the form method.
1038
     *
1039
     * This will mean two things:
1040
     *  - GET vars will be ignored by a POST form, and vice versa
1041
     *  - A submission where the HTTP method used doesn't match the form will return a 400 error.
1042
     *
1043
     * If set to false (the default), then the form method is only used to construct the default
1044
     * form.
1045
     *
1046
     * @param $bool boolean
1047
     * @return $this
1048
     */
1049
    public function setStrictFormMethodCheck($bool)
1050
    {
1051
        $this->strictFormMethodCheck = (bool)$bool;
1052
        return $this;
1053
    }
1054
1055
    /**
1056
     * @return boolean
1057
     */
1058
    public function getStrictFormMethodCheck()
1059
    {
1060
        return $this->strictFormMethodCheck;
1061
    }
1062
1063
    /**
1064
     * Return the form's action attribute.
1065
     * This is build by adding an executeForm get variable to the parent controller's Link() value
1066
     *
1067
     * @return string
1068
     */
1069
    public function FormAction()
1070
    {
1071
        if ($this->formActionPath) {
1072
            return $this->formActionPath;
1073
        }
1074
1075
        // Get action from request handler link
1076
        return $this->getRequestHandler()->Link();
1077
    }
1078
1079
    /**
1080
     * Set the form action attribute to a custom URL.
1081
     *
1082
     * Note: For "normal" forms, you shouldn't need to use this method.  It is
1083
     * recommended only for situations where you have two relatively distinct
1084
     * parts of the system trying to communicate via a form post.
1085
     *
1086
     * @param string $path
1087
     * @return $this
1088
     */
1089
    public function setFormAction($path)
1090
    {
1091
        $this->formActionPath = $path;
1092
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * Returns the name of the form.
1098
     *
1099
     * @return string
1100
     */
1101
    public function FormName()
1102
    {
1103
        return $this->getTemplateHelper()->generateFormID($this);
1104
    }
1105
1106
    /**
1107
     * Set the HTML ID attribute of the form.
1108
     *
1109
     * @param string $id
1110
     * @return $this
1111
     */
1112
    public function setHTMLID($id)
1113
    {
1114
        $this->htmlID = $id;
1115
1116
        return $this;
1117
    }
1118
1119
    /**
1120
     * @return string
1121
     */
1122
    public function getHTMLID()
1123
    {
1124
        return $this->htmlID;
1125
    }
1126
1127
    /**
1128
     * Get the controller or parent request handler.
1129
     *
1130
     * @return RequestHandler
1131
     */
1132
    public function getController()
1133
    {
1134
        return $this->controller;
1135
    }
1136
1137
    /**
1138
     * Set the controller or parent request handler.
1139
     *
1140
     * @param RequestHandler $controller
1141
     * @return $this
1142
     */
1143
    public function setController(RequestHandler $controller = null)
1144
    {
1145
        $this->controller = $controller;
1146
        return $this;
1147
    }
1148
1149
    /**
1150
     * Get the name of the form.
1151
     *
1152
     * @return string
1153
     */
1154
    public function getName()
1155
    {
1156
        return $this->name;
1157
    }
1158
1159
    /**
1160
     * Set the name of the form.
1161
     *
1162
     * @param string $name
1163
     * @return Form
1164
     */
1165
    public function setName($name)
1166
    {
1167
        $this->name = $name;
1168
1169
        return $this;
1170
    }
1171
1172
    /**
1173
     * Returns an object where there is a method with the same name as each data
1174
     * field on the form.
1175
     *
1176
     * That method will return the field itself.
1177
     *
1178
     * It means that you can execute $firstName = $form->FieldMap()->FirstName()
1179
     */
1180
    public function FieldMap()
1181
    {
1182
        return new Form_FieldMap($this);
1183
    }
1184
1185
    /**
1186
     * Set a message to the session, for display next time this form is shown.
1187
     *
1188
     * @param string $message the text of the message
1189
     * @param string $type Should be set to good, bad, or warning.
1190
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1191
     * Bool values will be treated as plain text flag.
1192
     */
1193
    public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1194
    {
1195
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1193 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...
1196
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1197
        $result->addMessage($message, $type, null, $cast);
1198
        $this->setSessionValidationResult($result);
1199
    }
1200
1201
    /**
1202
     * Set an error to the session, for display next time this form is shown.
1203
     *
1204
     * @param string $message the text of the message
1205
     * @param string $type Should be set to good, bad, or warning.
1206
     * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
1207
     * Bool values will be treated as plain text flag.
1208
     */
1209
    public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
1210
    {
1211
        $this->setMessage($message, $type, $cast);
0 ignored issues
show
Bug introduced by
It seems like $cast defined by parameter $cast on line 1209 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...
1212
        $result = $this->getSessionValidationResult() ?: ValidationResult::create();
1213
        $result->addError($message, $type, null, $cast);
1214
        $this->setSessionValidationResult($result);
1215
    }
1216
1217
    /**
1218
     * Returns the DataObject that has given this form its data
1219
     * through {@link loadDataFrom()}.
1220
     *
1221
     * @return DataObject
1222
     */
1223
    public function getRecord()
1224
    {
1225
        return $this->record;
1226
    }
1227
1228
    /**
1229
     * Get the legend value to be inserted into the
1230
     * <legend> element in Form.ss
1231
     *
1232
     * @return string
1233
     */
1234
    public function getLegend()
1235
    {
1236
        return $this->legend;
1237
    }
1238
1239
    /**
1240
     * Processing that occurs before a form is executed.
1241
     *
1242
     * This includes form validation, if it fails, we throw a ValidationException
1243
     *
1244
     * This includes form validation, if it fails, we redirect back
1245
     * to the form with appropriate error messages.
1246
     * Always return true if the current form action is exempt from validation
1247
     *
1248
     * Triggered through {@link httpSubmission()}.
1249
     *
1250
     *
1251
     * Note that CSRF protection takes place in {@link httpSubmission()},
1252
     * if it fails the form data will never reach this method.
1253
     *
1254
     * @return ValidationResult
1255
     */
1256
    public function validationResult()
1257
    {
1258
        // Automatically pass if there is no validator, or the clicked button is exempt
1259
        // Note: Soft support here for validation with absent request handler
1260
        $handler = $this->getRequestHandler();
1261
        $action = $handler ? $handler->buttonClicked() : null;
1262
        $validator = $this->getValidator();
1263
        if (!$validator || $this->actionIsValidationExempt($action)) {
0 ignored issues
show
Bug introduced by
It seems like $action defined by $handler ? $handler->buttonClicked() : null on line 1261 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...
1264
            return ValidationResult::create();
1265
        }
1266
1267
        // Invoke validator
1268
        $result = $validator->validate();
1269
        $this->loadMessagesFrom($result);
1270
        return $result;
1271
    }
1272
1273
    const MERGE_DEFAULT = 0;
1274
    const MERGE_CLEAR_MISSING = 1;
1275
    const MERGE_IGNORE_FALSEISH = 2;
1276
1277
    /**
1278
     * Load data from the given DataObject or array.
1279
     *
1280
     * It will call $object->MyField to get the value of MyField.
1281
     * If you passed an array, it will call $object[MyField].
1282
     * Doesn't save into dataless FormFields ({@link DatalessField}),
1283
     * as determined by {@link FieldList->dataFields()}.
1284
     *
1285
     * By default, if a field isn't set (as determined by isset()),
1286
     * its value will not be saved to the field, retaining
1287
     * potential existing values.
1288
     *
1289
     * Passed data should not be escaped, and is saved to the FormField instances unescaped.
1290
     * Escaping happens automatically on saving the data through {@link saveInto()}.
1291
     *
1292
     * Escaping happens automatically on saving the data through
1293
     * {@link saveInto()}.
1294
     *
1295
     * @uses FieldList->dataFields()
1296
     * @uses FormField->setValue()
1297
     *
1298
     * @param array|DataObject $data
1299
     * @param int $mergeStrategy
1300
     *  For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
1301
     *  what that property/key's value is.
1302
     *
1303
     *  By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
1304
     *  value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
1305
     *  "left alone", meaning they retain any previous value.
1306
     *
1307
     *  You can pass a bitmask here to change this behaviour.
1308
     *
1309
     *  Passing CLEAR_MISSING means that any fields that don't match any property/key in
1310
     *  {@link $data} are cleared.
1311
     *
1312
     *  Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
1313
     *  a field's value.
1314
     *
1315
     *  For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
1316
     *  CLEAR_MISSING
1317
     *
1318
     * @param array $fieldList An optional list of fields to process.  This can be useful when you have a
1319
     * form that has some fields that save to one object, and some that save to another.
1320
     * @return $this
1321
     */
1322
    public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
1323
    {
1324
        if (!is_object($data) && !is_array($data)) {
1325
            user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
1326
            return $this;
1327
        }
1328
1329
        // Handle the backwards compatible case of passing "true" as the second argument
1330
        if ($mergeStrategy === true) {
1331
            $mergeStrategy = self::MERGE_CLEAR_MISSING;
1332
        } elseif ($mergeStrategy === false) {
1333
            $mergeStrategy = 0;
1334
        }
1335
1336
        // If an object is passed, save it for historical reference through {@link getRecord()}
1337
        // Also use this to determine if we are loading a submitted form, or loading
1338
        // from a dataobject
1339
        $submitted = true;
1340
        if (is_object($data)) {
1341
            $this->record = $data;
1342
            $submitted = false;
1343
        }
1344
1345
        // dont include fields without data
1346
        $dataFields = $this->Fields()->dataFields();
1347
        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...
1348
            return $this;
1349
        }
1350
1351
        /** @var FormField $field */
1352
        foreach ($dataFields as $field) {
1353
            $name = $field->getName();
1354
1355
            // Skip fields that have been excluded
1356
            if ($fieldList && !in_array($name, $fieldList)) {
1357
                continue;
1358
            }
1359
1360
            // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
1361
            if (is_array($data) && isset($data[$name . '_unchanged'])) {
1362
                continue;
1363
            }
1364
1365
            // Does this property exist on $data?
1366
            $exists = false;
1367
            // The value from $data for this field
1368
            $val = null;
1369
1370
            if (is_object($data)) {
1371
                $exists = (
1372
                    isset($data->$name) ||
1373
                    $data->hasMethod($name) ||
1374
                    ($data->hasMethod('hasField') && $data->hasField($name))
1375
                );
1376
1377
                if ($exists) {
1378
                    $val = $data->__get($name);
1379
                }
1380
            } elseif (is_array($data)) {
1381
                if (array_key_exists($name, $data)) {
1382
                    $exists = true;
1383
                    $val = $data[$name];
1384
                } elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
1385
                    // If field is in array-notation we need to access nested data
1386
                    //discard first match which is just the whole string
1387
                    array_shift($matches);
1388
                    $keys = array_pop($matches);
1389
                    $name = array_shift($matches);
1390
                    $name = array_shift($name);
1391
                    if (array_key_exists($name, $data)) {
1392
                        $tmpData = &$data[$name];
1393
                        // drill down into the data array looking for the corresponding value
1394
                        foreach ($keys as $arrayKey) {
1395
                            if ($arrayKey !== '') {
1396
                                $tmpData = &$tmpData[$arrayKey];
1397
                            } else {
1398
                                //empty square brackets means new array
1399
                                if (is_array($tmpData)) {
1400
                                    $tmpData = array_shift($tmpData);
1401
                                }
1402
                            }
1403
                        }
1404
                        if ($tmpData) {
1405
                            $val = $tmpData;
1406
                            $exists = true;
1407
                        }
1408
                    }
1409
                }
1410
            }
1411
1412
            // save to the field if either a value is given, or loading of blank/undefined values is forced
1413
            $setValue = false;
1414
            if ($exists) {
1415
                if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
1416
                    $setValue = true;
1417
                }
1418
            } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
1419
                $setValue = true;
1420
            }
1421
1422
            // pass original data as well so composite fields can act on the additional information
1423
            if ($setValue) {
1424
                if ($submitted) {
1425
                    $field->setSubmittedValue($val, $data);
1426
                } else {
1427
                    $field->setValue($val, $data);
1428
                }
1429
            }
1430
        }
1431
        return $this;
1432
    }
1433
1434
    /**
1435
     * Save the contents of this form into the given data object.
1436
     * It will make use of setCastedField() to do this.
1437
     *
1438
     * @param DataObjectInterface $dataObject The object to save data into
1439
     * @param FieldList $fieldList An optional list of fields to process.  This can be useful when you have a
1440
     * form that has some fields that save to one object, and some that save to another.
1441
     */
1442
    public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
1443
    {
1444
        $dataFields = $this->fields->saveableFields();
1445
        $lastField = null;
1446
        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...
1447
            foreach ($dataFields as $field) {
1448
            // Skip fields that have been excluded
1449
                if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
1450
                    continue;
1451
                }
1452
1453
                $saveMethod = "save{$field->getName()}";
1454
                if ($field->getName() == "ClassName") {
1455
                    $lastField = $field;
1456
                } elseif ($dataObject->hasMethod($saveMethod)) {
1457
                    $dataObject->$saveMethod($field->dataValue());
1458
                } elseif ($field->getName() !== "ID") {
1459
                    $field->saveInto($dataObject);
1460
                }
1461
            }
1462
        }
1463
        if ($lastField) {
1464
            $lastField->saveInto($dataObject);
1465
        }
1466
    }
1467
1468
    /**
1469
     * Get the submitted data from this form through
1470
     * {@link FieldList->dataFields()}, which filters out
1471
     * any form-specific data like form-actions.
1472
     * Calls {@link FormField->dataValue()} on each field,
1473
     * which returns a value suitable for insertion into a DataObject
1474
     * property.
1475
     *
1476
     * @return array
1477
     */
1478
    public function getData()
1479
    {
1480
        $dataFields = $this->fields->dataFields();
1481
        $data = array();
1482
1483
        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...
1484
            /** @var FormField $field */
1485
            foreach ($dataFields as $field) {
1486
                if ($field->getName()) {
1487
                    $data[$field->getName()] = $field->dataValue();
1488
                }
1489
            }
1490
        }
1491
1492
        return $data;
1493
    }
1494
1495
    /**
1496
     * Return a rendered version of this form.
1497
     *
1498
     * This is returned when you access a form as $FormObject rather
1499
     * than <% with FormObject %>
1500
     *
1501
     * @return DBHTMLText
1502
     */
1503
    public function forTemplate()
1504
    {
1505
        $return = $this->renderWith($this->getTemplates());
1506
1507
        // Now that we're rendered, clear message
1508
        $this->clearMessage();
1509
1510
        return $return;
1511
    }
1512
1513
    /**
1514
     * Return a rendered version of this form, suitable for ajax post-back.
1515
     *
1516
     * It triggers slightly different behaviour, such as disabling the rewriting
1517
     * of # links.
1518
     *
1519
     * @return DBHTMLText
1520
     */
1521
    public function forAjaxTemplate()
1522
    {
1523
        $view = new SSViewer($this->getTemplates());
1524
1525
        $return = $view->dontRewriteHashlinks()->process($this);
1526
1527
        // Now that we're rendered, clear message
1528
        $this->clearMessage();
1529
1530
        return $return;
1531
    }
1532
1533
    /**
1534
     * Returns an HTML rendition of this form, without the <form> tag itself.
1535
     *
1536
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1537
     * and _form_enctype.  These are the attributes of the form.  These fields
1538
     * can be used to send the form to Ajax.
1539
     *
1540
     * @deprecated 5.0
1541
     * @return string
1542
     */
1543
    public function formHtmlContent()
1544
    {
1545
        Deprecation::notice('5.0');
1546
        $this->IncludeFormTag = false;
1547
        $content = $this->forTemplate();
1548
        $this->IncludeFormTag = true;
1549
1550
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
1551
            . " value=\"" . $this->FormAction() . "\" />\n";
1552
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1553
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1554
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1555
1556
        return $content;
1557
    }
1558
1559
    /**
1560
     * Render this form using the given template, and return the result as a string
1561
     * You can pass either an SSViewer or a template name
1562
     * @param string|array $template
1563
     * @return DBHTMLText
1564
     */
1565
    public function renderWithoutActionButton($template)
1566
    {
1567
        $custom = $this->customise(array(
1568
            "Actions" => "",
1569
        ));
1570
1571
        if (is_string($template)) {
1572
            $template = new SSViewer($template);
1573
        }
1574
1575
        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...
1576
    }
1577
1578
    /**
1579
     * Return the default button that should be clicked when another one isn't
1580
     * available.
1581
     *
1582
     * @return FormAction
1583
     */
1584
    public function defaultAction()
1585
    {
1586
        if ($this->hasDefaultAction && $this->actions) {
1587
            return $this->actions->first();
1588
        }
1589
        return null;
1590
    }
1591
1592
    /**
1593
     * Disable the default button.
1594
     *
1595
     * Ordinarily, when a form is processed and no action_XXX button is
1596
     * available, then the first button in the actions list will be pressed.
1597
     * However, if this is "delete", for example, this isn't such a good idea.
1598
     *
1599
     * @return Form
1600
     */
1601
    public function disableDefaultAction()
1602
    {
1603
        $this->hasDefaultAction = false;
1604
1605
        return $this;
1606
    }
1607
1608
    /**
1609
     * Disable the requirement of a security token on this form instance. This
1610
     * security protects against CSRF attacks, but you should disable this if
1611
     * you don't want to tie a form to a session - eg a search form.
1612
     *
1613
     * Check for token state with {@link getSecurityToken()} and
1614
     * {@link SecurityToken->isEnabled()}.
1615
     *
1616
     * @return Form
1617
     */
1618
    public function disableSecurityToken()
1619
    {
1620
        $this->securityToken = new NullSecurityToken();
1621
1622
        return $this;
1623
    }
1624
1625
    /**
1626
     * Enable {@link SecurityToken} protection for this form instance.
1627
     *
1628
     * Check for token state with {@link getSecurityToken()} and
1629
     * {@link SecurityToken->isEnabled()}.
1630
     *
1631
     * @return Form
1632
     */
1633
    public function enableSecurityToken()
1634
    {
1635
        $this->securityToken = new SecurityToken();
1636
1637
        return $this;
1638
    }
1639
1640
    /**
1641
     * Returns the security token for this form (if any exists).
1642
     *
1643
     * Doesn't check for {@link securityTokenEnabled()}.
1644
     *
1645
     * Use {@link SecurityToken::inst()} to get a global token.
1646
     *
1647
     * @return SecurityToken|null
1648
     */
1649
    public function getSecurityToken()
1650
    {
1651
        return $this->securityToken;
1652
    }
1653
1654
    /**
1655
     * Compiles all CSS-classes.
1656
     *
1657
     * @return string
1658
     */
1659
    public function extraClass()
1660
    {
1661
        return implode(array_unique($this->extraClasses), ' ');
1662
    }
1663
1664
    /**
1665
     * Add a CSS-class to the form-container. If needed, multiple classes can
1666
     * be added by delimiting a string with spaces.
1667
     *
1668
     * @param string $class A string containing a classname or several class
1669
     *              names delimited by a single space.
1670
     * @return $this
1671
     */
1672
    public function addExtraClass($class)
1673
    {
1674
        //split at white space
1675
        $classes = preg_split('/\s+/', $class);
1676
        foreach ($classes as $class) {
1677
            //add classes one by one
1678
            $this->extraClasses[$class] = $class;
1679
        }
1680
        return $this;
1681
    }
1682
1683
    /**
1684
     * Remove a CSS-class from the form-container. Multiple class names can
1685
     * be passed through as a space delimited string
1686
     *
1687
     * @param string $class
1688
     * @return $this
1689
     */
1690
    public function removeExtraClass($class)
1691
    {
1692
        //split at white space
1693
        $classes = preg_split('/\s+/', $class);
1694
        foreach ($classes as $class) {
1695
            //unset one by one
1696
            unset($this->extraClasses[$class]);
1697
        }
1698
        return $this;
1699
    }
1700
1701
    public function debug()
1702
    {
1703
        $result = "<h3>$this->class</h3><ul>";
1704
        foreach ($this->fields as $field) {
1705
            $result .= "<li>$field" . $field->debug() . "</li>";
1706
        }
1707
        $result .= "</ul>";
1708
1709
        if ($this->validator) {
1710
            /** @skipUpgrade */
1711
            $result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
1712
        }
1713
1714
        return $result;
1715
    }
1716
1717
    /**
1718
     * Current request handler, build by buildRequestHandler(),
1719
     * accessed by getRequestHandler()
1720
     *
1721
     * @var FormRequestHandler
1722
     */
1723
    protected $requestHandler = null;
1724
1725
    /**
1726
     * Get request handler for this form
1727
     *
1728
     * @return FormRequestHandler
1729
     */
1730
    public function getRequestHandler()
1731
    {
1732
        if (!$this->requestHandler) {
1733
            $this->requestHandler = $this->buildRequestHandler();
1734
        }
1735
        return $this->requestHandler;
1736
    }
1737
1738
    /**
1739
     * Assign a specific request handler for this form
1740
     *
1741
     * @param FormRequestHandler $handler
1742
     * @return $this
1743
     */
1744
    public function setRequestHandler(FormRequestHandler $handler)
1745
    {
1746
        $this->requestHandler = $handler;
1747
        return $this;
1748
    }
1749
1750
    /**
1751
     * Scaffold new request handler for this form
1752
     *
1753
     * @return FormRequestHandler
1754
     */
1755
    protected function buildRequestHandler()
1756
    {
1757
        return FormRequestHandler::create($this);
1758
    }
1759
}
1760