Passed
Pull Request — 4.8 (#9976)
by Loz
09:55
created

testGloballyDisabledSecurityTokenInheritsToNewForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 13
rs 10
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\CompositeField;
11
use SilverStripe\Forms\DateField;
12
use SilverStripe\Forms\DatetimeField;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\FileField;
15
use SilverStripe\Forms\Form;
16
use SilverStripe\Forms\FormAction;
17
use SilverStripe\Forms\HeaderField;
18
use SilverStripe\Forms\LookupField;
19
use SilverStripe\Forms\NumericField;
20
use SilverStripe\Forms\PasswordField;
21
use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken;
22
use SilverStripe\Forms\Tests\FormTest\ControllerWithSpecialSubmittedValueFields;
23
use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck;
24
use SilverStripe\Forms\Tests\FormTest\Player;
25
use SilverStripe\Forms\Tests\FormTest\Team;
26
use SilverStripe\Forms\Tests\FormTest\TestController;
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()
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
            ),
93
            new FieldList()
94
        );
95
96
        // url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7
97
        $requestData = [
98
            'key1' => 'val1',
99
            'namespace' => [
100
                'key2' => 'val2',
101
                'key3' => [
102
                    'key4' => 'val4',
103
                ]
104
            ],
105
            'othernamespace' => [
106
                'key5' => [
107
                    'key6' =>[
108
                        'key7' => 'val7'
109
                    ]
110
                ]
111
            ]
112
        ];
113
114
        $form->loadDataFrom($requestData);
115
116
        $fields = $form->Fields();
117
        $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...
118
        $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...
119
        $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...
120
        $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...
121
    }
122
123
    public function testSubmitReadonlyFields()
124
    {
125
        $this->get('FormTest_Controller');
126
127
        // Submitting a value for a readonly field should be ignored
128
        $response = $this->post(
129
            'FormTest_Controller/Form',
130
            [
131
                'Email' => 'invalid',
132
                'Number' => '888',
133
                'ReadonlyField' => '<script>alert("hacxzored")</script>'
134
                // leaving out "Required" field
135
            ]
136
        );
137
138
        // Number field updates its value
139
        $this->assertContains('<input type="text" name="Number" value="888"', $response->getBody());
140
141
142
        // Readonly field remains
143
        $this->assertContains(
144
            '<input type="text" name="ReadonlyField" value="This value is readonly"',
145
            $response->getBody()
146
        );
147
148
        $this->assertNotContains('hacxzored', $response->getBody());
149
    }
150
151
    public function testLoadDataFromUnchangedHandling()
152
    {
153
        $form = new Form(
154
            Controller::curr(),
155
            'Form',
156
            new FieldList(
157
                new TextField('key1'),
158
                new TextField('key2')
159
            ),
160
            new FieldList()
161
        );
162
        $form->loadDataFrom(
163
            [
164
            'key1' => 'save',
165
            'key2' => 'dontsave',
166
            'key2_unchanged' => '1'
167
            ]
168
        );
169
        $this->assertEquals(
170
            $form->getData(),
171
            [
172
                'key1' => 'save',
173
                'key2' => null,
174
            ],
175
            'loadDataFrom() doesnt save a field if a matching "<fieldname>_unchanged" flag is set'
176
        );
177
    }
178
179
    public function testLoadDataFromObject()
180
    {
181
        $form = new Form(
182
            Controller::curr(),
183
            'Form',
184
            new FieldList(
185
                new HeaderField('MyPlayerHeader', 'My Player'),
186
                new TextField('Name'), // appears in both Player and Team
187
                new TextareaField('Biography'),
188
                new DateField('Birthday'),
189
                new NumericField('BirthdayYear') // dynamic property
190
            ),
191
            new FieldList()
192
        );
193
194
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
195
        $form->loadDataFrom($captainWithDetails);
196
        $this->assertEquals(
197
            $form->getData(),
198
            [
199
                'Name' => 'Captain Details',
200
                'Biography' => 'Bio 1',
201
                'Birthday' => '1982-01-01',
202
                'BirthdayYear' => '1982',
203
            ],
204
            'LoadDataFrom() loads simple fields and dynamic getters'
205
        );
206
207
        $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
208
        $form->loadDataFrom($captainNoDetails);
209
        $this->assertEquals(
210
            $form->getData(),
211
            [
212
                'Name' => 'Captain No Details',
213
                'Biography' => null,
214
                'Birthday' => null,
215
                'BirthdayYear' => 0,
216
            ],
217
            'LoadNonBlankDataFrom() loads only fields with values, and doesnt overwrite existing values'
218
        );
219
    }
220
221
    public function testLoadDataFromClearMissingFields()
222
    {
223
        $form = new Form(
224
            Controller::curr(),
225
            'Form',
226
            new FieldList(
227
                new HeaderField('MyPlayerHeader', 'My Player'),
228
                new TextField('Name'), // appears in both Player and Team
229
                new TextareaField('Biography'),
230
                new DateField('Birthday'),
231
                new NumericField('BirthdayYear'), // dynamic property
232
                $unrelatedField = new TextField('UnrelatedFormField')
233
                //new CheckboxSetField('Teams') // relation editing
234
            ),
235
            new FieldList()
236
        );
237
        $unrelatedField->setValue("random value");
238
239
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
240
        $form->loadDataFrom($captainWithDetails);
241
        $this->assertEquals(
242
            $form->getData(),
243
            [
244
                'Name' => 'Captain Details',
245
                'Biography' => 'Bio 1',
246
                'Birthday' => '1982-01-01',
247
                'BirthdayYear' => '1982',
248
                'UnrelatedFormField' => 'random value',
249
            ],
250
            'LoadDataFrom() doesnt overwrite fields not found in the object'
251
        );
252
253
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
254
        $team2 = $this->objFromFixture(Team::class, 'team2');
255
        $form->loadDataFrom($captainWithDetails);
256
        $form->loadDataFrom($team2, Form::MERGE_CLEAR_MISSING);
257
        $this->assertEquals(
258
            $form->getData(),
259
            [
260
                'Name' => 'Team 2',
261
                'Biography' => '',
262
                'Birthday' => '',
263
                'BirthdayYear' => 0,
264
                'UnrelatedFormField' => null,
265
            ],
266
            'LoadDataFrom() overwrites fields not found in the object with $clearMissingFields=true'
267
        );
268
    }
269
270
    public function testLoadDataFromWithForceSetValueFlag()
271
    {
272
        // Get our data formatted in internal value and in submitted value
273
        // We're using very esoteric date and time format
274
        $dataInSubmittedValue = [
275
            'SomeDateTimeField' => 'Fri, Jun 15, \'18 17:28:05',
276
            'SomeTimeField' => '05 o\'clock PM 28 05'
277
        ];
278
        $dataInInternalValue = [
279
            'SomeDateTimeField' => '2018-06-15 17:28:05',
280
            'SomeTimeField' => '17:28:05'
281
        ];
282
283
        // Test loading our data with the MERGE_AS_INTERNAL_VALUE
284
        $form = $this->getStubFormWithWeirdValueFormat();
285
        $form->loadDataFrom($dataInInternalValue, Form::MERGE_AS_INTERNAL_VALUE);
286
287
        $this->assertEquals(
288
            $dataInInternalValue,
289
            $form->getData()
290
        );
291
292
        // Test loading our data with the MERGE_AS_SUBMITTED_VALUE and an data passed as an object
293
        $form = $this->getStubFormWithWeirdValueFormat();
294
        $form->loadDataFrom(ArrayData::create($dataInSubmittedValue), Form::MERGE_AS_SUBMITTED_VALUE);
295
        $this->assertEquals(
296
            $dataInInternalValue,
297
            $form->getData()
298
        );
299
300
        // Test loading our data without the MERGE_AS_INTERNAL_VALUE and without MERGE_AS_SUBMITTED_VALUE
301
        $form = $this->getStubFormWithWeirdValueFormat();
302
        $form->loadDataFrom($dataInSubmittedValue);
303
304
        $this->assertEquals(
305
            $dataInInternalValue,
306
            $form->getData()
307
        );
308
    }
309
310
    public function testLookupFieldDisabledSaving()
311
    {
312
        $object = new Team();
313
        $form = new Form(
314
            Controller::curr(),
315
            'Form',
316
            new FieldList(
317
                new LookupField('Players', 'Players')
318
            ),
319
            new FieldList()
320
        );
321
        $form->loadDataFrom(
322
            [
323
            'Players' => [
324
                    14,
325
                    18,
326
                    22
327
                ],
328
            ]
329
        );
330
        $form->saveInto($object);
331
        $playersIds = $object->Players()->getIDList();
332
333
        $this->assertTrue($form->validationResult()->isValid());
334
        $this->assertEquals(
335
            $playersIds,
336
            [],
337
            'saveInto() should not save into the DataObject for the LookupField'
338
        );
339
    }
340
341
    public function testDefaultAction()
342
    {
343
        $form = Form::create(Controller::curr(), 'Form', new FieldList(), new FieldList(
344
            new FormAction('doForm', 'Form Action')
345
        ));
346
        $this->assertNotNull($form->defaultAction());
347
        $this->assertEquals('action_doForm', $form->defaultAction()->getName());
348
349
        $form = Form::create(Controller::curr(), 'AnotherForm', new FieldList(), new FieldList(
350
            new CompositeField(
351
                new FormAction('doAnotherForm', 'Another Form Action')
352
            )
353
        ));
354
        $this->assertNotNull($form->defaultAction());
355
        $this->assertEquals('action_doAnotherForm', $form->defaultAction()->getName());
356
    }
357
358
    public function testLoadDataFromIgnoreFalseish()
359
    {
360
        $form = new Form(
361
            Controller::curr(),
362
            'Form',
363
            new FieldList(
364
                new TextField('Biography', 'Biography', 'Custom Default')
365
            ),
366
            new FieldList()
367
        );
368
369
        $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
370
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
371
372
        $form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH);
373
        $this->assertEquals(
374
            $form->getData(),
375
            ['Biography' => 'Custom Default'],
376
            'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish'
377
        );
378
379
        $form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH);
380
        $this->assertEquals(
381
            $form->getData(),
382
            ['Biography' => 'Bio 1'],
383
            'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish'
384
        );
385
    }
386
387
    public function testFormMethodOverride()
388
    {
389
        $form = $this->getStubForm();
390
        $form->setFormMethod('GET');
391
        $this->assertNull($form->Fields()->dataFieldByName('_method'));
392
393
        $form = $this->getStubForm();
394
        $form->setFormMethod('PUT');
395
        $this->assertEquals(
396
            $form->Fields()->dataFieldByName('_method')->Value(),
397
            'PUT',
398
            'PUT override in forms has PUT in hiddenfield'
399
        );
400
        $this->assertEquals(
401
            $form->FormMethod(),
402
            'POST',
403
            'PUT override in forms has POST in <form> tag'
404
        );
405
406
        $form = $this->getStubForm();
407
        $form->setFormMethod('DELETE');
408
        $this->assertEquals(
409
            $form->Fields()->dataFieldByName('_method')->Value(),
410
            'DELETE',
411
            'PUT override in forms has PUT in hiddenfield'
412
        );
413
        $this->assertEquals(
414
            $form->FormMethod(),
415
            'POST',
416
            'PUT override in forms has POST in <form> tag'
417
        );
418
    }
419
420
    public function testValidationExemptActions()
421
    {
422
        $this->get('FormTest_Controller');
423
424
        $this->submitForm(
425
            'Form_Form',
426
            'action_doSubmit',
427
            [
428
                'Email' => '[email protected]'
429
            ]
430
        );
431
432
        // Firstly, assert that required fields still work when not using an exempt action
433
        $this->assertPartialMatchBySelector(
434
            '#Form_Form_SomeRequiredField_Holder .required',
435
            ['"Some required field" is required'],
436
            'Required fields show a notification on field when left blank'
437
        );
438
439
        // Re-submit the form using validation-exempt button
440
        $this->submitForm(
441
            'Form_Form',
442
            'action_doSubmitValidationExempt',
443
            [
444
                'Email' => '[email protected]'
445
            ]
446
        );
447
448
        // The required message should be empty if validation was skipped
449
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
450
        $this->assertEmpty($items);
451
452
        // And the session message should show up is submitted successfully
453
        $this->assertPartialMatchBySelector(
454
            '#Form_Form_error',
455
            [
456
                'Validation skipped'
457
            ],
458
            'Form->sessionMessage() shows up after reloading the form'
459
        );
460
461
        // Test this same behaviour, but with a form-action exempted via instance
462
        $this->submitForm(
463
            'Form_Form',
464
            'action_doSubmitActionExempt',
465
            [
466
                'Email' => '[email protected]'
467
            ]
468
        );
469
470
        // The required message should be empty if validation was skipped
471
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
472
        $this->assertEmpty($items);
473
474
        // And the session message should show up is submitted successfully
475
        $this->assertPartialMatchBySelector(
476
            '#Form_Form_error',
477
            [
478
                'Validation bypassed!'
479
            ],
480
            'Form->sessionMessage() shows up after reloading the form'
481
        );
482
    }
483
484
    public function testSessionValidationMessage()
485
    {
486
        $this->get('FormTest_Controller');
487
488
        $response = $this->post(
489
            'FormTest_Controller/Form',
490
            [
491
                'Email' => 'invalid',
492
                'Number' => '<a href="http://mysite.com">link</a>' // XSS attempt
493
                // leaving out "Required" field
494
            ]
495
        );
496
497
        $this->assertPartialMatchBySelector(
498
            '#Form_Form_Email_Holder span.message',
499
            [
500
                'Please enter an email address'
501
            ],
502
            'Formfield validation shows note on field if invalid'
503
        );
504
        $this->assertPartialMatchBySelector(
505
            '#Form_Form_SomeRequiredField_Holder span.required',
506
            [
507
                '"Some required field" is required'
508
            ],
509
            'Required fields show a notification on field when left blank'
510
        );
511
512
        $this->assertContains(
513
            '&#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',
514
            $response->getBody(),
515
            "Validation messages are safely XML encoded"
516
        );
517
        $this->assertNotContains(
518
            '<a href="http://mysite.com">link</a>',
519
            $response->getBody(),
520
            "Unsafe content is not emitted directly inside the response body"
521
        );
522
    }
523
524
    public function testSessionSuccessMessage()
525
    {
526
        $this->get('FormTest_Controller');
527
528
        $this->post(
529
            'FormTest_Controller/Form',
530
            [
531
                'Email' => '[email protected]',
532
                'SomeRequiredField' => 'test',
533
            ]
534
        );
535
        $this->assertPartialMatchBySelector(
536
            '#Form_Form_error',
537
            [
538
                'Test save was successful'
539
            ],
540
            'Form->sessionMessage() shows up after reloading the form'
541
        );
542
    }
543
544
    public function testValidationException()
545
    {
546
        $this->get('FormTest_Controller');
547
548
        $this->post(
549
            'FormTest_Controller/Form',
550
            [
551
                'Email' => '[email protected]',
552
                'SomeRequiredField' => 'test',
553
                'action_doTriggerException' => 1,
554
            ]
555
        );
556
        $this->assertPartialMatchBySelector(
557
            '#Form_Form_Email_Holder span.message',
558
            [
559
                'Error on Email field'
560
            ],
561
            'Formfield validation shows note on field if invalid'
562
        );
563
        $this->assertPartialMatchBySelector(
564
            '#Form_Form_error',
565
            [
566
                'Error at top of form'
567
            ],
568
            'Required fields show a notification on field when left blank'
569
        );
570
    }
571
572
    public function testGloballyDisabledSecurityTokenInheritsToNewForm()
573
    {
574
        SecurityToken::enable();
575
576
        $form1 = $this->getStubForm();
577
        $this->assertInstanceOf(SecurityToken::class, $form1->getSecurityToken());
578
579
        SecurityToken::disable();
580
581
        $form2 = $this->getStubForm();
582
        $this->assertInstanceOf(NullSecurityToken::class, $form2->getSecurityToken());
583
584
        SecurityToken::enable();
585
    }
586
587
    public function testDisableSecurityTokenDoesntAddTokenFormField()
588
    {
589
        SecurityToken::enable();
590
591
        $formWithToken = $this->getStubForm();
592
        $this->assertInstanceOf(
593
            'SilverStripe\\Forms\\HiddenField',
594
            $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...
595
            'Token field added by default'
596
        );
597
598
        $formWithoutToken = $this->getStubForm();
599
        $formWithoutToken->disableSecurityToken();
600
        $this->assertNull(
601
            $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...
602
            'Token field not added if disableSecurityToken() is set'
603
        );
604
    }
605
606
    public function testDisableSecurityTokenAcceptsSubmissionWithoutToken()
607
    {
608
        SecurityToken::enable();
609
        $expectedToken = SecurityToken::inst()->getValue();
610
611
        $this->get('FormTest_ControllerWithSecurityToken');
612
        // can't use submitForm() as it'll automatically insert SecurityID into the POST data
613
        $response = $this->post(
614
            'FormTest_ControllerWithSecurityToken/Form',
615
            [
616
                'Email' => '[email protected]',
617
                'action_doSubmit' => 1
618
                // leaving out security token
619
            ]
620
        );
621
        $this->assertEquals(400, $response->getStatusCode(), 'Submission fails without security token');
622
623
        // Generate a new token which doesn't match the current one
624
        $generator = new RandomGenerator();
625
        $invalidToken = $generator->randomToken('sha1');
626
        $this->assertNotEquals($invalidToken, $expectedToken);
627
628
        // Test token with request
629
        $this->get('FormTest_ControllerWithSecurityToken');
630
        $response = $this->post(
631
            'FormTest_ControllerWithSecurityToken/Form',
632
            [
633
                'Email' => '[email protected]',
634
                'action_doSubmit' => 1,
635
                'SecurityID' => $invalidToken
636
            ]
637
        );
638
        $this->assertEquals(200, $response->getStatusCode(), 'Submission reloads form if security token invalid');
639
        $this->assertTrue(
640
            stripos($response->getBody(), 'name="SecurityID" value="' . $expectedToken . '"') !== false,
641
            'Submission reloads with correct security token after failure'
642
        );
643
        $this->assertTrue(
644
            stripos($response->getBody(), 'name="SecurityID" value="' . $invalidToken . '"') === false,
645
            'Submission reloads without incorrect security token after failure'
646
        );
647
648
        $matched = $this->cssParser()->getBySelector('#Form_Form_Email');
649
        $attrs = $matched[0]->attributes();
650
        $this->assertEquals('[email protected]', (string)$attrs['value'], 'Submitted data is preserved');
651
652
        $this->get('FormTest_ControllerWithSecurityToken');
653
        $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
654
        $this->assertEquals(
655
            1,
656
            count($tokenEls),
657
            'Token form field added for controller without disableSecurityToken()'
658
        );
659
        $token = (string)$tokenEls[0];
660
        $response = $this->submitForm(
661
            'Form_Form',
662
            null,
663
            [
664
                'Email' => '[email protected]',
665
                'SecurityID' => $token
666
            ]
667
        );
668
        $this->assertEquals(200, $response->getStatusCode(), 'Submission suceeds with security token');
669
    }
670
671
    public function testStrictFormMethodChecking()
672
    {
673
        $this->get('FormTest_ControllerWithStrictPostCheck');
674
        $response = $this->get(
675
            'FormTest_ControllerWithStrictPostCheck/Form/[email protected]&action_doSubmit=1'
676
        );
677
        $this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method');
678
679
        $this->get('FormTest_ControllerWithStrictPostCheck');
680
        $response = $this->post(
681
            'FormTest_ControllerWithStrictPostCheck/Form',
682
            [
683
                'Email' => '[email protected]',
684
                'action_doSubmit' => 1
685
            ]
686
        );
687
        $this->assertEquals(200, $response->getStatusCode(), 'Submission succeeds with correct method');
688
    }
689
690
    public function testEnableSecurityToken()
691
    {
692
        SecurityToken::disable();
693
        $form = $this->getStubForm();
694
        $this->assertFalse($form->getSecurityToken()->isEnabled());
695
        $form->enableSecurityToken();
696
        $this->assertTrue($form->getSecurityToken()->isEnabled());
697
698
        SecurityToken::disable(); // restore original
699
    }
700
701
    public function testDisableSecurityToken()
702
    {
703
        SecurityToken::enable();
704
        $form = $this->getStubForm();
705
        $this->assertTrue($form->getSecurityToken()->isEnabled());
706
        $form->disableSecurityToken();
707
        $this->assertFalse($form->getSecurityToken()->isEnabled());
708
709
        SecurityToken::disable(); // restore original
710
    }
711
712
    public function testEncType()
713
    {
714
        $form = $this->getStubForm();
715
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
716
717
        $form->setEncType(Form::ENC_TYPE_MULTIPART);
718
        $this->assertEquals('multipart/form-data', $form->getEncType());
719
720
        $form = $this->getStubForm();
721
        $form->Fields()->push(new FileField(null));
722
        $this->assertEquals('multipart/form-data', $form->getEncType());
723
724
        $form->setEncType(Form::ENC_TYPE_URLENCODED);
725
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
726
    }
727
728
    public function testAddExtraClass()
729
    {
730
        $form = $this->getStubForm();
731
        $form->addExtraClass('class1');
732
        $form->addExtraClass('class2');
733
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
734
    }
735
736
    public function testRemoveExtraClass()
737
    {
738
        $form = $this->getStubForm();
739
        $form->addExtraClass('class1');
740
        $form->addExtraClass('class2');
741
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
742
        $form->removeExtraClass('class1');
743
        $this->assertStringEndsWith('class2', $form->extraClass());
744
    }
745
746
    public function testAddManyExtraClasses()
747
    {
748
        $form = $this->getStubForm();
749
        //test we can split by a range of spaces and tabs
750
        $form->addExtraClass('class1 class2     class3	class4		class5');
751
        $this->assertStringEndsWith(
752
            'class1 class2 class3 class4 class5',
753
            $form->extraClass()
754
        );
755
        //test that duplicate classes don't get added
756
        $form->addExtraClass('class1 class2');
757
        $this->assertStringEndsWith(
758
            'class1 class2 class3 class4 class5',
759
            $form->extraClass()
760
        );
761
    }
762
763
    public function testRemoveManyExtraClasses()
764
    {
765
        $form = $this->getStubForm();
766
        $form->addExtraClass('class1 class2     class3	class4		class5');
767
        //test we can remove a single class we just added
768
        $form->removeExtraClass('class3');
769
        $this->assertStringEndsWith(
770
            'class1 class2 class4 class5',
771
            $form->extraClass()
772
        );
773
        //check we can remove many classes at once
774
        $form->removeExtraClass('class1 class5');
775
        $this->assertStringEndsWith(
776
            'class2 class4',
777
            $form->extraClass()
778
        );
779
        //check that removing a dud class is fine
780
        $form->removeExtraClass('dudClass');
781
        $this->assertStringEndsWith(
782
            'class2 class4',
783
            $form->extraClass()
784
        );
785
    }
786
787
    public function testDefaultClasses()
788
    {
789
        Form::config()->update(
790
            'default_classes',
791
            [
792
            'class1',
793
            ]
794
        );
795
796
        $form = $this->getStubForm();
797
798
        $this->assertContains('class1', $form->extraClass(), 'Class list does not contain expected class');
799
800
        Form::config()->update(
801
            'default_classes',
802
            [
803
            'class1',
804
            'class2',
805
            ]
806
        );
807
808
        $form = $this->getStubForm();
809
810
        $this->assertContains('class1 class2', $form->extraClass(), 'Class list does not contain expected class');
811
812
        Form::config()->update(
813
            'default_classes',
814
            [
815
            'class3',
816
            ]
817
        );
818
819
        $form = $this->getStubForm();
820
821
        $this->assertContains('class3', $form->extraClass(), 'Class list does not contain expected class');
822
823
        $form->removeExtraClass('class3');
824
825
        $this->assertNotContains('class3', $form->extraClass(), 'Class list contains unexpected class');
826
    }
827
828
    public function testAttributes()
829
    {
830
        $form = $this->getStubForm();
831
        $form->setAttribute('foo', 'bar');
832
        $this->assertEquals('bar', $form->getAttribute('foo'));
833
        $attrs = $form->getAttributes();
834
        $this->assertArrayHasKey('foo', $attrs);
835
        $this->assertEquals('bar', $attrs['foo']);
836
    }
837
838
    /**
839
     * @skipUpgrade
840
     */
841
    public function testButtonClicked()
842
    {
843
        $form = $this->getStubForm();
844
        $action = $form->getRequestHandler()->buttonClicked();
845
        $this->assertNull($action);
846
847
        $controller = new FormTest\TestController();
848
        $form = $controller->Form();
849
        $request = new HTTPRequest(
850
            'POST',
851
            'FormTest_Controller/Form',
852
            [],
853
            [
854
            'Email' => '[email protected]',
855
            'SomeRequiredField' => 1,
856
            'action_doSubmit' => 1
857
            ]
858
        );
859
        $request->setSession(new Session([]));
860
861
        $form->getRequestHandler()->httpSubmission($request);
862
        $button = $form->getRequestHandler()->buttonClicked();
863
        $this->assertInstanceOf(FormAction::class, $button);
864
        $this->assertEquals('doSubmit', $button->actionName());
865
        $form = new Form(
866
            $controller,
867
            'Form',
868
            new FieldList(new FormAction('doSubmit', 'Inline action')),
869
            new FieldList()
870
        );
871
        $form->disableSecurityToken();
872
        $request = new HTTPRequest(
873
            'POST',
874
            'FormTest_Controller/Form',
875
            [],
876
            [
877
            'action_doSubmit' => 1
878
            ]
879
        );
880
        $request->setSession(new Session([]));
881
882
        $form->getRequestHandler()->httpSubmission($request);
883
        $button = $form->getRequestHandler()->buttonClicked();
884
        $this->assertInstanceOf(FormAction::class, $button);
885
        $this->assertEquals('doSubmit', $button->actionName());
886
    }
887
888
    public function testCheckAccessAction()
889
    {
890
        $controller = new FormTest\TestController();
891
        $form = new Form(
892
            $controller,
893
            'Form',
894
            new FieldList(),
895
            new FieldList(new FormAction('actionName', 'Action'))
896
        );
897
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName'));
898
899
        $form = new Form(
900
            $controller,
901
            'Form',
902
            new FieldList(new FormAction('inlineAction', 'Inline action')),
903
            new FieldList()
904
        );
905
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction'));
906
    }
907
908
    public function testAttributesHTML()
909
    {
910
        $form = $this->getStubForm();
911
912
        $form->setAttribute('foo', 'bar');
913
        $this->assertContains('foo="bar"', $form->getAttributesHTML());
914
915
        $form->setAttribute('foo', null);
916
        $this->assertNotContains('foo="bar"', $form->getAttributesHTML());
917
918
        $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

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

1140
                html_entity_decode('&nbsp;', /** @scrutinizer ignore-type */ null, 'UTF-8'),
Loading history...
1141
                html_entity_decode('&#8239;', null, 'UTF-8'), // narrow non-breaking space
1142
            ],
1143
            ' ',
1144
            trim($input)
1145
        );
1146
    }
1147
}
1148