Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

FormTest::testAddExtraClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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