Completed
Push — master ( 098f19...84193b )
by Hamish
11:36
created

FormTest::testFieldMessageEscapeHtml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 9.0856
cc 1
eloc 19
nc 1
nop 0
1
<?php
2
3
/**
4
 * @package framework
5
 * @subpackage tests
6
 */
7
class FormTest extends FunctionalTest {
8
9
	protected static $fixture_file = 'FormTest.yml';
10
11
	protected $extraDataObjects = array(
12
		'FormTest_Player',
13
		'FormTest_Team',
14
	);
15
16
	public function setUp() {
17
		parent::setUp();
18
19
		Config::inst()->update('Director', 'rules', array(
20
			'FormTest_Controller' => 'FormTest_Controller'
21
		));
22
23
		// Suppress themes
24
		Config::inst()->remove('SSViewer', 'theme');
25
	}
26
27
	public function testLoadDataFromRequest() {
28
		$form = new Form(
29
			new Controller(),
30
			'Form',
31
			new FieldList(
32
				new TextField('key1'),
33
				new TextField('namespace[key2]'),
34
				new TextField('namespace[key3][key4]'),
35
				new TextField('othernamespace[key5][key6][key7]')
36
			),
37
			new FieldList()
38
		);
39
40
		// url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7
0 ignored issues
show
Unused Code Comprehensibility introduced by
44% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
41
		$requestData = array(
42
			'key1' => 'val1',
43
			'namespace' => array(
44
				'key2' => 'val2',
45
				'key3' => array(
46
					'key4' => 'val4',
47
				)
48
			),
49
			'othernamespace' => array(
50
				'key5' => array(
51
					'key6' =>array(
52
						'key7' => 'val7'
53
					)
54
				)
55
			)
56
		);
57
58
		$form->loadDataFrom($requestData);
59
60
		$fields = $form->Fields();
61
		$this->assertEquals($fields->fieldByName('key1')->Value(), 'val1');
62
		$this->assertEquals($fields->fieldByName('namespace[key2]')->Value(), 'val2');
63
		$this->assertEquals($fields->fieldByName('namespace[key3][key4]')->Value(), 'val4');
64
		$this->assertEquals($fields->fieldByName('othernamespace[key5][key6][key7]')->Value(), 'val7');
65
	}
66
67
	public function testLoadDataFromUnchangedHandling() {
68
		$form = new Form(
69
			new Controller(),
70
			'Form',
71
			new FieldList(
72
				new TextField('key1'),
73
				new TextField('key2')
74
			),
75
			new FieldList()
76
		);
77
		$form->loadDataFrom(array(
78
			'key1' => 'save',
79
			'key2' => 'dontsave',
80
			'key2_unchanged' => '1'
81
		));
82
		$this->assertEquals(
83
			$form->getData(),
84
			array(
85
				'key1' => 'save',
86
				'key2' => null,
87
			),
88
			'loadDataFrom() doesnt save a field if a matching "<fieldname>_unchanged" flag is set'
89
		);
90
	}
91
92
	public function testLoadDataFromObject() {
93
		$form = new Form(
94
		new Controller(),
95
			'Form',
96
			new FieldList(
97
				new HeaderField('MyPlayerHeader','My Player'),
98
				new TextField('Name'), // appears in both Player and Team
99
				new TextareaField('Biography'),
100
				new DateField('Birthday'),
101
				new NumericField('BirthdayYear') // dynamic property
102
			),
103
			new FieldList()
104
		);
105
106
		$captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails');
107
		$form->loadDataFrom($captainWithDetails);
0 ignored issues
show
Bug introduced by
It seems like $captainWithDetails defined by $this->objFromFixture('F..., 'captainWithDetails') on line 106 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
108
		$this->assertEquals(
109
			$form->getData(),
110
			array(
111
				'Name' => 'Captain Details',
112
				'Biography' => 'Bio 1',
113
				'Birthday' => '1982-01-01',
114
				'BirthdayYear' => '1982',
115
			),
116
			'LoadDataFrom() loads simple fields and dynamic getters'
117
		);
118
119
		$captainNoDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails');
120
		$form->loadDataFrom($captainNoDetails);
0 ignored issues
show
Bug introduced by
It seems like $captainNoDetails defined by $this->objFromFixture('F...r', 'captainNoDetails') on line 119 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
121
		$this->assertEquals(
122
			$form->getData(),
123
			array(
124
				'Name' => 'Captain No Details',
125
				'Biography' => null,
126
				'Birthday' => null,
127
				'BirthdayYear' => 0,
128
			),
129
			'LoadNonBlankDataFrom() loads only fields with values, and doesnt overwrite existing values'
130
		);
131
	}
132
133
	public function testLoadDataFromClearMissingFields() {
134
		$form = new Form(
135
			new Controller(),
136
			'Form',
137
			new FieldList(
138
				new HeaderField('MyPlayerHeader','My Player'),
139
				new TextField('Name'), // appears in both Player and Team
140
				new TextareaField('Biography'),
141
				new DateField('Birthday'),
142
				new NumericField('BirthdayYear'), // dynamic property
143
				$unrelatedField = new TextField('UnrelatedFormField')
144
				//new CheckboxSetField('Teams') // relation editing
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
145
			),
146
			new FieldList()
147
		);
148
		$unrelatedField->setValue("random value");
149
150
		$captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails');
151
		$captainNoDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails');
152
		$form->loadDataFrom($captainWithDetails);
0 ignored issues
show
Bug introduced by
It seems like $captainWithDetails defined by $this->objFromFixture('F..., 'captainWithDetails') on line 150 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
153
		$this->assertEquals(
154
			$form->getData(),
155
			array(
156
				'Name' => 'Captain Details',
157
				'Biography' => 'Bio 1',
158
				'Birthday' => '1982-01-01',
159
				'BirthdayYear' => '1982',
160
				'UnrelatedFormField' => 'random value',
161
			),
162
			'LoadDataFrom() doesnt overwrite fields not found in the object'
163
		);
164
165
		$captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails');
166
		$team2 = $this->objFromFixture('FormTest_Team', 'team2');
167
		$form->loadDataFrom($captainWithDetails);
0 ignored issues
show
Bug introduced by
It seems like $captainWithDetails defined by $this->objFromFixture('F...r', 'captainNoDetails') on line 165 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
168
		$form->loadDataFrom($team2, Form::MERGE_CLEAR_MISSING);
0 ignored issues
show
Bug introduced by
It seems like $team2 defined by $this->objFromFixture('FormTest_Team', 'team2') on line 166 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
169
		$this->assertEquals(
170
			$form->getData(),
171
			array(
172
				'Name' => 'Team 2',
173
				'Biography' => '',
174
				'Birthday' => '',
175
				'BirthdayYear' => 0,
176
				'UnrelatedFormField' => null,
177
			),
178
			'LoadDataFrom() overwrites fields not found in the object with $clearMissingFields=true'
179
		);
180
	}
181
182
	public function testLookupFieldDisabledSaving() {
183
		$object = new DataObjectTest_Team();
184
		$form = new Form(
185
			new Controller(),
186
			'Form',
187
			new FieldList(
188
				new LookupField('Players', 'Players')
189
			),
190
			new FieldList()
191
		);
192
		$form->loadDataFrom(array(
193
			'Players' => array(
194
				14,
195
				18,
196
				22
197
			),
198
		));
199
		$form->saveInto($object);
200
		$playersIds = $object->Players()->getIDList();
201
202
		$this->assertTrue($form->validate());
203
		$this->assertEquals(
204
			$playersIds,
205
			array(),
206
			'saveInto() should not save into the DataObject for the LookupField'
207
		);
208
	}
209
210
	public function testLoadDataFromIgnoreFalseish() {
211
		$form = new Form(
212
			new Controller(),
213
			'Form',
214
			new FieldList(
215
				new TextField('Biography', 'Biography', 'Custom Default')
216
			),
217
			new FieldList()
218
		);
219
220
		$captainNoDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails');
221
		$captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails');
222
223
		$form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH);
0 ignored issues
show
Bug introduced by
It seems like $captainNoDetails defined by $this->objFromFixture('F...r', 'captainNoDetails') on line 220 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
224
		$this->assertEquals(
225
			$form->getData(),
226
			array('Biography' => 'Custom Default'),
227
			'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish'
228
		);
229
230
		$form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH);
0 ignored issues
show
Bug introduced by
It seems like $captainWithDetails defined by $this->objFromFixture('F..., 'captainWithDetails') on line 221 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
231
		$this->assertEquals(
232
			$form->getData(),
233
			array('Biography' => 'Bio 1'),
234
			'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish'
235
		);
236
	}
237
238
	public function testFormMethodOverride() {
239
		$form = $this->getStubForm();
240
		$form->setFormMethod('GET');
241
		$this->assertNull($form->Fields()->dataFieldByName('_method'));
242
243
		$form = $this->getStubForm();
244
		$form->setFormMethod('PUT');
245
		$this->assertEquals($form->Fields()->dataFieldByName('_method')->Value(), 'PUT',
246
			'PUT override in forms has PUT in hiddenfield'
247
		);
248
		$this->assertEquals($form->FormMethod(), 'POST',
249
			'PUT override in forms has POST in <form> tag'
250
		);
251
252
		$form = $this->getStubForm();
253
		$form->setFormMethod('DELETE');
254
		$this->assertEquals($form->Fields()->dataFieldByName('_method')->Value(), 'DELETE',
255
			'PUT override in forms has PUT in hiddenfield'
256
		);
257
		$this->assertEquals($form->FormMethod(), 'POST',
258
			'PUT override in forms has POST in <form> tag'
259
		);
260
	}
261
262
	public function testValidationExemptActions() {
263
		$response = $this->get('FormTest_Controller');
264
265
		$response = $this->submitForm(
266
			'Form_Form',
267
			'action_doSubmit',
268
			array(
269
				'Email' => '[email protected]'
270
			)
271
		);
272
273
		// Firstly, assert that required fields still work when not using an exempt action
274
		$this->assertPartialMatchBySelector(
275
			'#Form_Form_SomeRequiredField_Holder .required',
276
			array('"Some Required Field" is required'),
277
			'Required fields show a notification on field when left blank'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Required fields show a ... field when left blank'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
278
		);
279
280
		// Re-submit the form using validation-exempt button
281
		$response = $this->submitForm(
282
			'Form_Form',
283
			'action_doSubmitValidationExempt',
284
			array(
285
				'Email' => '[email protected]'
286
			)
287
		);
288
289
		// The required message should be empty if validation was skipped
290
		$items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
291
		$this->assertEmpty($items);
292
293
		// And the session message should show up is submitted successfully
294
		$this->assertPartialMatchBySelector(
295
			'#Form_Form_error',
296
			array(
297
				'Validation skipped'
298
			),
299
			'Form->sessionMessage() shows up after reloading the form'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Form->sessionMessage() ...ter reloading the form'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
300
		);
301
302
		// Test this same behaviour, but with a form-action exempted via instance
303
		$response = $this->submitForm(
304
			'Form_Form',
305
			'action_doSubmitActionExempt',
306
			array(
307
				'Email' => '[email protected]'
308
			)
309
		);
310
311
		// The required message should be empty if validation was skipped
312
		$items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required');
313
		$this->assertEmpty($items);
314
315
		// And the session message should show up is submitted successfully
316
		$this->assertPartialMatchBySelector(
317
			'#Form_Form_error',
318
			array(
319
				'Validation bypassed!'
320
			),
321
			'Form->sessionMessage() shows up after reloading the form'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Form->sessionMessage() ...ter reloading the form'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
322
		);
323
	}
324
325
	public function testSessionValidationMessage() {
326
		$this->get('FormTest_Controller');
327
328
		$response = $this->post(
329
			'FormTest_Controller/Form',
330
			array(
331
				'Email' => 'invalid',
332
				'Number' => '<a href="http://mysite.com">link</a>' // XSS attempt
333
				// leaving out "Required" field
334
			)
335
		);
336
337
		$this->assertPartialMatchBySelector(
338
			'#Form_Form_Email_Holder span.message',
339
			array(
340
				'Please enter an email address'
341
			),
342
			'Formfield validation shows note on field if invalid'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Formfield validation sh...te on field if invalid'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
343
		);
344
		$this->assertPartialMatchBySelector(
345
			'#Form_Form_SomeRequiredField_Holder span.required',
346
			array(
347
				'"Some Required Field" is required'
348
			),
349
			'Required fields show a notification on field when left blank'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Required fields show a ... field when left blank'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
350
		);
351
352
		$this->assertContains(
353
			'&#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',
354
			$response->getBody(),
355
			"Validation messages are safely XML encoded"
356
		);
357
		$this->assertNotContains(
358
			'<a href="http://mysite.com">link</a>',
359
			$response->getBody(),
360
			"Unsafe content is not emitted directly inside the response body"
361
		);
362
	}
363
364
	public function testSessionSuccessMessage() {
365
		$this->get('FormTest_Controller');
366
367
		$response = $this->post(
368
			'FormTest_Controller/Form',
369
			array(
370
				'Email' => '[email protected]',
371
				'SomeRequiredField' => 'test',
372
			)
373
		);
374
		$this->assertPartialMatchBySelector(
375
			'#Form_Form_error',
376
			array(
377
				'Test save was successful'
378
			),
379
			'Form->sessionMessage() shows up after reloading the form'
0 ignored issues
show
Unused Code introduced by
The call to FormTest::assertPartialMatchBySelector() has too many arguments starting with 'Form->sessionMessage() ...ter reloading the form'.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
380
		);
381
	}
382
383
	public function testGloballyDisabledSecurityTokenInheritsToNewForm() {
384
		SecurityToken::enable();
385
386
		$form1 = $this->getStubForm();
387
		$this->assertInstanceOf('SecurityToken', $form1->getSecurityToken());
388
389
		SecurityToken::disable();
390
391
		$form2 = $this->getStubForm();
392
		$this->assertInstanceOf('NullSecurityToken', $form2->getSecurityToken());
393
394
		SecurityToken::enable();
395
	}
396
397
	public function testDisableSecurityTokenDoesntAddTokenFormField() {
398
		SecurityToken::enable();
399
400
		$formWithToken = $this->getStubForm();
401
		$this->assertInstanceOf(
402
			'HiddenField',
403
			$formWithToken->Fields()->fieldByName(SecurityToken::get_default_name()),
404
			'Token field added by default'
405
		);
406
407
		$formWithoutToken = $this->getStubForm();
408
		$formWithoutToken->disableSecurityToken();
409
		$this->assertNull(
410
			$formWithoutToken->Fields()->fieldByName(SecurityToken::get_default_name()),
411
			'Token field not added if disableSecurityToken() is set'
412
		);
413
	}
414
415
	public function testDisableSecurityTokenAcceptsSubmissionWithoutToken() {
416
		SecurityToken::enable();
417
		$expectedToken = SecurityToken::inst()->getValue();
418
419
		$response = $this->get('FormTest_ControllerWithSecurityToken');
420
		// can't use submitForm() as it'll automatically insert SecurityID into the POST data
421
		$response = $this->post(
422
			'FormTest_ControllerWithSecurityToken/Form',
423
			array(
424
				'Email' => '[email protected]',
425
				'action_doSubmit' => 1
426
				// leaving out security token
427
			)
428
		);
429
		$this->assertEquals(400, $response->getStatusCode(), 'Submission fails without security token');
430
431
		// Generate a new token which doesn't match the current one
432
		$generator = new RandomGenerator();
433
		$invalidToken = $generator->randomToken('sha1');
434
		$this->assertNotEquals($invalidToken, $expectedToken);
435
436
		// Test token with request
437
		$response = $this->get('FormTest_ControllerWithSecurityToken');
438
		$response = $this->post(
439
			'FormTest_ControllerWithSecurityToken/Form',
440
			array(
441
				'Email' => '[email protected]',
442
				'action_doSubmit' => 1,
443
				'SecurityID' => $invalidToken
444
			)
445
		);
446
		$this->assertEquals(200, $response->getStatusCode(), 'Submission reloads form if security token invalid');
447
		$this->assertTrue(
448
			stripos($response->getBody(), 'name="SecurityID" value="'.$expectedToken.'"') !== false,
449
			'Submission reloads with correct security token after failure'
450
		);
451
		$this->assertTrue(
452
			stripos($response->getBody(), 'name="SecurityID" value="'.$invalidToken.'"') === false,
453
			'Submission reloads without incorrect security token after failure'
454
		);
455
456
		$matched = $this->cssParser()->getBySelector('#Form_Form_Email');
457
		$attrs = $matched[0]->attributes();
458
		$this->assertEquals('[email protected]', (string)$attrs['value'], 'Submitted data is preserved');
459
460
		$response = $this->get('FormTest_ControllerWithSecurityToken');
461
		$tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
462
		$this->assertEquals(
463
			1,
464
			count($tokenEls),
465
			'Token form field added for controller without disableSecurityToken()'
466
		);
467
		$token = (string)$tokenEls[0];
468
		$response = $this->submitForm(
469
			'Form_Form',
470
			null,
471
			array(
472
				'Email' => '[email protected]',
473
				'SecurityID' => $token
474
			)
475
		);
476
		$this->assertEquals(200, $response->getStatusCode(), 'Submission suceeds with security token');
477
	}
478
479
	public function testStrictFormMethodChecking() {
480
		$response = $this->get('FormTest_ControllerWithStrictPostCheck');
481
		$response = $this->get(
482
			'FormTest_ControllerWithStrictPostCheck/Form/[email protected]&action_doSubmit=1'
483
		);
484
		$this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method');
485
486
		$response = $this->get('FormTest_ControllerWithStrictPostCheck');
487
		$response = $this->post(
488
			'FormTest_ControllerWithStrictPostCheck/Form',
489
			array(
490
				'Email' => '[email protected]',
491
				'action_doSubmit' => 1
492
			)
493
		);
494
		$this->assertEquals(200, $response->getStatusCode(), 'Submission succeeds with correct method');
495
	}
496
497
	public function testEnableSecurityToken() {
498
		SecurityToken::disable();
499
		$form = $this->getStubForm();
500
		$this->assertFalse($form->getSecurityToken()->isEnabled());
501
		$form->enableSecurityToken();
502
		$this->assertTrue($form->getSecurityToken()->isEnabled());
503
504
		SecurityToken::disable(); // restore original
505
	}
506
507
	public function testDisableSecurityToken() {
508
		SecurityToken::enable();
509
		$form = $this->getStubForm();
510
		$this->assertTrue($form->getSecurityToken()->isEnabled());
511
		$form->disableSecurityToken();
512
		$this->assertFalse($form->getSecurityToken()->isEnabled());
513
514
		SecurityToken::disable(); // restore original
515
	}
516
517
	public function testEncType() {
518
		$form = $this->getStubForm();
519
		$this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
520
521
		$form->setEncType(Form::ENC_TYPE_MULTIPART);
522
		$this->assertEquals('multipart/form-data', $form->getEncType());
523
524
		$form = $this->getStubForm();
525
		$form->Fields()->push(new FileField(null));
526
		$this->assertEquals('multipart/form-data', $form->getEncType());
527
528
		$form->setEncType(Form::ENC_TYPE_URLENCODED);
529
		$this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
530
	}
531
532
	public function testAddExtraClass() {
533
		$form = $this->getStubForm();
534
		$form->addExtraClass('class1');
535
		$form->addExtraClass('class2');
536
		$this->assertStringEndsWith('class1 class2', $form->extraClass());
537
	}
538
539
	public function testRemoveExtraClass() {
540
		$form = $this->getStubForm();
541
		$form->addExtraClass('class1');
542
		$form->addExtraClass('class2');
543
		$this->assertStringEndsWith('class1 class2', $form->extraClass());
544
		$form->removeExtraClass('class1');
545
		$this->assertStringEndsWith('class2', $form->extraClass());
546
	}
547
548
	public function testAddManyExtraClasses() {
549
		$form = $this->getStubForm();
550
		//test we can split by a range of spaces and tabs
551
		$form->addExtraClass('class1 class2     class3	class4		class5');
552
		$this->assertStringEndsWith(
553
			'class1 class2 class3 class4 class5',
554
			$form->extraClass()
555
		);
556
		//test that duplicate classes don't get added
557
		$form->addExtraClass('class1 class2');
558
		$this->assertStringEndsWith(
559
			'class1 class2 class3 class4 class5',
560
			$form->extraClass()
561
		);
562
	}
563
564
	public function testRemoveManyExtraClasses() {
565
		$form = $this->getStubForm();
566
		$form->addExtraClass('class1 class2     class3	class4		class5');
567
		//test we can remove a single class we just added
568
		$form->removeExtraClass('class3');
569
		$this->assertStringEndsWith(
570
			'class1 class2 class4 class5',
571
			$form->extraClass()
572
		);
573
		//check we can remove many classes at once
574
		$form->removeExtraClass('class1 class5');
575
		$this->assertStringEndsWith(
576
			'class2 class4',
577
			$form->extraClass()
578
		);
579
		//check that removing a dud class is fine
580
		$form->removeExtraClass('dudClass');
581
		$this->assertStringEndsWith(
582
			'class2 class4',
583
			$form->extraClass()
584
		);
585
	}
586
587
	public function testDefaultClasses() {
588
		Config::nest();
589
590
		Config::inst()->update('Form', 'default_classes', array(
591
			'class1',
592
		));
593
594
		$form = $this->getStubForm();
595
596
		$this->assertContains('class1', $form->extraClass(), 'Class list does not contain expected class');
597
598
		Config::inst()->update('Form', 'default_classes', array(
599
			'class1',
600
			'class2',
601
		));
602
603
		$form = $this->getStubForm();
604
605
		$this->assertContains('class1 class2', $form->extraClass(), 'Class list does not contain expected class');
606
607
		Config::inst()->update('Form', 'default_classes', array(
608
			'class3',
609
		));
610
611
		$form = $this->getStubForm();
612
613
		$this->assertContains('class3', $form->extraClass(), 'Class list does not contain expected class');
614
615
		$form->removeExtraClass('class3');
616
617
		$this->assertNotContains('class3', $form->extraClass(), 'Class list contains unexpected class');
618
619
		Config::unnest();
620
	}
621
622
	public function testAttributes() {
623
		$form = $this->getStubForm();
624
		$form->setAttribute('foo', 'bar');
625
		$this->assertEquals('bar', $form->getAttribute('foo'));
626
		$attrs = $form->getAttributes();
627
		$this->assertArrayHasKey('foo', $attrs);
628
		$this->assertEquals('bar', $attrs['foo']);
629
	}
630
631
	public function testAttributesHTML() {
632
		$form = $this->getStubForm();
633
634
		$form->setAttribute('foo', 'bar');
635
		$this->assertContains('foo="bar"', $form->getAttributesHTML());
636
637
		$form->setAttribute('foo', null);
638
		$this->assertNotContains('foo="bar"', $form->getAttributesHTML());
639
640
		$form->setAttribute('foo', true);
641
		$this->assertContains('foo="foo"', $form->getAttributesHTML());
642
643
		$form->setAttribute('one', 1);
644
		$form->setAttribute('two', 2);
645
		$form->setAttribute('three', 3);
646
		$this->assertNotContains('one="1"', $form->getAttributesHTML('one', 'two'));
647
		$this->assertNotContains('two="2"', $form->getAttributesHTML('one', 'two'));
648
		$this->assertContains('three="3"', $form->getAttributesHTML('one', 'two'));
649
	}
650
651
	function testMessageEscapeHtml() {
652
		$form = $this->getStubForm();
653
		$form->Controller()->handleRequest(new SS_HTTPRequest('GET', '/'), DataModel::inst()); // stub out request
0 ignored issues
show
Deprecated Code introduced by
The method Form::Controller() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
654
		$form->sessionMessage('<em>Escaped HTML</em>', 'good', true);
655
		$parser = new CSSContentParser($form->forTemplate());
656
		$messageEls = $parser->getBySelector('.message');
657
		$this->assertContains(
658
			'&lt;em&gt;Escaped HTML&lt;/em&gt;',
659
			$messageEls[0]->asXML()
660
		);
661
662
		$form = $this->getStubForm();
663
		$form->Controller()->handleRequest(new SS_HTTPRequest('GET', '/'), DataModel::inst()); // stub out request
0 ignored issues
show
Deprecated Code introduced by
The method Form::Controller() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
664
		$form->sessionMessage('<em>Unescaped HTML</em>', 'good', false);
665
		$parser = new CSSContentParser($form->forTemplate());
666
		$messageEls = $parser->getBySelector('.message');
667
		$this->assertContains(
668
			'<em>Unescaped HTML</em>',
669
			$messageEls[0]->asXML()
670
		);
671
	}
672
673
	function testFieldMessageEscapeHtml() {
674
		$form = $this->getStubForm();
675
		$form->Controller()->handleRequest(new SS_HTTPRequest('GET', '/'), DataModel::inst()); // stub out request
0 ignored issues
show
Deprecated Code introduced by
The method Form::Controller() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
676
		$form->addErrorMessage('key1', '<em>Escaped HTML</em>', 'good', true);
677
		$form->setupFormErrors();
678
		$parser = new CSSContentParser($result = $form->forTemplate());
679
		$messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
680
		$this->assertContains(
681
			'&lt;em&gt;Escaped HTML&lt;/em&gt;',
682
			$messageEls[0]->asXML()
683
		);
684
685
		$form = $this->getStubForm();
686
		$form->Controller()->handleRequest(new SS_HTTPRequest('GET', '/'), DataModel::inst()); // stub out request
0 ignored issues
show
Deprecated Code introduced by
The method Form::Controller() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
687
		$form->addErrorMessage('key1', '<em>Unescaped HTML</em>', 'good', false);
688
		$form->setupFormErrors();
689
		$parser = new CSSContentParser($form->forTemplate());
690
		$messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
691
		$this->assertContains(
692
			'<em>Unescaped HTML</em>',
693
			$messageEls[0]->asXML()
694
		);
695
	}
696
697
    public function testGetExtraFields()
698
    {
699
        $form = new FormTest_ExtraFieldsForm(
700
            new FormTest_Controller(),
701
            'Form',
702
            new FieldList(new TextField('key1')),
703
            new FieldList()
704
        );
705
706
        $data = array(
707
            'key1' => 'test',
708
            'ExtraFieldCheckbox' => false,
709
        );
710
711
        $form->loadDataFrom($data);
712
713
        $formData = $form->getData();
714
        $this->assertEmpty($formData['ExtraFieldCheckbox']);
715
    }
716
717
	protected function getStubForm() {
718
		return new Form(
719
			new FormTest_Controller(),
720
			'Form',
721
			new FieldList(new TextField('key1')),
722
			new FieldList()
723
		);
724
	}
725
726
}
727
728
/**
729
 * @package framework
730
 * @subpackage tests
731
 */
732
class FormTest_Player extends DataObject implements TestOnly {
733
	private static $db = array(
734
		'Name' => 'Varchar',
735
		'Biography' => 'Text',
736
		'Birthday' => 'Date'
737
	);
738
739
	private static $belongs_many_many = array(
740
		'Teams' => 'FormTest_Team'
741
	);
742
743
	private static $has_one = array(
744
		'FavouriteTeam' => 'FormTest_Team',
745
	);
746
747
	public function getBirthdayYear() {
748
		return ($this->Birthday) ? date('Y', strtotime($this->Birthday)) : null;
749
	}
750
751
}
752
753
/**
754
 * @package framework
755
 * @subpackage tests
756
 */
757
class FormTest_Team extends DataObject implements TestOnly {
758
	private static $db = array(
759
		'Name' => 'Varchar',
760
		'Region' => 'Varchar',
761
	);
762
763
	private static $many_many = array(
764
		'Players' => 'FormTest_Player'
765
	);
766
}
767
768
/**
769
 * @package framework
770
 * @subpackage tests
771
 */
772
class FormTest_Controller extends Controller implements TestOnly {
773
774
	private static $allowed_actions = array('Form');
775
776
	private static $url_handlers = array(
777
		'$Action//$ID/$OtherID' => "handleAction",
778
	);
779
780
	protected $template = 'BlankPage';
781
782
	public function Link($action = null) {
783
		return Controller::join_links('FormTest_Controller', $this->getRequest()->latestParam('Action'),
784
			$this->getRequest()->latestParam('ID'), $action);
785
	}
786
787
	public function Form() {
788
		$form = new Form(
789
			$this,
790
			'Form',
791
			new FieldList(
792
				new EmailField('Email'),
793
				new TextField('SomeRequiredField'),
794
				new CheckboxSetField('Boxes', null, array('1'=>'one','2'=>'two')),
795
				new NumericField('Number')
796
			),
797
			new FieldList(
798
				FormAction::create('doSubmit'),
799
				FormAction::create('doSubmitValidationExempt'),
800
				FormAction::create('doSubmitActionExempt')
801
					->setValidationExempt(true)
802
			),
803
			new RequiredFields(
804
				'Email',
805
				'SomeRequiredField'
806
			)
807
		);
808
		$form->setValidationExemptActions(array('doSubmitValidationExempt'));
809
		$form->disableSecurityToken(); // Disable CSRF protection for easier form submission handling
810
811
		return $form;
812
	}
813
814
	public function doSubmit($data, $form, $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
815
		$form->sessionMessage('Test save was successful', 'good');
816
		return $this->redirectBack();
817
	}
818
819
	public function doSubmitValidationExempt($data, $form, $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
820
		$form->sessionMessage('Validation skipped', 'good');
821
		return $this->redirectBack();
822
	}
823
824
	public function doSubmitActionExempt($data, $form, $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
825
		$form->sessionMessage('Validation bypassed!', 'good');
826
		return $this->redirectBack();
827
	}
828
829
	public function getViewer($action = null) {
830
		return new SSViewer('BlankPage');
831
	}
832
833
}
834
835
/**
836
 * @package framework
837
 * @subpackage tests
838
 */
839
class FormTest_ControllerWithSecurityToken extends Controller implements TestOnly {
840
841
	private static $allowed_actions = array('Form');
842
843
	private static $url_handlers = array(
844
		'$Action//$ID/$OtherID' => "handleAction",
845
	);
846
847
	protected $template = 'BlankPage';
848
849
	public function Link($action = null) {
850
		return Controller::join_links('FormTest_ControllerWithSecurityToken', $this->getRequest()->latestParam('Action'),
851
			$this->getRequest()->latestParam('ID'), $action);
852
	}
853
854
	public function Form() {
855
		$form = new Form(
856
			$this,
857
			'Form',
858
			new FieldList(
859
				new EmailField('Email')
860
			),
861
			new FieldList(
862
				new FormAction('doSubmit')
863
			)
864
		);
865
866
		return $form;
867
	}
868
869
	public function doSubmit($data, $form, $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
870
		$form->sessionMessage('Test save was successful', 'good');
871
		return $this->redirectBack();
872
	}
873
874
}
875
876
class FormTest_ControllerWithStrictPostCheck extends Controller implements TestOnly
877
{
878
879
    private static $allowed_actions = array('Form');
880
881
    protected $template = 'BlankPage';
882
883
    public function Link($action = null)
884
    {
885
        return Controller::join_links(
886
            'FormTest_ControllerWithStrictPostCheck',
887
            $this->request->latestParam('Action'),
888
            $this->request->latestParam('ID'),
889
            $action
890
        );
891
    }
892
893
    public function Form()
894
    {
895
        $form = new Form(
896
            $this,
897
            'Form',
898
            new FieldList(
899
                new EmailField('Email')
900
            ),
901
            new FieldList(
902
                new FormAction('doSubmit')
903
            )
904
        );
905
        $form->setFormMethod('POST');
906
        $form->setStrictFormMethodCheck(true);
907
        $form->disableSecurityToken(); // Disable CSRF protection for easier form submission handling
908
909
        return $form;
910
    }
911
912
    public function doSubmit($data, $form, $request)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
913
    {
914
        $form->sessionMessage('Test save was successful', 'good');
915
        return $this->redirectBack();
916
    }
917
}
918
919
class FormTest_ExtraFieldsForm extends Form implements TestOnly {
920
921
    public function getExtraFields() {
922
        $fields = parent::getExtraFields();
923
924
        $fields->push(new CheckboxField('ExtraFieldCheckbox', 'Extra Field Checkbox', 1));
925
926
        return $fields;
927
    }
928
929
}
930