[ 'val' => 'Just a string', 'type' => ValidationResult::TYPE_ERROR, 'cast' => ValidationResult::CAST_TEXT, ], 'message2' => [ 'val' => 'This is a good message', 'type' => ValidationResult::TYPE_GOOD, 'cast' => ValidationResult::CAST_TEXT, ], ], [ 'message1' => [ 'val' => 'This is a good message', 'type' => ValidationResult::TYPE_GOOD, 'cast' => ValidationResult::CAST_TEXT, ], 'message2' => [ 'val' => 'HTML is the future of the web', 'type' => ValidationResult::TYPE_GOOD, 'cast' => ValidationResult::CAST_HTML, ], ], [ 'message1' => [ 'val' => 'This is a good message', 'type' => ValidationResult::TYPE_GOOD, 'cast' => ValidationResult::CAST_TEXT, ], 'message2' => [ 'val' => 'HTML is the future of the web', 'type' => ValidationResult::TYPE_GOOD, 'cast' => ValidationResult::CAST_HTML, ], 'force' => true, ], ]; } public function testLoadDataFromRequest() { $form = new Form( Controller::curr(), 'Form', new FieldList( new TextField('key1'), new TextField('namespace[key2]'), new TextField('namespace[key3][key4]'), new TextField('othernamespace[key5][key6][key7]'), new TextField('dot.field') ), new FieldList() ); // url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7 $requestData = [ 'key1' => 'val1', 'namespace' => [ 'key2' => 'val2', 'key3' => [ 'key4' => 'val4', ] ], 'othernamespace' => [ 'key5' => [ 'key6' =>[ 'key7' => 'val7' ] ] ], 'dot.field' => 'dot.field val' ]; $form->loadDataFrom($requestData); $fields = $form->Fields(); $this->assertEquals('val1', $fields->fieldByName('key1')->Value()); $this->assertEquals('val2', $fields->fieldByName('namespace[key2]')->Value()); $this->assertEquals('val4', $fields->fieldByName('namespace[key3][key4]')->Value()); $this->assertEquals('val7', $fields->fieldByName('othernamespace[key5][key6][key7]')->Value()); $this->assertEquals('dot.field val', $fields->fieldByName('dot.field')->Value()); } public function testSubmitReadonlyFields() { $this->get('FormTest_Controller'); // Submitting a value for a readonly field should be ignored $response = $this->post( 'FormTest_Controller/Form', [ 'Email' => 'invalid', 'Number' => '888', 'ReadonlyField' => '' // leaving out "Required" field ] ); // Number field updates its value $this->assertStringContainsString('getBody()); // Readonly field remains $this->assertStringContainsString( 'getBody() ); $this->assertStringNotContainsString('hacxzored', $response->getBody()); } public function testLoadDataFromUnchangedHandling() { $form = new Form( Controller::curr(), 'Form', new FieldList( new TextField('key1'), new TextField('key2') ), new FieldList() ); $form->loadDataFrom( [ 'key1' => 'save', 'key2' => 'dontsave', 'key2_unchanged' => '1' ] ); $this->assertEquals( $form->getData(), [ 'key1' => 'save', 'key2' => null, ], 'loadDataFrom() doesnt save a field if a matching "_unchanged" flag is set' ); } public function testLoadDataFromObject() { $form = new Form( Controller::curr(), 'Form', new FieldList( new HeaderField('MyPlayerHeader', 'My Player'), new TextField('Name'), // appears in both Player and Team new TextareaField('Biography'), new DateField('Birthday'), new NumericField('BirthdayYear'), // dynamic property new TextField('FavouriteTeam.Name') // dot syntax ), new FieldList() ); $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails'); $form->loadDataFrom($captainWithDetails); $this->assertEquals( $form->getData(), [ 'Name' => 'Captain Details', 'Biography' => 'Bio 1', 'Birthday' => '1982-01-01', 'BirthdayYear' => '1982', 'FavouriteTeam.Name' => 'Team 1', ], 'LoadDataFrom() loads simple fields and dynamic getters' ); $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails'); $form->loadDataFrom($captainNoDetails); $this->assertEquals( $form->getData(), [ 'Name' => 'Captain No Details', 'Biography' => null, 'Birthday' => null, 'BirthdayYear' => 0, 'FavouriteTeam.Name' => null, ], 'LoadNonBlankDataFrom() loads only fields with values, and doesnt overwrite existing values' ); } public function testLoadDataFromClearMissingFields() { $form = new Form( Controller::curr(), 'Form', new FieldList( new HeaderField('MyPlayerHeader', 'My Player'), new TextField('Name'), // appears in both Player and Team new TextareaField('Biography'), new DateField('Birthday'), new NumericField('BirthdayYear'), // dynamic property new TextField('FavouriteTeam.Name'), // dot syntax $unrelatedField = new TextField('UnrelatedFormField') //new CheckboxSetField('Teams') // relation editing ), new FieldList() ); $unrelatedField->setValue("random value"); $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails'); $form->loadDataFrom($captainWithDetails); $this->assertEquals( $form->getData(), [ 'Name' => 'Captain Details', 'Biography' => 'Bio 1', 'Birthday' => '1982-01-01', 'BirthdayYear' => '1982', 'FavouriteTeam.Name' => 'Team 1', 'UnrelatedFormField' => 'random value', ], 'LoadDataFrom() doesnt overwrite fields not found in the object' ); $captainWithDetails = $this->objFromFixture(Player::class, 'captainNoDetails'); $team2 = $this->objFromFixture(Team::class, 'team2'); $form->loadDataFrom($captainWithDetails); $form->loadDataFrom($team2, Form::MERGE_CLEAR_MISSING); $this->assertEquals( $form->getData(), [ 'Name' => 'Team 2', 'Biography' => '', 'Birthday' => '', 'BirthdayYear' => 0, 'FavouriteTeam.Name' => null, 'UnrelatedFormField' => null, ], 'LoadDataFrom() overwrites fields not found in the object with $clearMissingFields=true' ); } public function testLoadDataFromWithForceSetValueFlag() { // Get our data formatted in internal value and in submitted value // We're using very esoteric date and time format $dataInSubmittedValue = [ 'SomeDateTimeField' => 'Fri, Jun 15, \'18 17:28:05', 'SomeTimeField' => '05 o\'clock PM 28 05' ]; $dataInInternalValue = [ 'SomeDateTimeField' => '2018-06-15 17:28:05', 'SomeTimeField' => '17:28:05' ]; // Test loading our data with the MERGE_AS_INTERNAL_VALUE $form = $this->getStubFormWithWeirdValueFormat(); $form->loadDataFrom($dataInInternalValue, Form::MERGE_AS_INTERNAL_VALUE); $this->assertEquals( $dataInInternalValue, $form->getData() ); // Test loading our data with the MERGE_AS_SUBMITTED_VALUE and an data passed as an object $form = $this->getStubFormWithWeirdValueFormat(); $form->loadDataFrom(ArrayData::create($dataInSubmittedValue), Form::MERGE_AS_SUBMITTED_VALUE); $this->assertEquals( $dataInInternalValue, $form->getData() ); // Test loading our data without the MERGE_AS_INTERNAL_VALUE and without MERGE_AS_SUBMITTED_VALUE $form = $this->getStubFormWithWeirdValueFormat(); $form->loadDataFrom($dataInSubmittedValue); $this->assertEquals( $dataInInternalValue, $form->getData() ); } public function testLookupFieldDisabledSaving() { $object = new Team(); $form = new Form( Controller::curr(), 'Form', new FieldList( new LookupField('Players', 'Players') ), new FieldList() ); $form->loadDataFrom( [ 'Players' => [ 14, 18, 22 ], ] ); $form->saveInto($object); $playersIds = $object->Players()->getIDList(); $this->assertTrue($form->validationResult()->isValid()); $this->assertEquals( $playersIds, [], 'saveInto() should not save into the DataObject for the LookupField' ); } public function testDefaultAction() { $form = Form::create(Controller::curr(), 'Form', new FieldList(), new FieldList( new FormAction('doForm', 'Form Action') )); $this->assertNotNull($form->defaultAction()); $this->assertEquals('action_doForm', $form->defaultAction()->getName()); $form = Form::create(Controller::curr(), 'AnotherForm', new FieldList(), new FieldList( new CompositeField( new FormAction('doAnotherForm', 'Another Form Action') ) )); $this->assertNotNull($form->defaultAction()); $this->assertEquals('action_doAnotherForm', $form->defaultAction()->getName()); } public function testLoadDataFromIgnoreFalseish() { $form = new Form( Controller::curr(), 'Form', new FieldList( new TextField('Biography', 'Biography', 'Custom Default') ), new FieldList() ); $captainNoDetails = $this->objFromFixture(Player::class, 'captainNoDetails'); $captainWithDetails = $this->objFromFixture(Player::class, 'captainWithDetails'); $form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH); $this->assertEquals( $form->getData(), ['Biography' => 'Custom Default'], 'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish' ); $form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH); $this->assertEquals( $form->getData(), ['Biography' => 'Bio 1'], 'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish' ); } public function testFormMethodOverride() { $form = $this->getStubForm(); $form->setFormMethod('GET'); $this->assertNull($form->Fields()->dataFieldByName('_method')); $form = $this->getStubForm(); $form->setFormMethod('PUT'); $this->assertEquals( $form->Fields()->dataFieldByName('_method')->Value(), 'PUT', 'PUT override in forms has PUT in hiddenfield' ); $this->assertEquals( $form->FormMethod(), 'POST', 'PUT override in forms has POST in
tag' ); $form = $this->getStubForm(); $form->setFormMethod('DELETE'); $this->assertEquals( $form->Fields()->dataFieldByName('_method')->Value(), 'DELETE', 'PUT override in forms has PUT in hiddenfield' ); $this->assertEquals( $form->FormMethod(), 'POST', 'PUT override in forms has POST in tag' ); } public function testValidationExemptActions() { $this->get('FormTest_Controller'); $this->submitForm( 'Form_Form', 'action_doSubmit', [ 'Email' => 'test@test.com' ] ); // Firstly, assert that required fields still work when not using an exempt action $this->assertPartialMatchBySelector( '#Form_Form_SomeRequiredField_Holder .required', ['"Some required field" is required'], 'Required fields show a notification on field when left blank' ); // Re-submit the form using validation-exempt button $this->submitForm( 'Form_Form', 'action_doSubmitValidationExempt', [ 'Email' => 'test@test.com' ] ); // The required message should be empty if validation was skipped $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required'); $this->assertEmpty($items); // And the session message should show up is submitted successfully $this->assertPartialMatchBySelector( '#Form_Form_error', [ 'Validation skipped' ], 'Form->sessionMessage() shows up after reloading the form' ); // Test this same behaviour, but with a form-action exempted via instance $this->submitForm( 'Form_Form', 'action_doSubmitActionExempt', [ 'Email' => 'test@test.com' ] ); // The required message should be empty if validation was skipped $items = $this->cssParser()->getBySelector('#Form_Form_SomeRequiredField_Holder .required'); $this->assertEmpty($items); // And the session message should show up is submitted successfully $this->assertPartialMatchBySelector( '#Form_Form_error', [ 'Validation bypassed!' ], 'Form->sessionMessage() shows up after reloading the form' ); } public function testSessionValidationMessage() { $this->get('FormTest_Controller'); $response = $this->post( 'FormTest_Controller/Form', [ 'Email' => 'invalid', 'Number' => 'link' // XSS attempt // leaving out "Required" field ] ); $this->assertPartialMatchBySelector( '#Form_Form_Email_Holder span.message', [ 'Please enter an email address' ], 'Formfield validation shows note on field if invalid' ); $this->assertPartialMatchBySelector( '#Form_Form_SomeRequiredField_Holder span.required', [ '"Some required field" is required' ], 'Required fields show a notification on field when left blank' ); $this->assertStringContainsString( ''<a href="http://mysite.com">link</a>' is not a number, only numbers can be accepted for this field', $response->getBody(), "Validation messages are safely XML encoded" ); $this->assertStringNotContainsString( 'link', $response->getBody(), "Unsafe content is not emitted directly inside the response body" ); } public function testSessionSuccessMessage() { $this->get('FormTest_Controller'); $this->post( 'FormTest_Controller/Form', [ 'Email' => 'test@test.com', 'SomeRequiredField' => 'test', ] ); $this->assertPartialMatchBySelector( '#Form_Form_error', [ 'Test save was successful' ], 'Form->sessionMessage() shows up after reloading the form' ); } public function testValidationException() { $this->get('FormTest_Controller'); $this->post( 'FormTest_Controller/Form', [ 'Email' => 'test@test.com', 'SomeRequiredField' => 'test', 'action_doTriggerException' => 1, ] ); $this->assertPartialMatchBySelector( '#Form_Form_Email_Holder span.message', [ 'Error on Email field' ], 'Formfield validation shows note on field if invalid' ); $this->assertPartialMatchBySelector( '#Form_Form_error', [ 'Error at top of form' ], 'Required fields show a notification on field when left blank' ); } public function testGloballyDisabledSecurityTokenInheritsToNewForm() { SecurityToken::enable(); $form1 = $this->getStubForm(); $this->assertInstanceOf(SecurityToken::class, $form1->getSecurityToken()); SecurityToken::disable(); $form2 = $this->getStubForm(); $this->assertInstanceOf(NullSecurityToken::class, $form2->getSecurityToken()); SecurityToken::enable(); } public function testDisableSecurityTokenDoesntAddTokenFormField() { SecurityToken::enable(); $formWithToken = $this->getStubForm(); $this->assertInstanceOf( 'SilverStripe\\Forms\\HiddenField', $formWithToken->Fields()->fieldByName(SecurityToken::get_default_name()), 'Token field added by default' ); $formWithoutToken = $this->getStubForm(); $formWithoutToken->disableSecurityToken(); $this->assertNull( $formWithoutToken->Fields()->fieldByName(SecurityToken::get_default_name()), 'Token field not added if disableSecurityToken() is set' ); } public function testDisableSecurityTokenAcceptsSubmissionWithoutToken(): void { SecurityToken::enable(); $expectedToken = SecurityToken::inst()->getValue(); $this->get('FormTest_ControllerWithSecurityToken'); // can't use submitForm() as it'll automatically insert SecurityID into the POST data $response = $this->post( 'FormTest_ControllerWithSecurityToken/Form', [ 'Email' => 'test@test.com', 'action_doSubmit' => 1 // leaving out security token ] ); $this->assertEquals(400, $response->getStatusCode(), 'Submission fails without security token'); // Generate a new token which doesn't match the current one $generator = new RandomGenerator(); $invalidToken = $generator->randomToken('sha1'); $this->assertNotEquals($invalidToken, $expectedToken); // Test token with request $this->get('FormTest_ControllerWithSecurityToken'); $response = $this->post( 'FormTest_ControllerWithSecurityToken/Form', [ 'Email' => 'test@test.com', 'action_doSubmit' => 1, 'SecurityID' => $invalidToken ] ); $this->assertEquals(200, $response->getStatusCode(), 'Submission reloads form if security token invalid'); $this->assertTrue( stripos($response->getBody() ?? '', 'name="SecurityID" value="' . $expectedToken . '"') !== false, 'Submission reloads with correct security token after failure' ); $this->assertTrue( stripos($response->getBody() ?? '', 'name="SecurityID" value="' . $invalidToken . '"') === false, 'Submission reloads without incorrect security token after failure' ); $matched = $this->cssParser()->getBySelector('#Form_Form_Email'); $attrs = $matched[0]->attributes(); $this->assertEquals('test@test.com', (string)$attrs['value'], 'Submitted data is preserved'); $this->get('FormTest_ControllerWithSecurityToken'); $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID'); $this->assertEquals( 1, count($tokenEls ?? []), 'Token form field added for controller without disableSecurityToken()' ); } public static function provideFormsSet() { return [ 'with security token' => [ ['Form_Form', null, [ 'Email' => 'test@test.com' ], true], 200, 'Submission succeeds with security token', ], 'without security token' => [ ['Form_Form', null, [ 'Email' => 'test@test.com' ], false], 200, 'Cannot submit form without security token', ], 'button with wrong name' => [ ['Form_Form', 'undefined', [ 'Email' => 'test@test.com' ], true], null, "Can't find button 'undefined' to submit as part of test.", ], ]; } #[DataProvider('provideFormsSet')] public function testSubmitFormWithSpecifiedParameters( array $formData, ?int $statusCode, string $testMessage ): void { $this->get('FormTest_ControllerWithSecurityToken'); [ $form, $button, $data, $withSecurityToken ] = [ ...$formData ]; if (is_null($button)) { $response = $this->submitForm($form, $button, $data, $withSecurityToken); $this->assertEquals($statusCode, $response->getStatusCode(), $testMessage); } else { // Test nonexistent button Exceptions $this->expectException(\Exception::class); $this->expectExceptionMessage($testMessage); $this->submitForm($form, $button, $data, $withSecurityToken); } } public function testStrictFormMethodChecking() { $this->get('FormTest_ControllerWithStrictPostCheck'); $response = $this->get( 'FormTest_ControllerWithStrictPostCheck/Form/?Email=test@test.com&action_doSubmit=1' ); $this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method'); $this->get('FormTest_ControllerWithStrictPostCheck'); $response = $this->post( 'FormTest_ControllerWithStrictPostCheck/Form', [ 'Email' => 'test@test.com', 'action_doSubmit' => 1 ] ); $this->assertEquals(200, $response->getStatusCode(), 'Submission succeeds with correct method'); } public function testEnableSecurityToken() { SecurityToken::disable(); $form = $this->getStubForm(); $this->assertFalse($form->getSecurityToken()->isEnabled()); $form->enableSecurityToken(); $this->assertTrue($form->getSecurityToken()->isEnabled()); SecurityToken::disable(); // restore original } public function testDisableSecurityToken() { SecurityToken::enable(); $form = $this->getStubForm(); $this->assertTrue($form->getSecurityToken()->isEnabled()); $form->disableSecurityToken(); $this->assertFalse($form->getSecurityToken()->isEnabled()); SecurityToken::disable(); // restore original } public function testEncType() { $form = $this->getStubForm(); $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType()); $form->setEncType(Form::ENC_TYPE_MULTIPART); $this->assertEquals('multipart/form-data', $form->getEncType()); $form = $this->getStubForm(); $form->Fields()->push(new FileField(null)); $this->assertEquals('multipart/form-data', $form->getEncType()); $form->setEncType(Form::ENC_TYPE_URLENCODED); $this->assertEquals('application/x-www-form-urlencoded', $form->getEncType()); } public function testAddExtraClass() { $form = $this->getStubForm(); $form->addExtraClass('class1'); $form->addExtraClass('class2'); $this->assertStringEndsWith('class1 class2', $form->extraClass()); } public function testHasExtraClass() { $form = $this->getStubForm(); $form->addExtraClass('class1'); $form->addExtraClass('class2'); $this->assertTrue($form->hasExtraClass('class1')); $this->assertTrue($form->hasExtraClass('class2')); $this->assertTrue($form->hasExtraClass('class1 class2')); $this->assertTrue($form->hasExtraClass('class2 class1')); $this->assertFalse($form->hasExtraClass('class3')); $this->assertFalse($form->hasExtraClass('class2 class3')); } public function testRemoveExtraClass() { $form = $this->getStubForm(); $form->addExtraClass('class1'); $form->addExtraClass('class2'); $this->assertStringEndsWith('class1 class2', $form->extraClass()); $form->removeExtraClass('class1'); $this->assertStringEndsWith('class2', $form->extraClass()); } public function testAddManyExtraClasses() { $form = $this->getStubForm(); //test we can split by a range of spaces and tabs $form->addExtraClass('class1 class2 class3 class4 class5'); $this->assertStringEndsWith( 'class1 class2 class3 class4 class5', $form->extraClass() ); //test that duplicate classes don't get added $form->addExtraClass('class1 class2'); $this->assertStringEndsWith( 'class1 class2 class3 class4 class5', $form->extraClass() ); } public function testRemoveManyExtraClasses() { $form = $this->getStubForm(); $form->addExtraClass('class1 class2 class3 class4 class5'); //test we can remove a single class we just added $form->removeExtraClass('class3'); $this->assertStringEndsWith( 'class1 class2 class4 class5', $form->extraClass() ); //check we can remove many classes at once $form->removeExtraClass('class1 class5'); $this->assertStringEndsWith( 'class2 class4', $form->extraClass() ); //check that removing a dud class is fine $form->removeExtraClass('dudClass'); $this->assertStringEndsWith( 'class2 class4', $form->extraClass() ); } public function testDefaultClasses() { Form::config()->merge( 'default_classes', [ 'class1', ] ); $form = $this->getStubForm(); $this->assertStringContainsString('class1', $form->extraClass(), 'Class list does not contain expected class'); Form::config()->merge( 'default_classes', [ 'class1', 'class2', ] ); $form = $this->getStubForm(); $this->assertStringContainsString('class1 class2', $form->extraClass(), 'Class list does not contain expected class'); Form::config()->merge( 'default_classes', [ 'class3', ] ); $form = $this->getStubForm(); $this->assertStringContainsString('class3', $form->extraClass(), 'Class list does not contain expected class'); $form->removeExtraClass('class3'); $this->assertStringNotContainsString('class3', $form->extraClass(), 'Class list contains unexpected class'); } public function testAttributes() { $form = $this->getStubForm(); $form->setAttribute('foo', 'bar'); $this->assertEquals('bar', $form->getAttribute('foo')); $attrs = $form->getAttributes(); $this->assertArrayHasKey('foo', $attrs); $this->assertEquals('bar', $attrs['foo']); } public function testButtonClicked() { $form = $this->getStubForm(); $action = $form->getRequestHandler()->buttonClicked(); $this->assertNull($action); $controller = new FormTest\TestController(); $form = $controller->Form(); $request = new HTTPRequest( 'POST', 'FormTest_Controller/Form', [], [ 'Email' => 'test@test.com', 'SomeRequiredField' => 1, 'action_doSubmit' => 1 ] ); $request->setSession(new Session([])); $form->getRequestHandler()->httpSubmission($request); $button = $form->getRequestHandler()->buttonClicked(); $this->assertInstanceOf(FormAction::class, $button); $this->assertEquals('doSubmit', $button->actionName()); $form = new Form( $controller, 'Form', new FieldList(new FormAction('doSubmit', 'Inline action')), new FieldList() ); $form->disableSecurityToken(); $request = new HTTPRequest( 'POST', 'FormTest_Controller/Form', [], [ 'action_doSubmit' => 1 ] ); $request->setSession(new Session([])); $form->getRequestHandler()->httpSubmission($request); $button = $form->getRequestHandler()->buttonClicked(); $this->assertInstanceOf(FormAction::class, $button); $this->assertEquals('doSubmit', $button->actionName()); } public function testCheckAccessAction() { $controller = new FormTest\TestController(); $form = new Form( $controller, 'Form', new FieldList(), new FieldList(new FormAction('actionName', 'Action')) ); $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName')); $form = new Form( $controller, 'Form', new FieldList(new FormAction('inlineAction', 'Inline action')), new FieldList() ); $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction')); } public function testAttributesHTML() { $form = $this->getStubForm(); $form->setAttribute('foo', 'bar'); $this->assertStringContainsString('foo="bar"', $form->getAttributesHTML()); $form->setAttribute('foo', null); $this->assertStringNotContainsString('foo="bar"', $form->getAttributesHTML()); $form->setAttribute('foo', true); $this->assertStringContainsString('foo="foo"', $form->getAttributesHTML()); $form->setAttribute('one', 1); $form->setAttribute('two', 2); $form->setAttribute('three', 3); $form->setAttribute('', ''); $this->assertStringNotContainsString('one="1"', $form->getAttributesHTML('one', 'two')); $this->assertStringNotContainsString('two="2"', $form->getAttributesHTML('one', 'two')); $this->assertStringContainsString('three="3"', $form->getAttributesHTML('one', 'two')); $this->assertStringNotContainsString('', $form->getAttributesHTML()); } function testMessageEscapeHtml() { $form = $this->getStubForm(); $form->setMessage('Escaped HTML', 'good', ValidationResult::CAST_TEXT); $parser = new CSSContentParser($form->forTemplate()); $messageEls = $parser->getBySelector('.message'); $this->assertStringContainsString( '<em>Escaped HTML</em>', $messageEls[0]->asXML() ); $form = $this->getStubForm(); $form->setMessage('Unescaped HTML', 'good', ValidationResult::CAST_HTML); $parser = new CSSContentParser($form->forTemplate()); $messageEls = $parser->getBySelector('.message'); $this->assertStringContainsString( 'Unescaped HTML', $messageEls[0]->asXML() ); } public function testFieldMessageEscapeHtml() { $form = $this->getStubForm(); $form->Fields()->dataFieldByName('key1')->setMessage('Escaped HTML', 'good'); $parser = new CSSContentParser($result = $form->forTemplate()); $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message'); $this->assertStringContainsString( '<em>Escaped HTML</em>', $messageEls[0]->asXML() ); // Test with HTML $form = $this->getStubForm(); $form ->Fields() ->dataFieldByName('key1') ->setMessage('Unescaped HTML', 'good', ValidationResult::CAST_HTML); $parser = new CSSContentParser($form->forTemplate()); $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message'); $this->assertStringContainsString( 'Unescaped HTML', $messageEls[0]->asXML() ); } #[DataProvider('formMessageDataProvider')] public function testFieldMessageAppend($messages) { $form = $this->getStubForm(); foreach ($messages as $message) { $form->appendMessage($message); } $parser = new CSSContentParser($form->forTemplate()); $messageEls = $parser->getBySelector('.message'); foreach ($messages as $message) { $this->assertStringContainsString($message, $messageEls[0]->asXML()); $this->assertEquals(1, substr_count($messageEls[0]->asXML(), $message), 'Should not append if already present'); } } #[DataProvider('formMessageExceptionsDataProvider')] public function testFieldMessageAppendExceptions(array $message1, array $message2, bool $force = false) { $form = $this->getStubForm(); $form->appendMessage($message1['val'], $message1['type'], $message1['cast']); if (!$force) { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage( sprintf( "Couldn't append message of type %s and cast %s to existing message of type %s and cast %s", $message2['type'], $message2['cast'], $message1['type'], $message1['cast'], ) ); } $form->appendMessage($message2['val'], $message2['type'], $message2['cast'], $force); if ($force) { $parser = new CSSContentParser($form->forTemplate()); $messageEls = $parser->getBySelector('.message'); $this->assertStringContainsString($message2['val'], $messageEls[0]->asXML()); $this->assertStringNotContainsString($message1['val'], $messageEls[0]->asXML()); } } public function testGetExtraFields() { $form = new FormTest\ExtraFieldsForm( new FormTest\TestController(), 'Form', new FieldList(new TextField('key1')), new FieldList() ); $data = [ 'key1' => 'test', 'ExtraFieldCheckbox' => false, ]; $form->loadDataFrom($data); $formData = $form->getData(); $this->assertEmpty($formData['ExtraFieldCheckbox']); } /** * @param bool $allow */ #[DataProvider('boolDataProvider')] public function testPasswordPostback($allow) { $form = $this->getStubForm(); $form->enableSecurityToken(); $form->Fields()->push( PasswordField::create('Password') ->setAllowValuePostback($allow) ); $form->Actions()->push(FormAction::create('doSubmit')); $request = new HTTPRequest( 'POST', 'FormTest_Controller/Form', [], [ 'key1' => 'foo', 'Password' => 'hidden', SecurityToken::inst()->getName() => 'fail', 'action_doSubmit' => 1, ] ); $form->getRequestHandler()->httpSubmission($request); $parser = new CSSContentParser($form->forTemplate()); $passwords = $parser->getBySelector('input#Password'); $this->assertNotNull($passwords); $this->assertCount(1, $passwords); /* @var \SimpleXMLElement $password */ $password = $passwords[0]; $attrs = iterator_to_array($password->attributes()); if ($allow) { $this->assertArrayHasKey('value', $attrs); $this->assertEquals('hidden', $attrs['value']); } else { $this->assertArrayNotHasKey('value', $attrs); } } /** * This test confirms that when a form validation fails, the submitted value are stored in the session and are * reloaded correctly once the form is re-rendered. This indirectly test `Form::restoreFormState`, * `Form::setSessionData`, `Form::getSessionData` and `Form::clearFormState`. */ public function testRestoreFromState() { // Use a specially crafted controlled for this request. The associated form contains fields that override the // `setSubmittedValue` and require an internal format that differs from the submitted format. $this->get('FormTest_ControllerWithSpecialSubmittedValueFields')->getBody(); // Posting our form. This should fail and redirect us to the form page and preload our submit value $response = $this->post( 'FormTest_ControllerWithSpecialSubmittedValueFields/Form', [ 'SomeDateField' => '15/06/2018', 'SomeFrenchNumericField' => '9 876,5432', 'SomeFrenchMoneyField' => [ 'Amount' => '9 876,54', 'Currency' => 'NZD' ] // Validation will fail because we leave out SomeRequiredField ], [] ); // Test our reloaded form field $body = $response->getBody(); $this->assertStringContainsString( 'assertStringContainsString( 'clean($body), 'Our reloaded form should contain a SomeFrenchNumericField with the value "9 876,5432"' ); $this->assertStringContainsString( 'assertStringContainsString( 'clean($body), 'Our reloaded form should contain a SomeFrenchMoneyField[Amount] with the value "9 876,54"' ); $this->assertEmpty( $this->mainSession->session()->get('FormInfo.Form_Form'), 'Our form was reloaded successfully. That should have cleared our session.' ); } protected function getStubForm() { return new Form( new FormTest\TestController(), 'Form', new FieldList(new TextField('key1')), new FieldList() ); } /** * Some fields handle submitted values differently from their internal values. This forms contains 2 such fields * * a SomeDateTimeField that expect a date such as `Fri, Jun 15, '18 17:28:05`, * * a SomeTimeField that expects it's time as `05 o'clock PM 28 05` * * @return Form */ protected function getStubFormWithWeirdValueFormat() { return new Form( Controller::curr(), 'Form', new FieldList( $dateField = DatetimeField::create('SomeDateTimeField') ->setHTML5(false) ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), $timeField = TimeField::create('SomeTimeField') ->setHTML5(false) ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format ), new FieldList() ); } /** * In some cases and locales, validation expects non-breaking spaces. * This homogenises narrow and regular NBSPs to a regular space character * * @param string $input * @return string The input value, with all non-breaking spaces replaced with spaces */ protected function clean($input) { return str_replace( [ html_entity_decode(' ', 0, 'UTF-8'), html_entity_decode(' ', 0, 'UTF-8'), // narrow non-breaking space ], ' ', trim($input ?? '') ); } }