Passed
Push — 4 ( cd0765...fc349d )
by
unknown
08:35
created

FormTest::testAddManyExtraClasses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 14
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms\Tests;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\Session;
8
use SilverStripe\Dev\CSSContentParser;
9
use SilverStripe\Dev\FunctionalTest;
10
use SilverStripe\Forms\DateField;
11
use SilverStripe\Forms\DatetimeField;
12
use SilverStripe\Forms\FieldList;
13
use SilverStripe\Forms\FileField;
14
use SilverStripe\Forms\Form;
15
use SilverStripe\Forms\FormAction;
16
use SilverStripe\Forms\HeaderField;
17
use SilverStripe\Forms\LookupField;
18
use SilverStripe\Forms\NumericField;
19
use SilverStripe\Forms\PasswordField;
20
use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken;
21
use SilverStripe\Forms\Tests\FormTest\ControllerWithSpecialSubmittedValueFields;
22
use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck;
23
use SilverStripe\Forms\Tests\FormTest\Player;
24
use SilverStripe\Forms\Tests\FormTest\Team;
25
use SilverStripe\Forms\Tests\FormTest\TestController;
26
use SilverStripe\Forms\Tests\ValidatorTest\TestValidator;
27
use SilverStripe\Forms\TextareaField;
28
use SilverStripe\Forms\TextField;
29
use SilverStripe\Forms\TimeField;
30
use SilverStripe\ORM\ValidationResult;
31
use SilverStripe\Security\NullSecurityToken;
32
use SilverStripe\Security\RandomGenerator;
33
use SilverStripe\Security\SecurityToken;
34
use SilverStripe\View\ArrayData;
35
use SilverStripe\View\SSViewer;
36
37
/**
38
 * @skipUpgrade
39
 */
40
class FormTest extends FunctionalTest
41
{
42
43
    protected static $fixture_file = 'FormTest.yml';
44
45
    protected static $extra_dataobjects = [
46
        Player::class,
47
        Team::class,
48
    ];
49
50
    protected static $extra_controllers = [
51
        TestController::class,
52
        ControllerWithSecurityToken::class,
53
        ControllerWithStrictPostCheck::class,
54
        ControllerWithSpecialSubmittedValueFields::class
55
    ];
56
57
    protected static $disable_themes = true;
58
59
    protected function setUp(): void
60
    {
61
        parent::setUp();
62
63
        // Suppress themes
64
        SSViewer::set_themes(
65
            [
66
            SSViewer::DEFAULT_THEME
67
            ]
68
        );
69
    }
70
71
    /**
72
     * @return array
73
     */
74
    public function boolDataProvider()
75
    {
76
        return [
77
            [false],
78
            [true],
79
        ];
80
    }
81
82
    public function testLoadDataFromRequest()
83
    {
84
        $form = new Form(
85
            Controller::curr(),
86
            'Form',
87
            new FieldList(
88
                new TextField('key1'),
89
                new TextField('namespace[key2]'),
90
                new TextField('namespace[key3][key4]'),
91
                new TextField('othernamespace[key5][key6][key7]'),
92
                new TextField('dot.field')
93
            ),
94
            new FieldList()
95
        );
96
97
        // url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7
98
        $requestData = [
99
            'key1' => 'val1',
100
            'namespace' => [
101
                'key2' => 'val2',
102
                'key3' => [
103
                    'key4' => 'val4',
104
                ]
105
            ],
106
            'othernamespace' => [
107
                'key5' => [
108
                    'key6' =>[
109
                        'key7' => 'val7'
110
                    ]
111
                ]
112
            ],
113
            'dot.field' => 'dot.field val'
114
115
        ];
116
117
        $form->loadDataFrom($requestData);
118
119
        $fields = $form->Fields();
120
        $this->assertEquals('val1', $fields->fieldByName('key1')->Value());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('key1') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
121
        $this->assertEquals('val2', $fields->fieldByName('namespace[key2]')->Value());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('namespace[key2]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
122
        $this->assertEquals('val4', $fields->fieldByName('namespace[key3][key4]')->Value());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('namespace[key3][key4]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
123
        $this->assertEquals('val7', $fields->fieldByName('othernamespace[key5][key6][key7]')->Value());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('ot...ace[key5][key6][key7]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
124
        $this->assertEquals('dot.field val', $fields->fieldByName('dot.field')->Value());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('dot.field') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
125
    }
126
127
    public function testSubmitReadonlyFields()
128
    {
129
        $this->get('FormTest_Controller');
130
131
        // Submitting a value for a readonly field should be ignored
132
        $response = $this->post(
133
            'FormTest_Controller/Form',
134
            [
135
                'Email' => 'invalid',
136
                'Number' => '888',
137
                'ReadonlyField' => '<script>alert("hacxzored")</script>'
138
                // leaving out "Required" field
139
            ]
140
        );
141
142
        // Number field updates its value
143
        $this->assertStringContainsString('<input type="text" name="Number" value="888"', $response->getBody());
144
145
146
        // Readonly field remains
147
        $this->assertStringContainsString(
148
            '<input type="text" name="ReadonlyField" value="This value is readonly"',
149
            $response->getBody()
150
        );
151
152
        $this->assertStringNotContainsString('hacxzored', $response->getBody());
153
    }
154
155
    public function testLoadDataFromUnchangedHandling()
156
    {
157
        $form = new Form(
158
            Controller::curr(),
159
            'Form',
160
            new FieldList(
161
                new TextField('key1'),
162
                new TextField('key2')
163
            ),
164
            new FieldList()
165
        );
166
        $form->loadDataFrom(
167
            [
168
            'key1' => 'save',
169
            'key2' => 'dontsave',
170
            'key2_unchanged' => '1'
171
            ]
172
        );
173
        $this->assertEquals(
174
            $form->getData(),
175
            [
176
                'key1' => 'save',
177
                'key2' => null,
178
            ],
179
            'loadDataFrom() doesnt save a field if a matching "<fieldname>_unchanged" flag is set'
180
        );
181
    }
182
183
    public function testLoadDataFromObject()
184
    {
185
        $form = new Form(
186
            Controller::curr(),
187
            'Form',
188
            new FieldList(
189
                new HeaderField('MyPlayerHeader', 'My Player'),
190
                new TextField('Name'), // appears in both Player and Team
191
                new TextareaField('Biography'),
192
                new DateField('Birthday'),
193
                new NumericField('BirthdayYear'), // dynamic property
194
                new TextField('FavouriteTeam.Name') // dot syntax
195
            ),
196
            new FieldList()
197
        );
198
199
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
200
        $form->loadDataFrom($captainWithDetails);
201
        $this->assertEquals(
202
            $form->getData(),
203
            [
204
                'Name' => 'Captain Details',
205
                'Biography' => 'Bio 1',
206
                'Birthday' => '1982-01-01',
207
                'BirthdayYear' => '1982',
208
                'FavouriteTeam.Name' => 'Team 1',
209
            ],
210
            'LoadDataFrom() loads simple fields and dynamic getters'
211
        );
212
213
        $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
214
        $form->loadDataFrom($captainNoDetails);
215
        $this->assertEquals(
216
            $form->getData(),
217
            [
218
                'Name' => 'Captain No Details',
219
                'Biography' => null,
220
                'Birthday' => null,
221
                'BirthdayYear' => 0,
222
                'FavouriteTeam.Name' => null,
223
            ],
224
            'LoadNonBlankDataFrom() loads only fields with values, and doesnt overwrite existing values'
225
        );
226
    }
227
228
    public function testLoadDataFromClearMissingFields()
229
    {
230
        $form = new Form(
231
            Controller::curr(),
232
            'Form',
233
            new FieldList(
234
                new HeaderField('MyPlayerHeader', 'My Player'),
235
                new TextField('Name'), // appears in both Player and Team
236
                new TextareaField('Biography'),
237
                new DateField('Birthday'),
238
                new NumericField('BirthdayYear'), // dynamic property
239
                new TextField('FavouriteTeam.Name'), // dot syntax
240
                $unrelatedField = new TextField('UnrelatedFormField')
241
                //new CheckboxSetField('Teams') // relation editing
242
            ),
243
            new FieldList()
244
        );
245
        $unrelatedField->setValue("random value");
246
247
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
248
        $form->loadDataFrom($captainWithDetails);
249
        $this->assertEquals(
250
            $form->getData(),
251
            [
252
                'Name' => 'Captain Details',
253
                'Biography' => 'Bio 1',
254
                'Birthday' => '1982-01-01',
255
                'BirthdayYear' => '1982',
256
                'FavouriteTeam.Name' => 'Team 1',
257
                'UnrelatedFormField' => 'random value',
258
            ],
259
            'LoadDataFrom() doesnt overwrite fields not found in the object'
260
        );
261
262
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
263
        $team2 = $this->objFromFixture(Team::class, 'team2');
264
        $form->loadDataFrom($captainWithDetails);
265
        $form->loadDataFrom($team2, Form::MERGE_CLEAR_MISSING);
266
        $this->assertEquals(
267
            $form->getData(),
268
            [
269
                'Name' => 'Team 2',
270
                'Biography' => '',
271
                'Birthday' => '',
272
                'BirthdayYear' => 0,
273
                'FavouriteTeam.Name' => null,
274
                'UnrelatedFormField' => null,
275
            ],
276
            'LoadDataFrom() overwrites fields not found in the object with $clearMissingFields=true'
277
        );
278
    }
279
280
    public function testLoadDataFromWithForceSetValueFlag()
281
    {
282
        // Get our data formatted in internal value and in submitted value
283
        // We're using very esoteric date and time format
284
        $dataInSubmittedValue = [
285
            'SomeDateTimeField' => 'Fri, Jun 15, \'18 17:28:05',
286
            'SomeTimeField' => '05 o\'clock PM 28 05'
287
        ];
288
        $dataInInternalValue = [
289
            'SomeDateTimeField' => '2018-06-15 17:28:05',
290
            'SomeTimeField' => '17:28:05'
291
        ];
292
293
        // Test loading our data with the MERGE_AS_INTERNAL_VALUE
294
        $form = $this->getStubFormWithWeirdValueFormat();
295
        $form->loadDataFrom($dataInInternalValue, Form::MERGE_AS_INTERNAL_VALUE);
296
297
        $this->assertEquals(
298
            $dataInInternalValue,
299
            $form->getData()
300
        );
301
302
        // Test loading our data with the MERGE_AS_SUBMITTED_VALUE and an data passed as an object
303
        $form = $this->getStubFormWithWeirdValueFormat();
304
        $form->loadDataFrom(ArrayData::create($dataInSubmittedValue), Form::MERGE_AS_SUBMITTED_VALUE);
305
        $this->assertEquals(
306
            $dataInInternalValue,
307
            $form->getData()
308
        );
309
310
        // Test loading our data without the MERGE_AS_INTERNAL_VALUE and without MERGE_AS_SUBMITTED_VALUE
311
        $form = $this->getStubFormWithWeirdValueFormat();
312
        $form->loadDataFrom($dataInSubmittedValue);
313
314
        $this->assertEquals(
315
            $dataInInternalValue,
316
            $form->getData()
317
        );
318
    }
319
320
    public function testLookupFieldDisabledSaving()
321
    {
322
        $object = new Team();
323
        $form = new Form(
324
            Controller::curr(),
325
            'Form',
326
            new FieldList(
327
                new LookupField('Players', 'Players')
328
            ),
329
            new FieldList()
330
        );
331
        $form->loadDataFrom(
332
            [
333
            'Players' => [
334
                    14,
335
                    18,
336
                    22
337
                ],
338
            ]
339
        );
340
        $form->saveInto($object);
341
        $playersIds = $object->Players()->getIDList();
342
343
        $this->assertTrue($form->validationResult()->isValid());
344
        $this->assertEquals(
345
            $playersIds,
346
            [],
347
            'saveInto() should not save into the DataObject for the LookupField'
348
        );
349
    }
350
351
    public function testLoadDataFromIgnoreFalseish()
352
    {
353
        $form = new Form(
354
            Controller::curr(),
355
            'Form',
356
            new FieldList(
357
                new TextField('Biography', 'Biography', 'Custom Default')
358
            ),
359
            new FieldList()
360
        );
361
362
        $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
363
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
364
365
        $form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH);
366
        $this->assertEquals(
367
            $form->getData(),
368
            ['Biography' => 'Custom Default'],
369
            'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish'
370
        );
371
372
        $form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH);
373
        $this->assertEquals(
374
            $form->getData(),
375
            ['Biography' => 'Bio 1'],
376
            'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish'
377
        );
378
    }
379
380
    public function testFormMethodOverride()
381
    {
382
        $form = $this->getStubForm();
383
        $form->setFormMethod('GET');
384
        $this->assertNull($form->Fields()->dataFieldByName('_method'));
385
386
        $form = $this->getStubForm();
387
        $form->setFormMethod('PUT');
388
        $this->assertEquals(
389
            $form->Fields()->dataFieldByName('_method')->Value(),
390
            'PUT',
391
            'PUT override in forms has PUT in hiddenfield'
392
        );
393
        $this->assertEquals(
394
            $form->FormMethod(),
395
            'POST',
396
            'PUT override in forms has POST in <form> tag'
397
        );
398
399
        $form = $this->getStubForm();
400
        $form->setFormMethod('DELETE');
401
        $this->assertEquals(
402
            $form->Fields()->dataFieldByName('_method')->Value(),
403
            'DELETE',
404
            'PUT override in forms has PUT in hiddenfield'
405
        );
406
        $this->assertEquals(
407
            $form->FormMethod(),
408
            'POST',
409
            'PUT override in forms has POST in <form> tag'
410
        );
411
    }
412
413
    public function testValidationExemptActions()
414
    {
415
        $this->get('FormTest_Controller');
416
417
        $this->submitForm(
418
            'Form_Form',
419
            'action_doSubmit',
420
            [
421
                'Email' => '[email protected]'
422
            ]
423
        );
424
425
        // Firstly, assert that required fields still work when not using an exempt action
426
        $this->assertPartialMatchBySelector(
427
            '#Form_Form_SomeRequiredField_Holder .required',
428
            ['"Some required field" is required'],
429
            'Required fields show a notification on field when left blank'
430
        );
431
432
        // Re-submit the form using validation-exempt button
433
        $this->submitForm(
434
            'Form_Form',
435
            'action_doSubmitValidationExempt',
436
            [
437
                'Email' => '[email protected]'
438
            ]
439
        );
440
441
        // The required message should be empty if validation was skipped
442
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
443
        $this->assertEmpty($items);
444
445
        // And the session message should show up is submitted successfully
446
        $this->assertPartialMatchBySelector(
447
            '#Form_Form_error',
448
            [
449
                'Validation skipped'
450
            ],
451
            'Form->sessionMessage() shows up after reloading the form'
452
        );
453
454
        // Test this same behaviour, but with a form-action exempted via instance
455
        $this->submitForm(
456
            'Form_Form',
457
            'action_doSubmitActionExempt',
458
            [
459
                'Email' => '[email protected]'
460
            ]
461
        );
462
463
        // The required message should be empty if validation was skipped
464
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
465
        $this->assertEmpty($items);
466
467
        // And the session message should show up is submitted successfully
468
        $this->assertPartialMatchBySelector(
469
            '#Form_Form_error',
470
            [
471
                'Validation bypassed!'
472
            ],
473
            'Form->sessionMessage() shows up after reloading the form'
474
        );
475
    }
476
477
    public function testSessionValidationMessage()
478
    {
479
        $this->get('FormTest_Controller');
480
481
        $response = $this->post(
482
            'FormTest_Controller/Form',
483
            [
484
                'Email' => 'invalid',
485
                'Number' => '<a href="http://mysite.com">link</a>' // XSS attempt
486
                // leaving out "Required" field
487
            ]
488
        );
489
490
        $this->assertPartialMatchBySelector(
491
            '#Form_Form_Email_Holder span.message',
492
            [
493
                'Please enter an email address'
494
            ],
495
            'Formfield validation shows note on field if invalid'
496
        );
497
        $this->assertPartialMatchBySelector(
498
            '#Form_Form_SomeRequiredField_Holder span.required',
499
            [
500
                '"Some required field" is required'
501
            ],
502
            'Required fields show a notification on field when left blank'
503
        );
504
505
        $this->assertStringContainsString(
506
            '&#039;&lt;a href=&quot;http://mysite.com&quot;&gt;link&lt;/a&gt;&#039; is not a number, only numbers can be accepted for this field',
507
            $response->getBody(),
508
            "Validation messages are safely XML encoded"
509
        );
510
        $this->assertStringNotContainsString(
511
            '<a href="http://mysite.com">link</a>',
512
            $response->getBody(),
513
            "Unsafe content is not emitted directly inside the response body"
514
        );
515
    }
516
517
    public function testSessionSuccessMessage()
518
    {
519
        $this->get('FormTest_Controller');
520
521
        $this->post(
522
            'FormTest_Controller/Form',
523
            [
524
                'Email' => '[email protected]',
525
                'SomeRequiredField' => 'test',
526
            ]
527
        );
528
        $this->assertPartialMatchBySelector(
529
            '#Form_Form_error',
530
            [
531
                'Test save was successful'
532
            ],
533
            'Form->sessionMessage() shows up after reloading the form'
534
        );
535
    }
536
537
    public function testValidationException()
538
    {
539
        $this->get('FormTest_Controller');
540
541
        $this->post(
542
            'FormTest_Controller/Form',
543
            [
544
                'Email' => '[email protected]',
545
                'SomeRequiredField' => 'test',
546
                'action_doTriggerException' => 1,
547
            ]
548
        );
549
        $this->assertPartialMatchBySelector(
550
            '#Form_Form_Email_Holder span.message',
551
            [
552
                'Error on Email field'
553
            ],
554
            'Formfield validation shows note on field if invalid'
555
        );
556
        $this->assertPartialMatchBySelector(
557
            '#Form_Form_error',
558
            [
559
                'Error at top of form'
560
            ],
561
            'Required fields show a notification on field when left blank'
562
        );
563
    }
564
565
    public function testGloballyDisabledSecurityTokenInheritsToNewForm()
566
    {
567
        SecurityToken::enable();
568
569
        $form1 = $this->getStubForm();
570
        $this->assertInstanceOf(SecurityToken::class, $form1->getSecurityToken());
571
572
        SecurityToken::disable();
573
574
        $form2 = $this->getStubForm();
575
        $this->assertInstanceOf(NullSecurityToken::class, $form2->getSecurityToken());
576
577
        SecurityToken::enable();
578
    }
579
580
    public function testDisableSecurityTokenDoesntAddTokenFormField()
581
    {
582
        SecurityToken::enable();
583
584
        $formWithToken = $this->getStubForm();
585
        $this->assertInstanceOf(
586
            'SilverStripe\\Forms\\HiddenField',
587
            $formWithToken->Fields()->fieldByName(SecurityToken::get_default_name()),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $formWithToken->Fields()...en::get_default_name()) targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
588
            'Token field added by default'
589
        );
590
591
        $formWithoutToken = $this->getStubForm();
592
        $formWithoutToken->disableSecurityToken();
593
        $this->assertNull(
594
            $formWithoutToken->Fields()->fieldByName(SecurityToken::get_default_name()),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $formWithoutToken->Field...en::get_default_name()) targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
595
            'Token field not added if disableSecurityToken() is set'
596
        );
597
    }
598
599
    public function testDisableSecurityTokenAcceptsSubmissionWithoutToken()
600
    {
601
        SecurityToken::enable();
602
        $expectedToken = SecurityToken::inst()->getValue();
603
604
        $this->get('FormTest_ControllerWithSecurityToken');
605
        // can't use submitForm() as it'll automatically insert SecurityID into the POST data
606
        $response = $this->post(
607
            'FormTest_ControllerWithSecurityToken/Form',
608
            [
609
                'Email' => '[email protected]',
610
                'action_doSubmit' => 1
611
                // leaving out security token
612
            ]
613
        );
614
        $this->assertEquals(400, $response->getStatusCode(), 'Submission fails without security token');
615
616
        // Generate a new token which doesn't match the current one
617
        $generator = new RandomGenerator();
618
        $invalidToken = $generator->randomToken('sha1');
619
        $this->assertNotEquals($invalidToken, $expectedToken);
620
621
        // Test token with request
622
        $this->get('FormTest_ControllerWithSecurityToken');
623
        $response = $this->post(
624
            'FormTest_ControllerWithSecurityToken/Form',
625
            [
626
                'Email' => '[email protected]',
627
                'action_doSubmit' => 1,
628
                'SecurityID' => $invalidToken
629
            ]
630
        );
631
        $this->assertEquals(200, $response->getStatusCode(), 'Submission reloads form if security token invalid');
632
        $this->assertTrue(
633
            stripos($response->getBody(), 'name="SecurityID" value="' . $expectedToken . '"') !== false,
634
            'Submission reloads with correct security token after failure'
635
        );
636
        $this->assertTrue(
637
            stripos($response->getBody(), 'name="SecurityID" value="' . $invalidToken . '"') === false,
638
            'Submission reloads without incorrect security token after failure'
639
        );
640
641
        $matched = $this->cssParser()->getBySelector('#Form_Form_Email');
642
        $attrs = $matched[0]->attributes();
643
        $this->assertEquals('[email protected]', (string)$attrs['value'], 'Submitted data is preserved');
644
645
        $this->get('FormTest_ControllerWithSecurityToken');
646
        $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
647
        $this->assertEquals(
648
            1,
649
            count($tokenEls),
650
            'Token form field added for controller without disableSecurityToken()'
651
        );
652
        $token = (string)$tokenEls[0];
653
        $response = $this->submitForm(
654
            'Form_Form',
655
            null,
656
            [
657
                'Email' => '[email protected]',
658
                'SecurityID' => $token
659
            ]
660
        );
661
        $this->assertEquals(200, $response->getStatusCode(), 'Submission suceeds with security token');
662
    }
663
664
    public function testStrictFormMethodChecking()
665
    {
666
        $this->get('FormTest_ControllerWithStrictPostCheck');
667
        $response = $this->get(
668
            'FormTest_ControllerWithStrictPostCheck/Form/[email protected]&action_doSubmit=1'
669
        );
670
        $this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method');
671
672
        $this->get('FormTest_ControllerWithStrictPostCheck');
673
        $response = $this->post(
674
            'FormTest_ControllerWithStrictPostCheck/Form',
675
            [
676
                'Email' => '[email protected]',
677
                'action_doSubmit' => 1
678
            ]
679
        );
680
        $this->assertEquals(200, $response->getStatusCode(), 'Submission succeeds with correct method');
681
    }
682
683
    public function testEnableSecurityToken()
684
    {
685
        SecurityToken::disable();
686
        $form = $this->getStubForm();
687
        $this->assertFalse($form->getSecurityToken()->isEnabled());
688
        $form->enableSecurityToken();
689
        $this->assertTrue($form->getSecurityToken()->isEnabled());
690
691
        SecurityToken::disable(); // restore original
692
    }
693
694
    public function testDisableSecurityToken()
695
    {
696
        SecurityToken::enable();
697
        $form = $this->getStubForm();
698
        $this->assertTrue($form->getSecurityToken()->isEnabled());
699
        $form->disableSecurityToken();
700
        $this->assertFalse($form->getSecurityToken()->isEnabled());
701
702
        SecurityToken::disable(); // restore original
703
    }
704
705
    public function testEncType()
706
    {
707
        $form = $this->getStubForm();
708
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
709
710
        $form->setEncType(Form::ENC_TYPE_MULTIPART);
711
        $this->assertEquals('multipart/form-data', $form->getEncType());
712
713
        $form = $this->getStubForm();
714
        $form->Fields()->push(new FileField(null));
715
        $this->assertEquals('multipart/form-data', $form->getEncType());
716
717
        $form->setEncType(Form::ENC_TYPE_URLENCODED);
718
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
719
    }
720
721
    public function testAddExtraClass()
722
    {
723
        $form = $this->getStubForm();
724
        $form->addExtraClass('class1');
725
        $form->addExtraClass('class2');
726
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
727
    }
728
729
    public function testHasExtraClass()
730
    {
731
        $form = $this->getStubForm();
732
        $form->addExtraClass('class1');
733
        $form->addExtraClass('class2');
734
        $this->assertTrue($form->hasExtraClass('class1'));
735
        $this->assertTrue($form->hasExtraClass('class2'));
736
        $this->assertTrue($form->hasExtraClass('class1 class2'));
737
        $this->assertTrue($form->hasExtraClass('class2 class1'));
738
        $this->assertFalse($form->hasExtraClass('class3'));
739
        $this->assertFalse($form->hasExtraClass('class2 class3'));
740
    }
741
742
    public function testRemoveExtraClass()
743
    {
744
        $form = $this->getStubForm();
745
        $form->addExtraClass('class1');
746
        $form->addExtraClass('class2');
747
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
748
        $form->removeExtraClass('class1');
749
        $this->assertStringEndsWith('class2', $form->extraClass());
750
    }
751
752
    public function testAddManyExtraClasses()
753
    {
754
        $form = $this->getStubForm();
755
        //test we can split by a range of spaces and tabs
756
        $form->addExtraClass('class1 class2     class3	class4		class5');
757
        $this->assertStringEndsWith(
758
            'class1 class2 class3 class4 class5',
759
            $form->extraClass()
760
        );
761
        //test that duplicate classes don't get added
762
        $form->addExtraClass('class1 class2');
763
        $this->assertStringEndsWith(
764
            'class1 class2 class3 class4 class5',
765
            $form->extraClass()
766
        );
767
    }
768
769
    public function testRemoveManyExtraClasses()
770
    {
771
        $form = $this->getStubForm();
772
        $form->addExtraClass('class1 class2     class3	class4		class5');
773
        //test we can remove a single class we just added
774
        $form->removeExtraClass('class3');
775
        $this->assertStringEndsWith(
776
            'class1 class2 class4 class5',
777
            $form->extraClass()
778
        );
779
        //check we can remove many classes at once
780
        $form->removeExtraClass('class1 class5');
781
        $this->assertStringEndsWith(
782
            'class2 class4',
783
            $form->extraClass()
784
        );
785
        //check that removing a dud class is fine
786
        $form->removeExtraClass('dudClass');
787
        $this->assertStringEndsWith(
788
            'class2 class4',
789
            $form->extraClass()
790
        );
791
    }
792
793
    public function testDefaultClasses()
794
    {
795
        Form::config()->update(
796
            'default_classes',
797
            [
798
            'class1',
799
            ]
800
        );
801
802
        $form = $this->getStubForm();
803
804
        $this->assertStringContainsString('class1', $form->extraClass(), 'Class list does not contain expected class');
805
806
        Form::config()->update(
807
            'default_classes',
808
            [
809
            'class1',
810
            'class2',
811
            ]
812
        );
813
814
        $form = $this->getStubForm();
815
816
        $this->assertStringContainsString('class1 class2', $form->extraClass(), 'Class list does not contain expected class');
817
818
        Form::config()->update(
819
            'default_classes',
820
            [
821
            'class3',
822
            ]
823
        );
824
825
        $form = $this->getStubForm();
826
827
        $this->assertStringContainsString('class3', $form->extraClass(), 'Class list does not contain expected class');
828
829
        $form->removeExtraClass('class3');
830
831
        $this->assertStringNotContainsString('class3', $form->extraClass(), 'Class list contains unexpected class');
832
    }
833
834
    public function testAttributes()
835
    {
836
        $form = $this->getStubForm();
837
        $form->setAttribute('foo', 'bar');
838
        $this->assertEquals('bar', $form->getAttribute('foo'));
839
        $attrs = $form->getAttributes();
840
        $this->assertArrayHasKey('foo', $attrs);
841
        $this->assertEquals('bar', $attrs['foo']);
842
    }
843
844
    /**
845
     * @skipUpgrade
846
     */
847
    public function testButtonClicked()
848
    {
849
        $form = $this->getStubForm();
850
        $action = $form->getRequestHandler()->buttonClicked();
851
        $this->assertNull($action);
852
853
        $controller = new FormTest\TestController();
854
        $form = $controller->Form();
855
        $request = new HTTPRequest(
856
            'POST',
857
            'FormTest_Controller/Form',
858
            [],
859
            [
860
            'Email' => '[email protected]',
861
            'SomeRequiredField' => 1,
862
            'action_doSubmit' => 1
863
            ]
864
        );
865
        $request->setSession(new Session([]));
866
867
        $form->getRequestHandler()->httpSubmission($request);
868
        $button = $form->getRequestHandler()->buttonClicked();
869
        $this->assertInstanceOf(FormAction::class, $button);
870
        $this->assertEquals('doSubmit', $button->actionName());
871
        $form = new Form(
872
            $controller,
873
            'Form',
874
            new FieldList(new FormAction('doSubmit', 'Inline action')),
875
            new FieldList()
876
        );
877
        $form->disableSecurityToken();
878
        $request = new HTTPRequest(
879
            'POST',
880
            'FormTest_Controller/Form',
881
            [],
882
            [
883
            'action_doSubmit' => 1
884
            ]
885
        );
886
        $request->setSession(new Session([]));
887
888
        $form->getRequestHandler()->httpSubmission($request);
889
        $button = $form->getRequestHandler()->buttonClicked();
890
        $this->assertInstanceOf(FormAction::class, $button);
891
        $this->assertEquals('doSubmit', $button->actionName());
892
    }
893
894
    public function testCheckAccessAction()
895
    {
896
        $controller = new FormTest\TestController();
897
        $form = new Form(
898
            $controller,
899
            'Form',
900
            new FieldList(),
901
            new FieldList(new FormAction('actionName', 'Action'))
902
        );
903
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName'));
904
905
        $form = new Form(
906
            $controller,
907
            'Form',
908
            new FieldList(new FormAction('inlineAction', 'Inline action')),
909
            new FieldList()
910
        );
911
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction'));
912
    }
913
914
    public function testAttributesHTML()
915
    {
916
        $form = $this->getStubForm();
917
918
        $form->setAttribute('foo', 'bar');
919
        $this->assertStringContainsString('foo="bar"', $form->getAttributesHTML());
920
921
        $form->setAttribute('foo', null);
922
        $this->assertStringNotContainsString('foo="bar"', $form->getAttributesHTML());
923
924
        $form->setAttribute('foo', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $value of SilverStripe\Forms\Form::setAttribute(). ( Ignorable by Annotation )

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

924
        $form->setAttribute('foo', /** @scrutinizer ignore-type */ true);
Loading history...
925
        $this->assertStringContainsString('foo="foo"', $form->getAttributesHTML());
926
927
        $form->setAttribute('one', 1);
928
        $form->setAttribute('two', 2);
929
        $form->setAttribute('three', 3);
930
        $form->setAttribute('<html>', '<html>');
931
        $this->assertStringNotContainsString('one="1"', $form->getAttributesHTML('one', 'two'));
932
        $this->assertStringNotContainsString('two="2"', $form->getAttributesHTML('one', 'two'));
933
        $this->assertStringContainsString('three="3"', $form->getAttributesHTML('one', 'two'));
934
        $this->assertStringNotContainsString('<html>', $form->getAttributesHTML());
935
    }
936
937
    function testMessageEscapeHtml()
938
    {
939
        $form = $this->getStubForm();
940
        $form->setMessage('<em>Escaped HTML</em>', 'good', ValidationResult::CAST_TEXT);
941
        $parser = new CSSContentParser($form->forTemplate());
942
        $messageEls = $parser->getBySelector('.message');
943
        $this->assertStringContainsString(
944
            '&lt;em&gt;Escaped HTML&lt;/em&gt;',
945
            $messageEls[0]->asXML()
946
        );
947
948
        $form = $this->getStubForm();
949
        $form->setMessage('<em>Unescaped HTML</em>', 'good', ValidationResult::CAST_HTML);
950
        $parser = new CSSContentParser($form->forTemplate());
951
        $messageEls = $parser->getBySelector('.message');
952
        $this->assertStringContainsString(
953
            '<em>Unescaped HTML</em>',
954
            $messageEls[0]->asXML()
955
        );
956
    }
957
958
    public function testFieldMessageEscapeHtml()
959
    {
960
        $form = $this->getStubForm();
961
        $form->Fields()->dataFieldByName('key1')->setMessage('<em>Escaped HTML</em>', 'good');
962
        $parser = new CSSContentParser($result = $form->forTemplate());
963
        $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
964
        $this->assertStringContainsString(
965
            '&lt;em&gt;Escaped HTML&lt;/em&gt;',
966
            $messageEls[0]->asXML()
967
        );
968
969
        // Test with HTML
970
        $form = $this->getStubForm();
971
        $form
972
            ->Fields()
973
            ->dataFieldByName('key1')
974
            ->setMessage('<em>Unescaped HTML</em>', 'good', ValidationResult::CAST_HTML);
975
        $parser = new CSSContentParser($form->forTemplate());
976
        $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
977
        $this->assertStringContainsString(
978
            '<em>Unescaped HTML</em>',
979
            $messageEls[0]->asXML()
980
        );
981
    }
982
983
    public function testGetExtraFields()
984
    {
985
        $form = new FormTest\ExtraFieldsForm(
986
            new FormTest\TestController(),
987
            'Form',
988
            new FieldList(new TextField('key1')),
989
            new FieldList()
990
        );
991
992
        $data = [
993
            'key1' => 'test',
994
            'ExtraFieldCheckbox' => false,
995
        ];
996
997
        $form->loadDataFrom($data);
998
999
        $formData = $form->getData();
1000
        $this->assertEmpty($formData['ExtraFieldCheckbox']);
1001
    }
1002
1003
    /**
1004
     * @dataProvider boolDataProvider
1005
     * @param bool $allow
1006
     */
1007
    public function testPasswordPostback($allow)
1008
    {
1009
        $form = $this->getStubForm();
1010
        $form->enableSecurityToken();
1011
        $form->Fields()->push(
1012
            PasswordField::create('Password')
1013
                ->setAllowValuePostback($allow)
1014
        );
1015
        $form->Actions()->push(FormAction::create('doSubmit'));
1016
        $request = new HTTPRequest(
1017
            'POST',
1018
            'FormTest_Controller/Form',
1019
            [],
1020
            [
1021
                'key1' => 'foo',
1022
                'Password' => 'hidden',
1023
                SecurityToken::inst()->getName() => 'fail',
1024
                'action_doSubmit' => 1,
1025
            ]
1026
        );
1027
        $form->getRequestHandler()->httpSubmission($request);
1028
        $parser = new CSSContentParser($form->forTemplate());
1029
        $passwords = $parser->getBySelector('input#Password');
1030
        $this->assertNotNull($passwords);
1031
        $this->assertCount(1, $passwords);
1032
        /* @var \SimpleXMLElement $password */
1033
        $password = $passwords[0];
1034
        $attrs = iterator_to_array($password->attributes());
1035
        if ($allow) {
1036
            $this->assertArrayHasKey('value', $attrs);
1037
            $this->assertEquals('hidden', $attrs['value']);
1038
        } else {
1039
            $this->assertArrayNotHasKey('value', $attrs);
1040
        }
1041
    }
1042
1043
    /**
1044
     * This test confirms that when a form validation fails, the submitted value are stored in the session and are
1045
     * reloaded correctly once the form is re-rendered. This indirectly test `Form::restoreFormState`,
1046
     * `Form::setSessionData`, `Form::getSessionData` and `Form::clearFormState`.
1047
     */
1048
    public function testRestoreFromState()
1049
    {
1050
        // Use a specially crafted controlled for this request. The associated form contains fields that override the
1051
        // `setSubmittedValue` and require an internal format that differs from the submitted format.
1052
        $this->get('FormTest_ControllerWithSpecialSubmittedValueFields')->getBody();
1053
1054
        // Posting our form. This should fail and redirect us to the form page and preload our submit value
1055
        $response = $this->post(
1056
            'FormTest_ControllerWithSpecialSubmittedValueFields/Form',
1057
            [
1058
                'SomeDateField' => '15/06/2018',
1059
                'SomeFrenchNumericField' => '9 876,5432',
1060
                'SomeFrenchMoneyField' => [
1061
                    'Amount' => '9 876,54',
1062
                    'Currency' => 'NZD'
1063
                ]
1064
                // Validation will fail because we leave out SomeRequiredField
1065
            ],
1066
            []
1067
        );
1068
1069
        // Test our reloaded form field
1070
        $body = $response->getBody();
1071
        $this->assertStringContainsString(
1072
            '<input type="text" name="SomeDateField" value="15/06/2018"',
1073
            $body,
1074
            'Our reloaded form should contain a SomeDateField with the value "15/06/2018"'
1075
        );
1076
1077
        $this->assertStringContainsString(
1078
            '<input type="text" name="SomeFrenchNumericField" value="9 876,5432" ',
1079
            $this->clean($body),
1080
            'Our reloaded form should contain a SomeFrenchNumericField with the value "9 876,5432"'
1081
        );
1082
1083
        $this->assertStringContainsString(
1084
            '<input type="text" name="SomeFrenchMoneyField[Currency]" value="NZD"',
1085
            $body,
1086
            'Our reloaded form should contain a SomeFrenchMoneyField[Currency] with the value "NZD"'
1087
        );
1088
1089
        $this->assertStringContainsString(
1090
            '<input type="text" name="SomeFrenchMoneyField[Amount]" value="9 876,54" ',
1091
            $this->clean($body),
1092
            'Our reloaded form should contain a SomeFrenchMoneyField[Amount] with the value "9 876,54"'
1093
        );
1094
1095
        $this->assertEmpty(
1096
            $this->mainSession->session()->get('FormInfo.Form_Form'),
1097
            'Our form was reloaded successfully. That should have cleared our session.'
1098
        );
1099
    }
1100
1101
    protected function getStubForm()
1102
    {
1103
        return new Form(
1104
            new FormTest\TestController(),
1105
            'Form',
1106
            new FieldList(new TextField('key1')),
1107
            new FieldList()
1108
        );
1109
    }
1110
1111
    /**
1112
     * Some fields handle submitted values differently from their internal values. This forms contains 2 such fields
1113
     * * a SomeDateTimeField that expect a date such as `Fri, Jun 15, '18 17:28:05`,
1114
     * * a SomeTimeField that expects it's time as `05 o'clock PM 28 05`
1115
     *
1116
     * @return Form
1117
     */
1118
    protected function getStubFormWithWeirdValueFormat()
1119
    {
1120
        return new Form(
1121
            Controller::curr(),
1122
            'Form',
1123
            new FieldList(
1124
                $dateField = DatetimeField::create('SomeDateTimeField')
1125
                    ->setHTML5(false)
1126
                    ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"),
1127
                $timeField = TimeField::create('SomeTimeField')
1128
                    ->setHTML5(false)
1129
                    ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format
1130
            ),
1131
            new FieldList()
1132
        );
1133
    }
1134
1135
    /**
1136
     * In some cases and locales, validation expects non-breaking spaces.
1137
     * This homogenises narrow and regular NBSPs to a regular space character
1138
     *
1139
     * @param  string $input
1140
     * @return string The input value, with all non-breaking spaces replaced with spaces
1141
     */
1142
    protected function clean($input)
1143
    {
1144
        return str_replace(
1145
            [
1146
                html_entity_decode('&nbsp;', null, 'UTF-8'),
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type integer expected by parameter $flags of html_entity_decode(). ( Ignorable by Annotation )

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

1146
                html_entity_decode('&nbsp;', /** @scrutinizer ignore-type */ null, 'UTF-8'),
Loading history...
1147
                html_entity_decode('&#8239;', null, 'UTF-8'), // narrow non-breaking space
1148
            ],
1149
            ' ',
1150
            trim($input)
1151
        );
1152
    }
1153
}
1154