Completed
Push — master ( 5c98d3...0a7e4c )
by Loz
11:47
created

FormTest::testButtonClicked()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 34
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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