From a82955bfd57c2262f272da7a51615aecc13555d8 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Tue, 8 May 2012 12:00:06 +0900
Subject: [PATCH] Issue #1174766 by Niklas Fiekas, ericduran: Added Support the
 #pattern FAPI property for native HTML5 pattern attribute.

---
 core/includes/form.inc                        | 50 ++++++++++++++++
 core/modules/system/system.module             | 10 ++--
 core/modules/system/tests/form.test           | 59 +++++++++++++++++++
 .../tests/modules/form_test/form_test.module  | 41 +++++++++++++
 4 files changed, 155 insertions(+), 5 deletions(-)

diff --git a/core/includes/form.inc b/core/includes/form.inc
index 6d577806d191..a555bf09a164 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -3213,6 +3213,56 @@ function form_process_actions($element, &$form_state) {
   return $element;
 }
 
+/**
+ * #process callback for #pattern form element property.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   generic input element.
+ * @param $form_state
+ *   The $form_state array for the form this element belongs to.
+ *
+ * @return
+ *   The processed element.
+ *
+ * @see form_validate_pattern()
+ */
+function form_process_pattern($element, &$form_state) {
+  if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
+    $element['#attributes']['pattern'] = $element['#pattern'];
+    $element['#element_validate'][] = 'form_validate_pattern';
+  }
+
+  return $element;
+}
+
+/**
+ * #element_validate callback for #pattern form element property.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   generic form element.
+ * @param $form_state
+ *   The $form_state array for the form this element belongs to.
+ *
+ * @see form_process_pattern()
+ */
+function form_validate_pattern($element, &$form_state) {
+  if ($element['#value'] !== '') {
+    // The pattern must match the entire string and should have the same
+    // behavior as the RegExp object in ECMA 262.
+    // - Use bracket-style delimiters to avoid introducing a special delimiter
+    //   character like '/' that would have to be escaped.
+    // - Put in brackets so that the pattern can't interfere with what's
+    //   prepended and appended.
+    $pattern = '{^(?:' . $element['#pattern'] . ')$}';
+
+    if (!preg_match($pattern, $element['#value'])) {
+      form_error($element, t('%name field is not in the right format.', array('%name' => $element['#title'])));
+    }
+  }
+}
+
 /**
  * Processes a container element.
  *
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index c8df3c5b5f83..987137b55267 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -361,7 +361,7 @@ function system_element_info() {
     '#size' => 60,
     '#maxlength' => 128,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_autocomplete', 'ajax_process_form'),
+    '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'),
     '#theme' => 'textfield',
     '#theme_wrappers' => array('form_element'),
   );
@@ -370,7 +370,7 @@ function system_element_info() {
     '#size' => 30,
     '#maxlength' => 128,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_autocomplete', 'ajax_process_form'),
+    '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'),
     '#theme' => 'tel',
     '#theme_wrappers' => array('form_element'),
   );
@@ -379,7 +379,7 @@ function system_element_info() {
     '#size' => 60,
     '#maxlength' => EMAIL_MAX_LENGTH,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_autocomplete', 'ajax_process_form'),
+    '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'),
     '#element_validate' => array('form_validate_email'),
     '#theme' => 'email',
     '#theme_wrappers' => array('form_element'),
@@ -389,7 +389,7 @@ function system_element_info() {
     '#size' => 60,
     '#maxlength' => 255,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_autocomplete', 'ajax_process_form'),
+    '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'),
     '#element_validate' => array('form_validate_url'),
     '#theme' => 'url',
     '#theme_wrappers' => array('form_element'),
@@ -437,7 +437,7 @@ function system_element_info() {
     '#input' => TRUE,
     '#size' => 60,
     '#maxlength' => 128,
-    '#process' => array('ajax_process_form'),
+    '#process' => array('ajax_process_form', 'form_process_pattern'),
     '#theme' => 'password',
     '#theme_wrappers' => array('form_element'),
   );
diff --git a/core/modules/system/tests/form.test b/core/modules/system/tests/form.test
index ada1a1beb261..dbb2ff5367cc 100644
--- a/core/modules/system/tests/form.test
+++ b/core/modules/system/tests/form.test
@@ -784,6 +784,65 @@ class FormValidationTestCase extends DrupalWebTestCase {
     $this->assertText(t('!name field is required.', array('!name' => 'Title')));
     $this->assertText('Test element is invalid');
   }
+
+  /**
+   * Tests #pattern validation.
+   */
+  function testPatternValidation() {
+    $textfield_error = t('%name field is not in the right format.', array('%name' => 'One digit followed by lowercase letters'));
+    $tel_error = t('%name field is not in the right format.', array('%name' => 'Everything except numbers'));
+    $password_error = t('%name field is not in the right format.', array('%name' => 'Password'));
+
+    // Invalid textfield, valid tel.
+    $edit = array(
+      'textfield' => 'invalid',
+      'tel' => 'valid',
+    );
+    $this->drupalPost('form-test/pattern', $edit, 'Submit');
+    $this->assertRaw($textfield_error);
+    $this->assertNoRaw($tel_error);
+    $this->assertNoRaw($password_error);
+
+    // Valid textfield, invalid tel, valid password.
+    $edit = array(
+      'textfield' => '7seven',
+      'tel' => '818937',
+      'password' => '0100110',
+    );
+    $this->drupalPost('form-test/pattern', $edit, 'Submit');
+    $this->assertNoRaw($textfield_error);
+    $this->assertRaw($tel_error);
+    $this->assertNoRaw($password_error);
+
+    // Non required fields are not validated if empty.
+    $edit = array(
+      'textfield' => '',
+      'tel' => '',
+    );
+    $this->drupalPost('form-test/pattern', $edit, 'Submit');
+    $this->assertNoRaw($textfield_error);
+    $this->assertNoRaw($tel_error);
+    $this->assertNoRaw($password_error);
+
+    // Invalid password.
+    $edit = array(
+      'password' => $this->randomName(),
+    );
+    $this->drupalPost('form-test/pattern', $edit, 'Submit');
+    $this->assertNoRaw($textfield_error);
+    $this->assertNoRaw($tel_error);
+    $this->assertRaw($password_error);
+
+    // The pattern attribute overrides #pattern and is not validated on the
+    // server side.
+    $edit = array(
+      'textfield' => '',
+      'tel' => '',
+      'url' => 'http://www.example.com/',
+    );
+    $this->drupalPost('form-test/pattern', $edit, 'Submit');
+    $this->assertNoRaw(t('%name field is not in the right format.', array('%name' => 'Client side validation')));
+  }
 }
 
 /**
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index e892f767f6e9..f681c75c691c 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -37,6 +37,12 @@ function form_test_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/pattern'] = array(
+    'title' => 'Pattern validation',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_pattern_form'),
+    'access callback' => TRUE,
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -531,6 +537,41 @@ function form_test_limit_validation_errors_form_partial_submit($form, $form_stat
   }
 }
 
+/**
+ * Builds a simple form using the FAPI #pattern proterty.
+ */
+function form_test_pattern_form($form, &$form_state) {
+  $form['textfield'] = array(
+    '#type' => 'textfield',
+    '#title' => 'One digit followed by lowercase letters',
+    '#pattern' => '[0-9][a-z]+',
+  );
+  $form['tel'] = array(
+    '#type' => 'tel',
+    '#title' => 'Everything except numbers',
+    '#pattern' => '[^\d]*',
+  );
+  $form['password'] = array(
+    '#type' => 'password',
+    '#title' => 'Password',
+    '#pattern' => '[01]+',
+  );
+  $form['url'] = array(
+    '#type' => 'url',
+    '#title' => 'Client side validation',
+    '#decription' => 'Just client side validation, using the #pattern attribute.',
+    '#attributes' => array(
+      'pattern' => '.*foo.*',
+    ),
+    '#pattern' => 'ignored',
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Submit',
+  );
+  return $form;
+}
+
 /**
  * Create a header and options array. Helper function for callbacks.
  */
-- 
GitLab