Passed
Push — pulls/fix-nbsp-test ( b9885a )
by Sam
08:54
created

FormTest::testAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 8
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
            ),
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 testLoadDataFromIgnoreFalseish()
342
    {
343
        $form = new Form(
344
            Controller::curr(),
345
            'Form',
346
            new FieldList(
347
                new TextField('Biography', 'Biography', 'Custom Default')
348
            ),
349
            new FieldList()
350
        );
351
352
        $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails');
353
        $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails');
354
355
        $form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH);
356
        $this->assertEquals(
357
            $form->getData(),
358
            ['Biography' => 'Custom Default'],
359
            'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish'
360
        );
361
362
        $form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH);
363
        $this->assertEquals(
364
            $form->getData(),
365
            ['Biography' => 'Bio 1'],
366
            'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish'
367
        );
368
    }
369
370
    public function testFormMethodOverride()
371
    {
372
        $form = $this->getStubForm();
373
        $form->setFormMethod('GET');
374
        $this->assertNull($form->Fields()->dataFieldByName('_method'));
375
376
        $form = $this->getStubForm();
377
        $form->setFormMethod('PUT');
378
        $this->assertEquals(
379
            $form->Fields()->dataFieldByName('_method')->Value(),
380
            'PUT',
381
            'PUT override in forms has PUT in hiddenfield'
382
        );
383
        $this->assertEquals(
384
            $form->FormMethod(),
385
            'POST',
386
            'PUT override in forms has POST in <form> tag'
387
        );
388
389
        $form = $this->getStubForm();
390
        $form->setFormMethod('DELETE');
391
        $this->assertEquals(
392
            $form->Fields()->dataFieldByName('_method')->Value(),
393
            'DELETE',
394
            'PUT override in forms has PUT in hiddenfield'
395
        );
396
        $this->assertEquals(
397
            $form->FormMethod(),
398
            'POST',
399
            'PUT override in forms has POST in <form> tag'
400
        );
401
    }
402
403
    public function testValidationExemptActions()
404
    {
405
        $this->get('FormTest_Controller');
406
407
        $this->submitForm(
408
            'Form_Form',
409
            'action_doSubmit',
410
            [
411
                'Email' => '[email protected]'
412
            ]
413
        );
414
415
        // Firstly, assert that required fields still work when not using an exempt action
416
        $this->assertPartialMatchBySelector(
417
            '#Form_Form_SomeRequiredField_Holder .required',
418
            ['"Some required field" is required'],
419
            'Required fields show a notification on field when left blank'
420
        );
421
422
        // Re-submit the form using validation-exempt button
423
        $this->submitForm(
424
            'Form_Form',
425
            'action_doSubmitValidationExempt',
426
            [
427
                'Email' => '[email protected]'
428
            ]
429
        );
430
431
        // The required message should be empty if validation was skipped
432
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
433
        $this->assertEmpty($items);
434
435
        // And the session message should show up is submitted successfully
436
        $this->assertPartialMatchBySelector(
437
            '#Form_Form_error',
438
            [
439
                'Validation skipped'
440
            ],
441
            'Form->sessionMessage() shows up after reloading the form'
442
        );
443
444
        // Test this same behaviour, but with a form-action exempted via instance
445
        $this->submitForm(
446
            'Form_Form',
447
            'action_doSubmitActionExempt',
448
            [
449
                'Email' => '[email protected]'
450
            ]
451
        );
452
453
        // The required message should be empty if validation was skipped
454
        $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
455
        $this->assertEmpty($items);
456
457
        // And the session message should show up is submitted successfully
458
        $this->assertPartialMatchBySelector(
459
            '#Form_Form_error',
460
            [
461
                'Validation bypassed!'
462
            ],
463
            'Form->sessionMessage() shows up after reloading the form'
464
        );
465
    }
466
467
    public function testSessionValidationMessage()
468
    {
469
        $this->get('FormTest_Controller');
470
471
        $response = $this->post(
472
            'FormTest_Controller/Form',
473
            [
474
                'Email' => 'invalid',
475
                'Number' => '<a href="http://mysite.com">link</a>' // XSS attempt
476
                // leaving out "Required" field
477
            ]
478
        );
479
480
        $this->assertPartialMatchBySelector(
481
            '#Form_Form_Email_Holder span.message',
482
            [
483
                'Please enter an email address'
484
            ],
485
            'Formfield validation shows note on field if invalid'
486
        );
487
        $this->assertPartialMatchBySelector(
488
            '#Form_Form_SomeRequiredField_Holder span.required',
489
            [
490
                '"Some required field" is required'
491
            ],
492
            'Required fields show a notification on field when left blank'
493
        );
494
495
        $this->assertContains(
496
            '&#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',
497
            $response->getBody(),
498
            "Validation messages are safely XML encoded"
499
        );
500
        $this->assertNotContains(
501
            '<a href="http://mysite.com">link</a>',
502
            $response->getBody(),
503
            "Unsafe content is not emitted directly inside the response body"
504
        );
505
    }
506
507
    public function testSessionSuccessMessage()
508
    {
509
        $this->get('FormTest_Controller');
510
511
        $this->post(
512
            'FormTest_Controller/Form',
513
            [
514
                'Email' => '[email protected]',
515
                'SomeRequiredField' => 'test',
516
            ]
517
        );
518
        $this->assertPartialMatchBySelector(
519
            '#Form_Form_error',
520
            [
521
                'Test save was successful'
522
            ],
523
            'Form->sessionMessage() shows up after reloading the form'
524
        );
525
    }
526
527
    public function testValidationException()
528
    {
529
        $this->get('FormTest_Controller');
530
531
        $this->post(
532
            'FormTest_Controller/Form',
533
            [
534
                'Email' => '[email protected]',
535
                'SomeRequiredField' => 'test',
536
                'action_doTriggerException' => 1,
537
            ]
538
        );
539
        $this->assertPartialMatchBySelector(
540
            '#Form_Form_Email_Holder span.message',
541
            [
542
                'Error on Email field'
543
            ],
544
            'Formfield validation shows note on field if invalid'
545
        );
546
        $this->assertPartialMatchBySelector(
547
            '#Form_Form_error',
548
            [
549
                'Error at top of form'
550
            ],
551
            'Required fields show a notification on field when left blank'
552
        );
553
    }
554
555
    public function testGloballyDisabledSecurityTokenInheritsToNewForm()
556
    {
557
        SecurityToken::enable();
558
559
        $form1 = $this->getStubForm();
560
        $this->assertInstanceOf(SecurityToken::class, $form1->getSecurityToken());
561
562
        SecurityToken::disable();
563
564
        $form2 = $this->getStubForm();
565
        $this->assertInstanceOf(NullSecurityToken::class, $form2->getSecurityToken());
566
567
        SecurityToken::enable();
568
    }
569
570
    public function testDisableSecurityTokenDoesntAddTokenFormField()
571
    {
572
        SecurityToken::enable();
573
574
        $formWithToken = $this->getStubForm();
575
        $this->assertInstanceOf(
576
            'SilverStripe\\Forms\\HiddenField',
577
            $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...
578
            'Token field added by default'
579
        );
580
581
        $formWithoutToken = $this->getStubForm();
582
        $formWithoutToken->disableSecurityToken();
583
        $this->assertNull(
584
            $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...
585
            'Token field not added if disableSecurityToken() is set'
586
        );
587
    }
588
589
    public function testDisableSecurityTokenAcceptsSubmissionWithoutToken()
590
    {
591
        SecurityToken::enable();
592
        $expectedToken = SecurityToken::inst()->getValue();
593
594
        $this->get('FormTest_ControllerWithSecurityToken');
595
        // can't use submitForm() as it'll automatically insert SecurityID into the POST data
596
        $response = $this->post(
597
            'FormTest_ControllerWithSecurityToken/Form',
598
            [
599
                'Email' => '[email protected]',
600
                'action_doSubmit' => 1
601
                // leaving out security token
602
            ]
603
        );
604
        $this->assertEquals(400, $response->getStatusCode(), 'Submission fails without security token');
605
606
        // Generate a new token which doesn't match the current one
607
        $generator = new RandomGenerator();
608
        $invalidToken = $generator->randomToken('sha1');
609
        $this->assertNotEquals($invalidToken, $expectedToken);
610
611
        // Test token with request
612
        $this->get('FormTest_ControllerWithSecurityToken');
613
        $response = $this->post(
614
            'FormTest_ControllerWithSecurityToken/Form',
615
            [
616
                'Email' => '[email protected]',
617
                'action_doSubmit' => 1,
618
                'SecurityID' => $invalidToken
619
            ]
620
        );
621
        $this->assertEquals(200, $response->getStatusCode(), 'Submission reloads form if security token invalid');
622
        $this->assertTrue(
623
            stripos($response->getBody(), 'name="SecurityID" value="' . $expectedToken . '"') !== false,
624
            'Submission reloads with correct security token after failure'
625
        );
626
        $this->assertTrue(
627
            stripos($response->getBody(), 'name="SecurityID" value="' . $invalidToken . '"') === false,
628
            'Submission reloads without incorrect security token after failure'
629
        );
630
631
        $matched = $this->cssParser()->getBySelector('#Form_Form_Email');
632
        $attrs = $matched[0]->attributes();
633
        $this->assertEquals('[email protected]', (string)$attrs['value'], 'Submitted data is preserved');
634
635
        $this->get('FormTest_ControllerWithSecurityToken');
636
        $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
637
        $this->assertEquals(
638
            1,
639
            count($tokenEls),
640
            'Token form field added for controller without disableSecurityToken()'
641
        );
642
        $token = (string)$tokenEls[0];
643
        $response = $this->submitForm(
644
            'Form_Form',
645
            null,
646
            [
647
                'Email' => '[email protected]',
648
                'SecurityID' => $token
649
            ]
650
        );
651
        $this->assertEquals(200, $response->getStatusCode(), 'Submission suceeds with security token');
652
    }
653
654
    public function testStrictFormMethodChecking()
655
    {
656
        $this->get('FormTest_ControllerWithStrictPostCheck');
657
        $response = $this->get(
658
            'FormTest_ControllerWithStrictPostCheck/Form/[email protected]&action_doSubmit=1'
659
        );
660
        $this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method');
661
662
        $this->get('FormTest_ControllerWithStrictPostCheck');
663
        $response = $this->post(
664
            'FormTest_ControllerWithStrictPostCheck/Form',
665
            [
666
                'Email' => '[email protected]',
667
                'action_doSubmit' => 1
668
            ]
669
        );
670
        $this->assertEquals(200, $response->getStatusCode(), 'Submission succeeds with correct method');
671
    }
672
673
    public function testEnableSecurityToken()
674
    {
675
        SecurityToken::disable();
676
        $form = $this->getStubForm();
677
        $this->assertFalse($form->getSecurityToken()->isEnabled());
678
        $form->enableSecurityToken();
679
        $this->assertTrue($form->getSecurityToken()->isEnabled());
680
681
        SecurityToken::disable(); // restore original
682
    }
683
684
    public function testDisableSecurityToken()
685
    {
686
        SecurityToken::enable();
687
        $form = $this->getStubForm();
688
        $this->assertTrue($form->getSecurityToken()->isEnabled());
689
        $form->disableSecurityToken();
690
        $this->assertFalse($form->getSecurityToken()->isEnabled());
691
692
        SecurityToken::disable(); // restore original
693
    }
694
695
    public function testEncType()
696
    {
697
        $form = $this->getStubForm();
698
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
699
700
        $form->setEncType(Form::ENC_TYPE_MULTIPART);
701
        $this->assertEquals('multipart/form-data', $form->getEncType());
702
703
        $form = $this->getStubForm();
704
        $form->Fields()->push(new FileField(null));
705
        $this->assertEquals('multipart/form-data', $form->getEncType());
706
707
        $form->setEncType(Form::ENC_TYPE_URLENCODED);
708
        $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
709
    }
710
711
    public function testAddExtraClass()
712
    {
713
        $form = $this->getStubForm();
714
        $form->addExtraClass('class1');
715
        $form->addExtraClass('class2');
716
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
717
    }
718
719
    public function testRemoveExtraClass()
720
    {
721
        $form = $this->getStubForm();
722
        $form->addExtraClass('class1');
723
        $form->addExtraClass('class2');
724
        $this->assertStringEndsWith('class1 class2', $form->extraClass());
725
        $form->removeExtraClass('class1');
726
        $this->assertStringEndsWith('class2', $form->extraClass());
727
    }
728
729
    public function testAddManyExtraClasses()
730
    {
731
        $form = $this->getStubForm();
732
        //test we can split by a range of spaces and tabs
733
        $form->addExtraClass('class1 class2     class3	class4		class5');
734
        $this->assertStringEndsWith(
735
            'class1 class2 class3 class4 class5',
736
            $form->extraClass()
737
        );
738
        //test that duplicate classes don't get added
739
        $form->addExtraClass('class1 class2');
740
        $this->assertStringEndsWith(
741
            'class1 class2 class3 class4 class5',
742
            $form->extraClass()
743
        );
744
    }
745
746
    public function testRemoveManyExtraClasses()
747
    {
748
        $form = $this->getStubForm();
749
        $form->addExtraClass('class1 class2     class3	class4		class5');
750
        //test we can remove a single class we just added
751
        $form->removeExtraClass('class3');
752
        $this->assertStringEndsWith(
753
            'class1 class2 class4 class5',
754
            $form->extraClass()
755
        );
756
        //check we can remove many classes at once
757
        $form->removeExtraClass('class1 class5');
758
        $this->assertStringEndsWith(
759
            'class2 class4',
760
            $form->extraClass()
761
        );
762
        //check that removing a dud class is fine
763
        $form->removeExtraClass('dudClass');
764
        $this->assertStringEndsWith(
765
            'class2 class4',
766
            $form->extraClass()
767
        );
768
    }
769
770
    public function testDefaultClasses()
771
    {
772
        Form::config()->update(
773
            'default_classes',
774
            [
775
            'class1',
776
            ]
777
        );
778
779
        $form = $this->getStubForm();
780
781
        $this->assertContains('class1', $form->extraClass(), 'Class list does not contain expected class');
782
783
        Form::config()->update(
784
            'default_classes',
785
            [
786
            'class1',
787
            'class2',
788
            ]
789
        );
790
791
        $form = $this->getStubForm();
792
793
        $this->assertContains('class1 class2', $form->extraClass(), 'Class list does not contain expected class');
794
795
        Form::config()->update(
796
            'default_classes',
797
            [
798
            'class3',
799
            ]
800
        );
801
802
        $form = $this->getStubForm();
803
804
        $this->assertContains('class3', $form->extraClass(), 'Class list does not contain expected class');
805
806
        $form->removeExtraClass('class3');
807
808
        $this->assertNotContains('class3', $form->extraClass(), 'Class list contains unexpected class');
809
    }
810
811
    public function testAttributes()
812
    {
813
        $form = $this->getStubForm();
814
        $form->setAttribute('foo', 'bar');
815
        $this->assertEquals('bar', $form->getAttribute('foo'));
816
        $attrs = $form->getAttributes();
817
        $this->assertArrayHasKey('foo', $attrs);
818
        $this->assertEquals('bar', $attrs['foo']);
819
    }
820
821
    /**
822
     * @skipUpgrade
823
     */
824
    public function testButtonClicked()
825
    {
826
        $form = $this->getStubForm();
827
        $action = $form->getRequestHandler()->buttonClicked();
828
        $this->assertNull($action);
829
830
        $controller = new FormTest\TestController();
831
        $form = $controller->Form();
832
        $request = new HTTPRequest(
833
            'POST',
834
            'FormTest_Controller/Form',
835
            [],
836
            [
837
            'Email' => '[email protected]',
838
            'SomeRequiredField' => 1,
839
            'action_doSubmit' => 1
840
            ]
841
        );
842
        $request->setSession(new Session([]));
843
844
        $form->getRequestHandler()->httpSubmission($request);
845
        $button = $form->getRequestHandler()->buttonClicked();
846
        $this->assertInstanceOf(FormAction::class, $button);
847
        $this->assertEquals('doSubmit', $button->actionName());
848
        $form = new Form(
849
            $controller,
850
            'Form',
851
            new FieldList(new FormAction('doSubmit', 'Inline action')),
852
            new FieldList()
853
        );
854
        $form->disableSecurityToken();
855
        $request = new HTTPRequest(
856
            'POST',
857
            'FormTest_Controller/Form',
858
            [],
859
            [
860
            'action_doSubmit' => 1
861
            ]
862
        );
863
        $request->setSession(new Session([]));
864
865
        $form->getRequestHandler()->httpSubmission($request);
866
        $button = $form->getRequestHandler()->buttonClicked();
867
        $this->assertInstanceOf(FormAction::class, $button);
868
        $this->assertEquals('doSubmit', $button->actionName());
869
    }
870
871
    public function testCheckAccessAction()
872
    {
873
        $controller = new FormTest\TestController();
874
        $form = new Form(
875
            $controller,
876
            'Form',
877
            new FieldList(),
878
            new FieldList(new FormAction('actionName', 'Action'))
879
        );
880
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName'));
881
882
        $form = new Form(
883
            $controller,
884
            'Form',
885
            new FieldList(new FormAction('inlineAction', 'Inline action')),
886
            new FieldList()
887
        );
888
        $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction'));
889
    }
890
891
    public function testAttributesHTML()
892
    {
893
        $form = $this->getStubForm();
894
895
        $form->setAttribute('foo', 'bar');
896
        $this->assertContains('foo="bar"', $form->getAttributesHTML());
897
898
        $form->setAttribute('foo', null);
899
        $this->assertNotContains('foo="bar"', $form->getAttributesHTML());
900
901
        $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

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