Passed
Pull Request — 4 (#10112)
by Guy
06:11
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\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()
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->assertContains('<input type="text" name="Number" value="888"', $response->getBody());
144
145
146
        // Readonly field remains
147
        $this->assertContains(
148
            '<input type="text" name="ReadonlyField" value="This value is readonly"',
149
            $response->getBody()
150
        );
151
152
        $this->assertNotContains('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->assertContains(
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->assertNotContains(
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->extraClass('class2'));
0 ignored issues
show
Unused Code introduced by
The call to SilverStripe\Forms\Form::extraClass() has too many arguments starting with 'class2'. ( Ignorable by Annotation )

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

735
        $this->assertTrue($form->/** @scrutinizer ignore-call */ extraClass('class2'));

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

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

Loading history...
Bug introduced by
$form->extraClass('class2') of type string is incompatible with the type boolean expected by parameter $condition of PHPUnit_Framework_Assert::assertTrue(). ( Ignorable by Annotation )

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

735
        $this->assertTrue(/** @scrutinizer ignore-type */ $form->extraClass('class2'));
Loading history...
736
        $this->assertTrue($form->extraClass('class1 class2'));
737
        $this->assertTrue($form->extraClass('class2 class1'));
738
        $this->assertFalse($form->extraClass('class3'));
0 ignored issues
show
Bug introduced by
$form->extraClass('class3') of type string is incompatible with the type boolean expected by parameter $condition of PHPUnit_Framework_Assert::assertFalse(). ( Ignorable by Annotation )

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

738
        $this->assertFalse(/** @scrutinizer ignore-type */ $form->extraClass('class3'));
Loading history...
739
    }
740
741
    public function testRemoveExtraClass()
742
    {
743
        $form = $this->getStubForm();
744
        $form->addExtraClass('class1');
745
        $form->addExtraClass('class2');
746
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
747
        $form->removeExtraClass('class1');
748
        $this->assertStringEndsWith('class2', $form->extraClass());
749
    }
750
751
    public function testAddManyExtraClasses()
752
    {
753
        $form = $this->getStubForm();
754
        //test we can split by a range of spaces and tabs
755
        $form->addExtraClass('class1 class2     class3	class4		class5');
756
        $this->assertStringEndsWith(
757
            'class1 class2 class3 class4 class5',
758
            $form->extraClass()
759
        );
760
        //test that duplicate classes don't get added
761
        $form->addExtraClass('class1 class2');
762
        $this->assertStringEndsWith(
763
            'class1 class2 class3 class4 class5',
764
            $form->extraClass()
765
        );
766
    }
767
768
    public function testRemoveManyExtraClasses()
769
    {
770
        $form = $this->getStubForm();
771
        $form->addExtraClass('class1 class2     class3	class4		class5');
772
        //test we can remove a single class we just added
773
        $form->removeExtraClass('class3');
774
        $this->assertStringEndsWith(
775
            'class1 class2 class4 class5',
776
            $form->extraClass()
777
        );
778
        //check we can remove many classes at once
779
        $form->removeExtraClass('class1 class5');
780
        $this->assertStringEndsWith(
781
            'class2 class4',
782
            $form->extraClass()
783
        );
784
        //check that removing a dud class is fine
785
        $form->removeExtraClass('dudClass');
786
        $this->assertStringEndsWith(
787
            'class2 class4',
788
            $form->extraClass()
789
        );
790
    }
791
792
    public function testDefaultClasses()
793
    {
794
        Form::config()->update(
795
            'default_classes',
796
            [
797
            'class1',
798
            ]
799
        );
800
801
        $form = $this->getStubForm();
802
803
        $this->assertContains('class1', $form->extraClass(), 'Class list does not contain expected class');
804
805
        Form::config()->update(
806
            'default_classes',
807
            [
808
            'class1',
809
            'class2',
810
            ]
811
        );
812
813
        $form = $this->getStubForm();
814
815
        $this->assertContains('class1 class2', $form->extraClass(), 'Class list does not contain expected class');
816
817
        Form::config()->update(
818
            'default_classes',
819
            [
820
            'class3',
821
            ]
822
        );
823
824
        $form = $this->getStubForm();
825
826
        $this->assertContains('class3', $form->extraClass(), 'Class list does not contain expected class');
827
828
        $form->removeExtraClass('class3');
829
830
        $this->assertNotContains('class3', $form->extraClass(), 'Class list contains unexpected class');
831
    }
832
833
    public function testAttributes()
834
    {
835
        $form = $this->getStubForm();
836
        $form->setAttribute('foo', 'bar');
837
        $this->assertEquals('bar', $form->getAttribute('foo'));
838
        $attrs = $form->getAttributes();
839
        $this->assertArrayHasKey('foo', $attrs);
840
        $this->assertEquals('bar', $attrs['foo']);
841
    }
842
843
    /**
844
     * @skipUpgrade
845
     */
846
    public function testButtonClicked()
847
    {
848
        $form = $this->getStubForm();
849
        $action = $form->getRequestHandler()->buttonClicked();
850
        $this->assertNull($action);
851
852
        $controller = new FormTest\TestController();
853
        $form = $controller->Form();
854
        $request = new HTTPRequest(
855
            'POST',
856
            'FormTest_Controller/Form',
857
            [],
858
            [
859
            'Email' => '[email protected]',
860
            'SomeRequiredField' => 1,
861
            'action_doSubmit' => 1
862
            ]
863
        );
864
        $request->setSession(new Session([]));
865
866
        $form->getRequestHandler()->httpSubmission($request);
867
        $button = $form->getRequestHandler()->buttonClicked();
868
        $this->assertInstanceOf(FormAction::class, $button);
869
        $this->assertEquals('doSubmit', $button->actionName());
870
        $form = new Form(
871
            $controller,
872
            'Form',
873
            new FieldList(new FormAction('doSubmit', 'Inline action')),
874
            new FieldList()
875
        );
876
        $form->disableSecurityToken();
877
        $request = new HTTPRequest(
878
            'POST',
879
            'FormTest_Controller/Form',
880
            [],
881
            [
882
            'action_doSubmit' => 1
883
            ]
884
        );
885
        $request->setSession(new Session([]));
886
887
        $form->getRequestHandler()->httpSubmission($request);
888
        $button = $form->getRequestHandler()->buttonClicked();
889
        $this->assertInstanceOf(FormAction::class, $button);
890
        $this->assertEquals('doSubmit', $button->actionName());
891
    }
892
893
    public function testCheckAccessAction()
894
    {
895
        $controller = new FormTest\TestController();
896
        $form = new Form(
897
            $controller,
898
            'Form',
899
            new FieldList(),
900
            new FieldList(new FormAction('actionName', 'Action'))
901
        );
902
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName'));
903
904
        $form = new Form(
905
            $controller,
906
            'Form',
907
            new FieldList(new FormAction('inlineAction', 'Inline action')),
908
            new FieldList()
909
        );
910
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction'));
911
    }
912
913
    public function testAttributesHTML()
914
    {
915
        $form = $this->getStubForm();
916
917
        $form->setAttribute('foo', 'bar');
918
        $this->assertContains('foo="bar"', $form->getAttributesHTML());
919
920
        $form->setAttribute('foo', null);
921
        $this->assertNotContains('foo="bar"', $form->getAttributesHTML());
922
923
        $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

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

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