/** * @covers ::handleFormErrors * @covers ::setElementErrorsFromFormState */ public function testSetElementErrorsFromFormState() { $form_error_handler = $this->getMockBuilder('Drupal\\Core\\Form\\FormErrorHandler')->setMethods(['drupalSetMessage'])->getMock(); $form = ['#parents' => []]; $form['test'] = ['#type' => 'textfield', '#title' => 'Test', '#parents' => ['test'], '#id' => 'edit-test']; $form_state = new FormState(); $form_state->setErrorByName('test', 'invalid'); $form_error_handler->handleFormErrors($form, $form_state); $this->assertSame('invalid', $form['test']['#errors']); }
/** * @covers ::handleFormErrors * @covers ::setElementErrorsFromFormState */ public function testSetElementErrorsFromFormState() { $form_error_handler = $this->getMockBuilder('Drupal\\Core\\Form\\FormErrorHandler')->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock('Drupal\\Core\\Utility\\LinkGeneratorInterface')])->setMethods(['drupalSetMessage'])->getMock(); $form = ['#parents' => []]; $form['test'] = ['#type' => 'textfield', '#title' => 'Test', '#parents' => ['test'], '#id' => 'edit-test']; $form_state = new FormState(); $form_state->setErrorByName('test', 'invalid'); $form_error_handler->handleFormErrors($form, $form_state); $this->assertSame('invalid', $form['test']['#errors']); }
/** * Given an array of values and an array of fields, extract data for use. * * This function generates the data to send for validation to Mollom by walking * through the submitted form values and * - copying element values as specified via 'mapping' in hook_mollom_form_info() * into the dedicated data properties * - collecting and concatenating all fields that have been selected for textual * analysis into the 'post_body' property * * The processing accounts for the following possibilities: * - A field was selected for textual analysis, but there is no submitted form * value. The value should have been appended to the 'post_body' property, but * will be skipped. * - A field is contained in the 'mapping' and there is a submitted form value. * The value will not be appended to the 'post_body', but instead be assigned * to the specified data property. * - All fields specified in 'mapping', for which there is a submitted value, * but which were NOT selected for textual analysis, are assigned to the * specified data property. This is usually the case for form elements that * hold system user information. * * @param $form_state * An associative array containing * - values: The submitted form values. * - buttons: A list of button form elements. See form_state_values_clean(). * @param $fields * A list of strings representing form elements to extract. Nested fields are * in the form of 'parent][child'. * @param $mapping * An associative array of form elements to map to Mollom's dedicated data * properties. See hook_mollom_form_info() for details. * * @see hook_mollom_form_info() */ public static function extractMollomValues(FormState $form_state, $fields, $mapping) { $user = \Drupal::currentUser(); // All elements specified in $mapping must be excluded from $fields, as they // are used for dedicated $data properties instead. To reduce the parsing code // size, we are turning a given $mapping of e.g. // array('post_title' => 'title_form_element') // into // array('title_form_element' => 'post_title') // and we reset $mapping afterwards. // When iterating over the $fields, this allows us to quickly test whether the // current field should be excluded, and if it should, we directly get the // mapped property name to rebuild $mapping with the field values. $exclude_fields = array(); if (!empty($mapping)) { $exclude_fields = array_flip($mapping); } $mapping = array(); // Process all fields that have been selected for text analysis. $post_body = array(); foreach ($fields as $field) { // Nested elements use a key of 'parent][child', so we need to recurse. $parents = explode('][', $field); $value = $form_state->getValue($parents); // If this field was contained in $mapping and should be excluded, add it to // $mapping with the actual form element value, and continue to the next // field. Also unset this field from $exclude_fields, so we can process the // remaining mappings below. if (isset($exclude_fields[$field])) { if (is_array($value)) { $value = implode(' ', MollomUtilities::flattenFormValue($value)); } $mapping[$exclude_fields[$field]] = $value; unset($exclude_fields[$field]); continue; } // Only add form element values that are not empty. if (isset($value)) { // UTF-8 validation happens later. if (is_string($value) && strlen($value)) { $post_body[$field] = $value; } elseif (is_array($value) && !empty($value)) { // Ensure we have a flat, indexed array to implode(); form values of // field_attach_form() use several subkeys. $value = MollomUtilities::flattenFormValue($value); $post_body = array_merge($post_body, $value); } } } $post_body = implode("\n", $post_body); // Try to assign any further form values by processing the remaining mappings, // which have been turned into $exclude_fields above. All fields that were // already used for 'post_body' no longer exist in $exclude_fields. foreach ($exclude_fields as $field => $property) { // If the postTitle field was not included in the enabled fields, then don't // set it's mapping here. if ($property === 'post_title' && !in_array($field, $fields)) { continue; } // Nested elements use a key of 'parent][child', so we need to recurse. $parents = explode('][', $field); $value = $form_state->getValue($parents); if (isset($value)) { if (is_array($value)) { $value = MollomUtilities::flattenFormValue($value); $value = implode(' ', $value); } $mapping[$property] = $value; } } // Build the data structure expected by the Mollom API. $data = array(); // Post id; not sent to Mollom. // @see submitForm() if (!empty($mapping['post_id'])) { $data['postId'] = $mapping['post_id']; } // Post title. if (!empty($mapping['post_title'])) { $data['postTitle'] = $mapping['post_title']; } // Post body. if (!empty($post_body)) { $data['postBody'] = $post_body; } // Author ID. // If a non-anonymous user ID was mapped via form values, use that. if (!empty($mapping['author_id'])) { $data['authorId'] = $mapping['author_id']; } elseif (!empty($user->id())) { $data['authorId'] = $user->id(); } // Load the user account of the author, if any, for the following author* // property assignments. $account = FALSE; if (isset($data['authorId'])) { /** @var \Drupal\user\Entity\User $account */ $account = User::load($data['authorId']); $author_username = $account->getUsername(); $author_email = $account->getEmail(); // Author creation date. $data['authorCreated'] = $account->getCreatedTime(); // In case a post of a registered user is edited and a form value mapping // exists for author_id, but no form value mapping exists for author_name, // use the name of the user account associated with author_id. // $account may be the same as the currently logged-in $user at this point. if (!empty($author_username)) { $data['authorName'] = $author_username; } if (!empty($author_email)) { $data['authorMail'] = $author_email; } } // Author name. // A form value mapping always has precedence. if (!empty($mapping['author_name'])) { $data['authorName'] = $mapping['author_name']; } // Author e-mail. if (!empty($mapping['author_mail'])) { $data['authorMail'] = $mapping['author_mail']; } // Author homepage. if (!empty($mapping['author_url'])) { $data['authorUrl'] = $mapping['author_url']; } // Author OpenID. if (!empty($mapping['author_openid'])) { $data['authorOpenid'] = $mapping['author_openid']; } // Author IP. $data['authorIp'] = \Drupal::request()->getClientIp(); $mollom_form = $form_state->getValue('mollom'); // Honeypot. // For the Mollom backend, it only matters whether 'honeypot' is non-empty. // The submitted value is only taken over to allow site administrators to // see the actual honeypot value in watchdog log entries. if (isset($mollom_form['homepage']) && $mollom_form['homepage'] !== '') { $data['honeypot'] = $mollom_form['homepage']; } // Add the contextCreated parameter if a callback exists. if (isset($mollom_form['context created callback']) && function_exists($mollom_form['context created callback'])) { if (!empty($mapping['context_id'])) { $contextCreated = call_user_func($mollom_form['context created callback'], $mapping['context_id']); if ($contextCreated !== FALSE) { $data['contextCreated'] = $contextCreated; } } } // Ensure that all $data values contain valid UTF-8. Invalid UTF-8 would be // sanitized into an empty string, so the Mollom backend would not receive // any value. $invalid_utf8 = FALSE; $invalid_xml = FALSE; // Include the CAPTCHA solution user input in the UTF-8 validation. $solution = isset($mollom_form['captcha']['captcha_input']) ? array('solution' => $mollom_form['captcha']['captcha_input']) : array(); foreach ($data + $solution as $key => $value) { // Check for invalid UTF-8 byte sequences first. if (!Unicode::validateUtf8($value)) { $invalid_utf8 = TRUE; // Replace the bogus string, since $data will be logged as // check_plain(var_export($data)), and check_plain() would empty the // entire exported variable string otherwise. $data[$key] = '- Invalid UTF-8 -'; } elseif (preg_match('@[^\\x9\\xA\\xD\\x20-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{10000}-\\x{10FFFF}]@u', $value)) { $invalid_xml = TRUE; } } if ($invalid_utf8 || $invalid_xml) { $form_state->setErrorByName('', t('Your submission contains invalid characters and will not be accepted.')); Logger::addMessage(['message' => 'Invalid @type in form values', 'arguments' => ['@type' => $invalid_utf8 ? 'UTF-8' : 'XML characters'], 'data' => $data]); $data = FALSE; } return $data; }
/** * Tests that form errors during submission throw an exception. * * @covers ::setErrorByName * * @expectedException \LogicException * @expectedExceptionMessage Form errors cannot be set after form validation has finished. */ public function testFormErrorsDuringSubmission() { $form_state = new FormState(); $form_state->setValidationComplete(); $form_state->setErrorByName('test', 'message'); }
/** * Tests selectItemAjax(). * * GIVEN the embridge search form * WHEN the ajax handler for the select button is run with errors * THEN an ajax response should be returned with the expected commands. * * @covers ::selectItemAjax * * @test */ public function selectItemAjaxAjaxCallbackReturnsReplaceHtmlWhenErrorsExist() { $form = ['#attached' => ['test']]; $form_state = new FormState(); $form_state->setErrorByName('test', 'test error'); $response = $this->form->selectItemAjax($form, $form_state); $this->assertInstanceOf('\\Drupal\\Core\\Ajax\\AjaxResponse', $response); $commands = $response->getCommands(); $this->assertNotEmpty($commands); $this->assertCount(3, $commands); $this->assertEquals('replaceWith', $commands[0]['method']); $this->assertEquals('html', $commands[1]['method']); $this->assertEquals('append', $commands[2]['method']); }
/** * Test submitForm with errors. * * @covers ::ajaxSave * * @test */ public function ajaxSaveWithErrorsReturnsHtmlCommand() { // #attached required for CommandWithAttachedAssetsTrait checks. $form = ['#attached' => []]; $form_state = new FormState(); $form_state->setErrorByName('test', 'ERROR ERROR!!'); $response = $this->form->ajaxSave($form, $form_state); $this->assertInstanceOf('\\Drupal\\Core\\Ajax\\AjaxResponse', $response); $commands = $response->getCommands(); $this->assertNotEmpty($commands); $this->assertCount(1, $commands); $this->assertEquals('insert', $commands[0]['command']); }