Completed
Push — member-groupset-delete ( a90a9a )
by Loz
11:22
created

FormTest::testSubmitReadonlyFields()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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