Completed
Push — 4 ( 5fbfd8...bd8494 )
by Ingo
09:20
created

Form::getAttributesHTML()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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

289
        $fields->/** @scrutinizer ignore-call */ 
290
                 setForm($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
290
        $actions->setForm($this);
0 ignored issues
show
Bug introduced by
The method setForm() does not exist on null. ( Ignorable by Annotation )

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

290
        $actions->/** @scrutinizer ignore-call */ 
291
                  setForm($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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')) {
307
            $securityEnabled = $controller->securityTokenEnabled();
0 ignored issues
show
Bug introduced by
The method securityTokenEnabled() does not exist on SilverStripe\Control\RequestHandler. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

307
            /** @scrutinizer ignore-call */ 
308
            $securityEnabled = $controller->securityTokenEnabled();
Loading history...
Bug introduced by
The method securityTokenEnabled() does not exist on null. ( Ignorable by Annotation )

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

307
            /** @scrutinizer ignore-call */ 
308
            $securityEnabled = $controller->securityTokenEnabled();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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) {
0 ignored issues
show
introduced by
$request is of type SilverStripe\Control\HTTPRequest, thus it always evaluated to true.
Loading history...
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) {
0 ignored issues
show
introduced by
$existingResult is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
449
                if ($result) {
0 ignored issues
show
introduced by
$result is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
450
                    $existingResult->combineAnd($result);
451
                } else {
452
                    $result = $existingResult;
453
                }
454
            }
455
        }
456
457
        // Serialise
458
        $resultData = $result ? serialize($result) : null;
0 ignored issues
show
introduced by
$result is of type SilverStripe\ORM\ValidationResult, thus it always evaluated to true.
Loading history...
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) {
0 ignored issues
show
introduced by
$field is of type SilverStripe\Forms\FormField, thus it always evaluated to true.
Loading history...
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
    /**
568
     * Convert this form into a readonly form
569
     *
570
     * @return $this
571
     */
572
    public function makeReadonly()
573
    {
574
        $this->transform(new ReadonlyTransformation());
575
        return $this;
576
    }
577
578
    /**
579
     * Set whether the user should be redirected back down to the
580
     * form on the page upon validation errors in the form or if
581
     * they just need to redirect back to the page
582
     *
583
     * @param bool $bool Redirect to form on error?
584
     * @return $this
585
     */
586
    public function setRedirectToFormOnValidationError($bool)
587
    {
588
        $this->redirectToFormOnValidationError = $bool;
589
        return $this;
590
    }
591
592
    /**
593
     * Get whether the user should be redirected back down to the
594
     * form on the page upon validation errors
595
     *
596
     * @return bool
597
     */
598
    public function getRedirectToFormOnValidationError()
599
    {
600
        return $this->redirectToFormOnValidationError;
601
    }
602
603
    /**
604
     * @param FormTransformation $trans
605
     */
606
    public function transform(FormTransformation $trans)
607
    {
608
        $newFields = new FieldList();
609
        foreach ($this->fields as $field) {
610
            $newFields->push($field->transform($trans));
611
        }
612
        $this->fields = $newFields;
613
614
        $newActions = new FieldList();
615
        foreach ($this->actions as $action) {
616
            $newActions->push($action->transform($trans));
617
        }
618
        $this->actions = $newActions;
619
620
621
        // We have to remove validation, if the fields are not editable ;-)
622
        if ($this->validator) {
623
            $this->validator->removeValidation();
624
        }
625
    }
626
627
    /**
628
     * Get the {@link Validator} attached to this form.
629
     * @return Validator
630
     */
631
    public function getValidator()
632
    {
633
        return $this->validator;
634
    }
635
636
    /**
637
     * Set the {@link Validator} on this form.
638
     * @param Validator $validator
639
     * @return $this
640
     */
641
    public function setValidator(Validator $validator)
642
    {
643
        if ($validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\Validator, thus it always evaluated to true.
Loading history...
644
            $this->validator = $validator;
645
            $this->validator->setForm($this);
646
        }
647
        return $this;
648
    }
649
650
    /**
651
     * Remove the {@link Validator} from this from.
652
     */
653
    public function unsetValidator()
654
    {
655
        $this->validator = null;
656
        return $this;
657
    }
658
659
    /**
660
     * Set actions that are exempt from validation
661
     *
662
     * @param array
663
     * @return $this
664
     */
665
    public function setValidationExemptActions($actions)
666
    {
667
        $this->validationExemptActions = $actions;
668
        return $this;
669
    }
670
671
    /**
672
     * Get a list of actions that are exempt from validation
673
     *
674
     * @return array
675
     */
676
    public function getValidationExemptActions()
677
    {
678
        return $this->validationExemptActions;
679
    }
680
681
    /**
682
     * Passed a FormAction, returns true if that action is exempt from Form validation
683
     *
684
     * @param FormAction $action
685
     * @return bool
686
     */
687
    public function actionIsValidationExempt($action)
688
    {
689
        // Non-actions don't bypass validation
690
        if (!$action) {
0 ignored issues
show
introduced by
$action is of type SilverStripe\Forms\FormAction, thus it always evaluated to true.
Loading history...
691
            return false;
692
        }
693
        if ($action->getValidationExempt()) {
694
            return true;
695
        }
696
        if (in_array($action->actionName(), $this->getValidationExemptActions())) {
697
            return true;
698
        }
699
        return false;
700
    }
701
702
    /**
703
     * Generate extra special fields - namely the security token field (if required).
704
     *
705
     * @return FieldList
706
     */
707
    public function getExtraFields()
708
    {
709
        $extraFields = new FieldList();
710
711
        $token = $this->getSecurityToken();
712
        if ($token) {
713
            $tokenField = $token->updateFieldSet($this->fields);
714
            if ($tokenField) {
715
                $tokenField->setForm($this);
716
            }
717
        }
718
        $this->securityTokenAdded = true;
719
720
        // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
721
        if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
722
            $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
723
            $methodField->setForm($this);
724
            $extraFields->push($methodField);
725
        }
726
727
        return $extraFields;
728
    }
729
730
    /**
731
     * Return the form's fields - used by the templates
732
     *
733
     * @return FieldList The form fields
734
     */
735
    public function Fields()
736
    {
737
        foreach ($this->getExtraFields() as $field) {
738
            if (!$this->fields->fieldByName($field->getName())) {
739
                $this->fields->push($field);
740
            }
741
        }
742
743
        return $this->fields;
744
    }
745
746
    /**
747
     * Return all <input type="hidden"> fields
748
     * in a form - including fields nested in {@link CompositeFields}.
749
     * Useful when doing custom field layouts.
750
     *
751
     * @return FieldList
752
     */
753
    public function HiddenFields()
754
    {
755
        return $this->Fields()->HiddenFields();
756
    }
757
758
    /**
759
     * Return all fields except for the hidden fields.
760
     * Useful when making your own simplified form layouts.
761
     */
762
    public function VisibleFields()
763
    {
764
        return $this->Fields()->VisibleFields();
765
    }
766
767
    /**
768
     * Setter for the form fields.
769
     *
770
     * @param FieldList $fields
771
     * @return $this
772
     */
773
    public function setFields($fields)
774
    {
775
        $this->fields = $fields;
776
        return $this;
777
    }
778
779
    /**
780
     * Return the form's action buttons - used by the templates
781
     *
782
     * @return FieldList The action list
783
     */
784
    public function Actions()
785
    {
786
        return $this->actions;
787
    }
788
789
    /**
790
     * Setter for the form actions.
791
     *
792
     * @param FieldList $actions
793
     * @return $this
794
     */
795
    public function setActions($actions)
796
    {
797
        $this->actions = $actions;
798
        return $this;
799
    }
800
801
    /**
802
     * Unset all form actions
803
     */
804
    public function unsetAllActions()
805
    {
806
        $this->actions = new FieldList();
807
        return $this;
808
    }
809
810
    /**
811
     * @param string $name
812
     * @param string $value
813
     * @return $this
814
     */
815
    public function setAttribute($name, $value)
816
    {
817
        $this->attributes[$name] = $value;
818
        return $this;
819
    }
820
821
    /**
822
     * @param string $name
823
     * @return string
824
     */
825
    public function getAttribute($name)
826
    {
827
        if (isset($this->attributes[$name])) {
828
            return $this->attributes[$name];
829
        }
830
        return null;
831
    }
832
833
    /**
834
     * @return array
835
     */
836
    public function getAttributes()
837
    {
838
        $attrs = array(
839
            'id' => $this->FormName(),
840
            'action' => $this->FormAction(),
841
            'method' => $this->FormMethod(),
842
            'enctype' => $this->getEncType(),
843
            'target' => $this->target,
844
            'class' => $this->extraClass(),
845
        );
846
847
        if ($this->validator && $this->validator->getErrors()) {
848
            if (!isset($attrs['class'])) {
849
                $attrs['class'] = '';
850
            }
851
            $attrs['class'] .= ' validationerror';
852
        }
853
854
        $attrs = array_merge($attrs, $this->attributes);
855
856
        return $attrs;
857
    }
858
859
    /**
860
     * Return the attributes of the form tag - used by the templates.
861
     *
862
     * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
863
     * If at least one argument is passed as a string, all arguments act as excludes by name.
864
     *
865
     * @return string HTML attributes, ready for insertion into an HTML tag
866
     */
867
    public function getAttributesHTML($attrs = null)
868
    {
869
        $exclude = (is_string($attrs)) ? func_get_args() : null;
870
871
        $attrs = $this->getAttributes();
872
873
        // Remove empty
874
        $attrs = array_filter((array)$attrs, function ($value) {
875
            return ($value || $value === 0);
876
        });
877
878
        // Remove excluded
879
        if ($exclude) {
880
            $attrs = array_diff_key($attrs, array_flip($exclude));
881
        }
882
883
        // Prepare HTML-friendly 'method' attribute (lower-case)
884
        if (isset($attrs['method'])) {
885
            $attrs['method'] = strtolower($attrs['method']);
886
        }
887
888
        // Create markup
889
        $parts = array();
890
        foreach ($attrs as $name => $value) {
891
            $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
0 ignored issues
show
Bug introduced by
Are you sure SilverStripe\Core\Convert::raw2att($value) of type string|array can be used in concatenation? ( Ignorable by Annotation )

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

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

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

1503
                } elseif ($dataObject->/** @scrutinizer ignore-call */ hasMethod($saveMethod)) {
Loading history...
1504
                    $dataObject->$saveMethod($field->dataValue());
1505
                } elseif ($field->getName() !== "ID") {
1506
                    $field->saveInto($dataObject);
1507
                }
1508
            }
1509
        }
1510
        if ($lastField) {
1511
            $lastField->saveInto($dataObject);
1512
        }
1513
    }
1514
1515
    /**
1516
     * Get the submitted data from this form through
1517
     * {@link FieldList->dataFields()}, which filters out
1518
     * any form-specific data like form-actions.
1519
     * Calls {@link FormField->dataValue()} on each field,
1520
     * which returns a value suitable for insertion into a DataObject
1521
     * property.
1522
     *
1523
     * @return array
1524
     */
1525
    public function getData()
1526
    {
1527
        $dataFields = $this->fields->dataFields();
1528
        $data = array();
1529
1530
        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...
1531
            /** @var FormField $field */
1532
            foreach ($dataFields as $field) {
1533
                if ($field->getName()) {
1534
                    $data[$field->getName()] = $field->dataValue();
1535
                }
1536
            }
1537
        }
1538
1539
        return $data;
1540
    }
1541
1542
    /**
1543
     * Return a rendered version of this form.
1544
     *
1545
     * This is returned when you access a form as $FormObject rather
1546
     * than <% with FormObject %>
1547
     *
1548
     * @return DBHTMLText
1549
     */
1550
    public function forTemplate()
1551
    {
1552
        if (!$this->canBeCached()) {
1553
            HTTPCacheControlMiddleware::singleton()->disableCache();
1554
        }
1555
1556
        $return = $this->renderWith($this->getTemplates());
1557
1558
        // Now that we're rendered, clear message
1559
        $this->clearMessage();
1560
1561
        return $return;
1562
    }
1563
1564
    /**
1565
     * Return a rendered version of this form, suitable for ajax post-back.
1566
     *
1567
     * It triggers slightly different behaviour, such as disabling the rewriting
1568
     * of # links.
1569
     *
1570
     * @return DBHTMLText
1571
     */
1572
    public function forAjaxTemplate()
1573
    {
1574
        $view = SSViewer::create($this->getTemplates());
1575
1576
        $return = $view->dontRewriteHashlinks()->process($this);
1577
1578
        // Now that we're rendered, clear message
1579
        $this->clearMessage();
1580
1581
        return $return;
1582
    }
1583
1584
    /**
1585
     * Returns an HTML rendition of this form, without the <form> tag itself.
1586
     *
1587
     * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
1588
     * and _form_enctype.  These are the attributes of the form.  These fields
1589
     * can be used to send the form to Ajax.
1590
     *
1591
     * @deprecated 5.0
1592
     * @return string
1593
     */
1594
    public function formHtmlContent()
1595
    {
1596
        Deprecation::notice('5.0');
1597
        $this->IncludeFormTag = false;
1598
        $content = $this->forTemplate();
1599
        $this->IncludeFormTag = true;
1600
1601
        $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
1602
            . " value=\"" . $this->FormAction() . "\" />\n";
1603
        $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
1604
        $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
1605
        $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
1606
1607
        return $content;
1608
    }
1609
1610
    /**
1611
     * Render this form using the given template, and return the result as a string
1612
     * You can pass either an SSViewer or a template name
1613
     * @param string|array $template
1614
     * @return DBHTMLText
1615
     */
1616
    public function renderWithoutActionButton($template)
1617
    {
1618
        $custom = $this->customise(array(
1619
            "Actions" => "",
1620
        ));
1621
1622
        if (is_string($template)) {
1623
            $template = SSViewer::create($template);
1624
        }
1625
1626
        return $template->process($custom);
1627
    }
1628
1629
    /**
1630
     * Return the default button that should be clicked when another one isn't
1631
     * available.
1632
     *
1633
     * @return FormAction
1634
     */
1635
    public function defaultAction()
1636
    {
1637
        if ($this->hasDefaultAction && $this->actions) {
1638
            return $this->actions->first();
1639
        }
1640
        return null;
1641
    }
1642
1643
    /**
1644
     * Disable the default button.
1645
     *
1646
     * Ordinarily, when a form is processed and no action_XXX button is
1647
     * available, then the first button in the actions list will be pressed.
1648
     * However, if this is "delete", for example, this isn't such a good idea.
1649
     *
1650
     * @return Form
1651
     */
1652
    public function disableDefaultAction()
1653
    {
1654
        $this->hasDefaultAction = false;
1655
1656
        return $this;
1657
    }
1658
1659
    /**
1660
     * Disable the requirement of a security token on this form instance. This
1661
     * security protects against CSRF attacks, but you should disable this if
1662
     * you don't want to tie a form to a session - eg a search form.
1663
     *
1664
     * Check for token state with {@link getSecurityToken()} and
1665
     * {@link SecurityToken->isEnabled()}.
1666
     *
1667
     * @return Form
1668
     */
1669
    public function disableSecurityToken()
1670
    {
1671
        $this->securityToken = new NullSecurityToken();
1672
1673
        return $this;
1674
    }
1675
1676
    /**
1677
     * Enable {@link SecurityToken} protection for this form instance.
1678
     *
1679
     * Check for token state with {@link getSecurityToken()} and
1680
     * {@link SecurityToken->isEnabled()}.
1681
     *
1682
     * @return Form
1683
     */
1684
    public function enableSecurityToken()
1685
    {
1686
        $this->securityToken = new SecurityToken();
1687
1688
        return $this;
1689
    }
1690
1691
    /**
1692
     * Returns the security token for this form (if any exists).
1693
     *
1694
     * Doesn't check for {@link securityTokenEnabled()}.
1695
     *
1696
     * Use {@link SecurityToken::inst()} to get a global token.
1697
     *
1698
     * @return SecurityToken|null
1699
     */
1700
    public function getSecurityToken()
1701
    {
1702
        return $this->securityToken;
1703
    }
1704
1705
    /**
1706
     * Compiles all CSS-classes.
1707
     *
1708
     * @return string
1709
     */
1710
    public function extraClass()
1711
    {
1712
        return implode(array_unique($this->extraClasses), ' ');
0 ignored issues
show
Unused Code introduced by
The call to implode() has too many arguments starting with ' '. ( Ignorable by Annotation )

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

1712
        return /** @scrutinizer ignore-call */ implode(array_unique($this->extraClasses), ' ');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1713
    }
1714
1715
    /**
1716
     * Add a CSS-class to the form-container. If needed, multiple classes can
1717
     * be added by delimiting a string with spaces.
1718
     *
1719
     * @param string $class A string containing a classname or several class
1720
     *              names delimited by a single space.
1721
     * @return $this
1722
     */
1723
    public function addExtraClass($class)
1724
    {
1725
        //split at white space
1726
        $classes = preg_split('/\s+/', $class);
1727
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1728
            //add classes one by one
1729
            $this->extraClasses[$class] = $class;
1730
        }
1731
        return $this;
1732
    }
1733
1734
    /**
1735
     * Remove a CSS-class from the form-container. Multiple class names can
1736
     * be passed through as a space delimited string
1737
     *
1738
     * @param string $class
1739
     * @return $this
1740
     */
1741
    public function removeExtraClass($class)
1742
    {
1743
        //split at white space
1744
        $classes = preg_split('/\s+/', $class);
1745
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
1746
            //unset one by one
1747
            unset($this->extraClasses[$class]);
1748
        }
1749
        return $this;
1750
    }
1751
1752
    public function debug()
1753
    {
1754
        $class = static::class;
1755
        $result = "<h3>$class</h3><ul>";
1756
        foreach ($this->fields as $field) {
1757
            $result .= "<li>$field" . $field->debug() . "</li>";
1758
        }
1759
        $result .= "</ul>";
1760
1761
        if ($this->validator) {
1762
            /** @skipUpgrade */
1763
            $result .= '<h3>' . _t(__CLASS__ . '.VALIDATOR', 'Validator') . '</h3>' . $this->validator->debug();
0 ignored issues
show
Bug introduced by
The method debug() does not exist on SilverStripe\Forms\Validator. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

1763
            $result .= '<h3>' . _t(__CLASS__ . '.VALIDATOR', 'Validator') . '</h3>' . $this->validator->/** @scrutinizer ignore-call */ debug();
Loading history...
1764
        }
1765
1766
        return $result;
1767
    }
1768
1769
    /**
1770
     * Current request handler, build by buildRequestHandler(),
1771
     * accessed by getRequestHandler()
1772
     *
1773
     * @var FormRequestHandler
1774
     */
1775
    protected $requestHandler = null;
1776
1777
    /**
1778
     * Get request handler for this form
1779
     *
1780
     * @return FormRequestHandler
1781
     */
1782
    public function getRequestHandler()
1783
    {
1784
        if (!$this->requestHandler) {
1785
            $this->requestHandler = $this->buildRequestHandler();
1786
        }
1787
        return $this->requestHandler;
1788
    }
1789
1790
    /**
1791
     * Assign a specific request handler for this form
1792
     *
1793
     * @param FormRequestHandler $handler
1794
     * @return $this
1795
     */
1796
    public function setRequestHandler(FormRequestHandler $handler)
1797
    {
1798
        $this->requestHandler = $handler;
1799
        return $this;
1800
    }
1801
1802
    /**
1803
     * Scaffold new request handler for this form
1804
     *
1805
     * @return FormRequestHandler
1806
     */
1807
    protected function buildRequestHandler()
1808
    {
1809
        return FormRequestHandler::create($this);
1810
    }
1811
1812
    /**
1813
     * Can the body of this form be cached?
1814
     *
1815
     * @return bool
1816
     */
1817
    protected function canBeCached()
1818
    {
1819
        if ($this->getSecurityToken()->isEnabled()) {
1820
            return false;
1821
        }
1822
        if ($this->FormMethod() !== 'GET') {
1823
            return false;
1824
        }
1825
1826
        // Don't cache if there are required fields, or some other complex validator
1827
        $validator = $this->getValidator();
1828
        if ($validator instanceof RequiredFields) {
1829
            if (count($this->validator->getRequired())) {
0 ignored issues
show
Bug introduced by
The method getRequired() does not exist on SilverStripe\Forms\Validator. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

1829
            if (count($this->validator->/** @scrutinizer ignore-call */ getRequired())) {
Loading history...
1830
                return false;
1831
            }
1832
        } else {
1833
            return false;
1834
        }
1835
        return true;
1836
    }
1837
}
1838