From 2c10d1526fa2feddbbd40f73a9cb894091a5d8a7 Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Thu, 16 Sep 2010 20:14:49 +0000
Subject: [PATCH] - Patch #740834 by makononov, sun: form elements cannot be
 rendered without form_builder().

---
 includes/common.inc                  |  24 +++++
 includes/form.inc                    | 148 +++++++++++++--------------
 modules/field_ui/field_ui.admin.inc  |  15 ++-
 modules/simpletest/tests/common.test | 132 +++++++++++++++++++++++-
 4 files changed, 234 insertions(+), 85 deletions(-)

diff --git a/includes/common.inc b/includes/common.inc
index 07422d08ddf7..bebe2ad52deb 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -5601,6 +5601,30 @@ function element_get_visible_children(array $elements) {
   return array_keys($visible_children);
 }
 
+/**
+ * Sets HTML attributes based on element properties.
+ *
+ * @param $element
+ *   The renderable element to process.
+ * @param $map
+ *   An associative array whose keys are element property names and whose values
+ *   are the HTML attribute names to set for corresponding the property; e.g.,
+ *   array('#propertyname' => 'attributename'). If both names are identical
+ *   except for the leading '#', then an attribute name value is sufficient and
+ *   no property name needs to be specified.
+ */
+function element_set_attributes(array &$element, array $map) {
+  foreach ($map as $property => $attribute) {
+    // If the key is numeric, the attribute name needs to be taken over.
+    if (is_int($property)) {
+      $property = '#' . $attribute;
+    }
+    if (isset($element[$property])) {
+      $element['#attributes'][$attribute] = $element[$property];
+    }
+  }
+}
+
 /**
  * Sets a value in a nested array with variable depth.
  *
diff --git a/includes/form.inc b/includes/form.inc
index 59ed6517af49..e42beea73145 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -1414,17 +1414,20 @@ function form_get_errors() {
 }
 
 /**
- * Return the error message filed against the form with the specified name.
+ * Returns the error message filed against the given form element.
+ *
+ * Form errors higher up in the form structure override deeper errors as well as
+ * errors on the element itself.
  */
 function form_get_error($element) {
   $form = form_set_error();
-  $key = $element['#parents'][0];
-  if (isset($form[$key])) {
-    return $form[$key];
-  }
-  $key = implode('][', $element['#parents']);
-  if (isset($form[$key])) {
-    return $form[$key];
+  $parents = array();
+  foreach ($element['#parents'] as $parent) {
+    $parents[] = $parent;
+    $key = implode('][', $parents);
+    if (isset($form[$key])) {
+      return $form[$key];
+    }
   }
 }
 
@@ -2251,10 +2254,15 @@ function _form_options_flatten($array) {
  */
 function theme_select($variables) {
   $element = $variables['element'];
-  $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
+  element_set_attributes($element, array('id', 'name', 'size'));
   _form_set_class($element, array('form-select'));
-  $multiple = $element['#multiple'];
-  return '<select name="' . $element['#name'] . '' . ($multiple ? '[]' : '') . '"' . ($multiple ? ' multiple="multiple" ' : '') . drupal_attributes($element['#attributes']) . ' id="' . $element['#id'] . '" ' . $size . '>' . form_select_options($element) . '</select>';
+
+  if (!empty($element['#multiple'])) {
+    $element['#attributes']['multiple'] = 'multiple';
+    $element['#attributes']['name'] .= '[]';
+  }
+
+  return '<select' . drupal_attributes($element['#attributes']) . '>' . form_select_options($element) . '</select>';
 }
 
 /**
@@ -2276,7 +2284,7 @@ function form_select_options($element, $choices = NULL) {
   // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
   // isset() fails in this situation.
   $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
-  $value_is_array = is_array($element['#value']);
+  $value_is_array = $value_valid && is_array($element['#value']);
   $options = '';
   foreach ($choices as $key => $choice) {
     if (is_array($choice)) {
@@ -2364,6 +2372,8 @@ function form_get_options($element, $key) {
  */
 function theme_fieldset($variables) {
   $element = $variables['element'];
+  element_set_attributes($element, array('id'));
+  _form_set_class($element, array('form-wrapper'));
 
   $output = '<fieldset' . drupal_attributes($element['#attributes']) . '>';
   if (!empty($element['#title'])) {
@@ -2397,10 +2407,9 @@ function theme_fieldset($variables) {
 function theme_radio($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'radio';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#return_value'];
-  if (check_plain($element['#value']) == $element['#return_value']) {
+  element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
+
+  if (isset($element['#return_value']) && check_plain($element['#value']) == $element['#return_value']) {
     $element['#attributes']['checked'] = 'checked';
   }
   _form_set_class($element, array('form-radio'));
@@ -2422,7 +2431,7 @@ function theme_radio($variables) {
 function theme_radios($variables) {
   $element = $variables['element'];
   $attributes = array();
-  if (!empty($element['#id'])) {
+  if (isset($element['#id'])) {
     $attributes['id'] = $element['#id'];
   }
   $attributes['class'] = 'form-radios';
@@ -2507,9 +2516,11 @@ function theme_date($variables) {
 function form_process_date($element) {
   // Default to current date
   if (empty($element['#value'])) {
-    $element['#value'] = array('day' => format_date(REQUEST_TIME, 'custom', 'j'),
-                            'month' => format_date(REQUEST_TIME, 'custom', 'n'),
-                            'year' => format_date(REQUEST_TIME, 'custom', 'Y'));
+    $element['#value'] = array(
+      'day' => format_date(REQUEST_TIME, 'custom', 'j'),
+      'month' => format_date(REQUEST_TIME, 'custom', 'n'),
+      'year' => format_date(REQUEST_TIME, 'custom', 'Y'),
+    );
   }
 
   $element['#tree'] = TRUE;
@@ -2529,9 +2540,11 @@ function form_process_date($element) {
       case 'day':
         $options = drupal_map_assoc(range(1, 31));
         break;
+
       case 'month':
         $options = drupal_map_assoc(range(1, 12), 'map_month');
         break;
+
       case 'year':
         $options = drupal_map_assoc(range(1900, 2050));
         break;
@@ -2632,11 +2645,10 @@ function theme_checkbox($variables) {
   $element = $variables['element'];
   $t = get_t();
   $element['#attributes']['type'] = 'checkbox';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#return_value'];
+  element_set_attributes($element, array('id', 'name', '#return_value' => 'value'));
+
   // Unchecked checkbox has #value of integer 0.
-  if ($element['#value'] !== 0 && $element['#value'] == $element['#return_value']) {
+  if (isset($element['#return_value']) && isset($element['#value']) && $element['#value'] !== 0 && $element['#value'] == $element['#return_value']) {
     $element['#attributes']['checked'] = 'checked';
   }
   _form_set_class($element, array('form-checkbox'));
@@ -2657,12 +2669,12 @@ function theme_checkbox($variables) {
 function theme_checkboxes($variables) {
   $element = $variables['element'];
   $attributes = array();
-  if (!empty($element['#id'])) {
+  if (isset($element['#id'])) {
     $attributes['id'] = $element['#id'];
   }
-  $attributes['class'] = 'form-checkboxes';
+  $attributes['class'][] = 'form-checkboxes';
   if (!empty($element['#attributes']['class'])) {
-    $attributes['class'] .= ' ' . implode(' ', $element['#attributes']['class']);
+    $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']);
   }
   return '<div' . drupal_attributes($attributes) . '>' . (!empty($element['#children']) ? $element['#children'] : '') . '</div>';
 }
@@ -2938,7 +2950,6 @@ function form_process_fieldset(&$element, &$form_state) {
   if (!isset($element['#attributes']['class'])) {
     $element['#attributes']['class'] = array();
   }
-  $element['#attributes']['class'][] = 'form-wrapper';
 
   // Collapsible fieldsets
   if (!empty($element['#collapsible'])) {
@@ -2948,7 +2959,6 @@ function form_process_fieldset(&$element, &$form_state) {
       $element['#attributes']['class'][] = 'collapsed';
     }
   }
-  $element['#attributes']['id'] = $element['#id'];
 
   return $element;
 }
@@ -2964,6 +2974,10 @@ function form_process_fieldset(&$element, &$form_state) {
  *   The modified element with all group members.
  */
 function form_pre_render_fieldset($element) {
+  // Fieldsets may be rendered outside of a Form API context.
+  if (!isset($element['#parents']) || !isset($element['#groups'])) {
+    return $element;
+  }
   // Inject group member elements belonging to this group.
   $parents = implode('][', $element['#parents']);
   $children = element_children($element['#groups'][$parents]);
@@ -3073,8 +3087,7 @@ function theme_vertical_tabs($variables) {
  * @ingroup themeable
  */
 function theme_submit($variables) {
-  $element = $variables['element'];
-  return theme('button', $element);
+  return theme('button', $variables['element']);
 }
 
 /**
@@ -3090,11 +3103,8 @@ function theme_submit($variables) {
 function theme_button($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'submit';
-  if (!empty($element['#name'])) {
-    $element['#attributes']['name'] = $element['#name'];
-  }
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#value'];
+  element_set_attributes($element, array('id', 'name', 'value'));
+
   $element['#attributes']['class'][] = 'form-' . $element['#button_type'];
   if (!empty($element['#attributes']['disabled'])) {
     $element['#attributes']['class'][] = 'form-button-disabled';
@@ -3116,11 +3126,8 @@ function theme_button($variables) {
 function theme_image_button($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'image';
-  $element['#attributes']['name'] = $element['#name'];
-  if (!empty($element['#value'])) {
-    $element['#attributes']['value'] = $element['#value'];
-  }
-  $element['#attributes']['id'] = $element['#id'];
+  element_set_attributes($element, array('id', 'name', 'value'));
+
   $element['#attributes']['src'] = file_create_url($element['#src']);
   if (!empty($element['#title'])) {
     $element['#attributes']['alt'] = $element['#title'];
@@ -3148,9 +3155,7 @@ function theme_image_button($variables) {
 function theme_hidden($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'hidden';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#value'];
+  element_set_attributes($element, array('id', 'name', 'value'));
   return '<input' . drupal_attributes($element['#attributes']) . " />\n";
 }
 
@@ -3168,15 +3173,7 @@ function theme_hidden($variables) {
 function theme_textfield($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'text';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#value'];
-  if (!empty($element['#size'])) {
-    $element['#attributes']['size'] = $element['#size'];
-  }
-  if (!empty($element['#maxlength'])) {
-    $element['#attributes']['maxlength'] = $element['#maxlength'];
-  }
+  element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength'));
   _form_set_class($element, array('form-text'));
 
   $extra = '';
@@ -3186,7 +3183,7 @@ function theme_textfield($variables) {
 
     $attributes = array();
     $attributes['type'] = 'hidden';
-    $attributes['id'] = $element['#id'] . '-autocomplete';
+    $attributes['id'] = $element['#attributes']['id'] . '-autocomplete';
     $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE));
     $attributes['disabled'] = 'disabled';
     $attributes['class'][] = 'autocomplete';
@@ -3210,14 +3207,13 @@ function theme_textfield($variables) {
  */
 function theme_form($variables) {
   $element = $variables['element'];
-  if (!empty($element['#action'])) {
+  if (isset($element['#action'])) {
     $element['#attributes']['action'] = drupal_strip_dangerous_protocols($element['#action']);
   }
-  $element['#attributes']['method'] = $element['#method'];
+  element_set_attributes($element, array('method', 'id'));
   if (empty($element['#attributes']['accept-charset'])) {
     $element['#attributes']['accept-charset'] = "UTF-8";
   }
-  $element['#attributes']['id'] = $element['#id'];
   // Anonymous DIV to satisfy XHTML compliance.
   return '<form' . drupal_attributes($element['#attributes']) . '><div>' . $element['#children'] . '</div></form>';
 }
@@ -3235,10 +3231,7 @@ function theme_form($variables) {
  */
 function theme_textarea($variables) {
   $element = $variables['element'];
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['cols'] = $element['#cols'];
-  $element['#attributes']['rows'] = $element['#rows'];
+  element_set_attributes($element, array('id', 'name', 'cols', 'rows'));
   _form_set_class($element, array('form-textarea'));
 
   $wrapper_attributes = array(
@@ -3271,15 +3264,7 @@ function theme_textarea($variables) {
 function theme_password($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'password';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  $element['#attributes']['value'] = $element['#value'];
-  if (!empty($element['#size'])) {
-    $element['#attributes']['size'] = $element['#size'];
-  }
-  if (!empty($element['#maxlength'])) {
-    $element['#attributes']['maxlength'] = $element['#maxlength'];
-  }
+  element_set_attributes($element, array('id', 'name', 'value', 'size', 'maxlength'));
   _form_set_class($element, array('form-text'));
 
   return '<input' . drupal_attributes($element['#attributes']) . ' />';
@@ -3316,11 +3301,7 @@ function form_process_weight($element) {
 function theme_file($variables) {
   $element = $variables['element'];
   $element['#attributes']['type'] = 'file';
-  $element['#attributes']['name'] = $element['#name'];
-  $element['#attributes']['id'] = $element['#id'];
-  if (!empty($element['#size'])) {
-    $element['#attributes']['size'] = $element['#size'];
-  }
+  element_set_attributes($element, array('id', 'name', 'size'));
   _form_set_class($element, array('form-file'));
 
   return '<input' . drupal_attributes($element['#attributes']) . ' />';
@@ -3373,10 +3354,16 @@ function theme_file($variables) {
  * @ingroup themeable
  */
 function theme_form_element($variables) {
-  $element = $variables['element'];
+  $element = &$variables['element'];
   // This is also used in the installer, pre-database setup.
   $t = get_t();
 
+  // This function is invoked as theme wrapper, but the rendered form element
+  // may not necessarily have been processed by form_builder().
+  $element += array(
+    '#title_display' => 'before',
+  );
+
   // Add element #id for #type 'item'.
   if (isset($element['#markup']) && !empty($element['#id'])) {
     $attributes['id'] = $element['#id'];
@@ -3422,7 +3409,7 @@ function theme_form_element($variables) {
   }
 
   if (!empty($element['#description'])) {
-    $output .= ' <div class="description">' . $element['#description'] . "</div>\n";
+    $output .= '<div class="description">' . $element['#description'] . "</div>\n";
   }
 
   $output .= "</div>\n";
@@ -3521,10 +3508,13 @@ function _form_set_class(&$element, $class = array()) {
     }
     $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class);
   }
-  if ($element['#required']) {
+  // This function is invoked from form element theme functions, but the
+  // rendered form element may not necessarily have been processed by
+  // form_builder().
+  if (!empty($element['#required'])) {
     $element['#attributes']['class'][] = 'required';
   }
-  if (form_get_error($element)) {
+  if (isset($element['#parents']) && form_get_error($element)) {
     $element['#attributes']['class'][] = 'error';
   }
 }
diff --git a/modules/field_ui/field_ui.admin.inc b/modules/field_ui/field_ui.admin.inc
index cf3e8c3e9df0..dcfd7eca4060 100644
--- a/modules/field_ui/field_ui.admin.inc
+++ b/modules/field_ui/field_ui.admin.inc
@@ -1648,13 +1648,22 @@ function field_ui_field_edit_form($form, &$form_state, $instance) {
   $bundles = field_info_bundles();
 
   // Create a form structure for the instance values.
-  $form['instance'] = array(
+  // @todo Fieldset element info needs to be merged in order to not skip the
+  //   default element definition for #pre_render. While the current default
+  //   value could simply be hard-coded, we'd possibly forget this location
+  //   when system_element_info() is updated. See also form_builder(). This
+  //   particular #pre_render, field_ui_field_edit_instance_pre_render(), might
+  //   as well be entirely needless though.
+  $form['instance'] = array_merge(element_info('fieldset'), array(
     '#tree' => TRUE,
     '#type' => 'fieldset',
     '#title' => t('%type settings', array('%type' => $bundles[$entity_type][$bundle]['label'])),
-    '#description' => t('These settings apply only to the %field field when used in the %type type.', array('%field' => $instance['label'], '%type' => $bundles[$entity_type][$bundle]['label'])),
+    '#description' => t('These settings apply only to the %field field when used in the %type type.', array(
+      '%field' => $instance['label'],
+      '%type' => $bundles[$entity_type][$bundle]['label'],
+    )),
     '#pre_render' => array('field_ui_field_edit_instance_pre_render'),
-  );
+  ));
 
   // Build the non-configurable instance values.
   $form['instance']['field_name'] = array(
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index bcec70f0c758..c90a59004cca 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -1389,11 +1389,11 @@ class JavaScriptTestCase extends DrupalWebTestCase {
 /**
  * Tests for drupal_render().
  */
-class DrupalRenderUnitTestCase extends DrupalWebTestCase {
+class DrupalRenderTestCase extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'Drupal render',
-      'description' => 'Performs unit tests on drupal_render().',
+      'name' => 'drupal_render()',
+      'description' => 'Performs functional tests on drupal_render().',
       'group' => 'System',
     );
   }
@@ -1470,6 +1470,132 @@ class DrupalRenderUnitTestCase extends DrupalWebTestCase {
     // Test that passing arguments to the theme function works.
     $this->assertEqual(drupal_render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
   }
+
+  /**
+   * Test rendering form elements without passing through form_builder().
+   */
+  function testDrupalRenderFormElements() {
+    // Define a series of form elements.
+    $element = array(
+      '#type' => 'button',
+      '#value' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'submit'));
+
+    $element = array(
+      '#type' => 'textfield',
+      '#title' => $this->randomName(),
+      '#value' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'text'));
+
+    $element = array(
+      '#type' => 'password',
+      '#title' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'password'));
+
+    $element = array(
+      '#type' => 'textarea',
+      '#title' => $this->randomName(),
+      '#value' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//textarea');
+
+    $element = array(
+      '#type' => 'radio',
+      '#title' => $this->randomName(),
+      '#value' => FALSE,
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'radio'));
+
+    $element = array(
+      '#type' => 'checkbox',
+      '#title' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'checkbox'));
+
+    $element = array(
+      '#type' => 'select',
+      '#title' => $this->randomName(),
+      '#options' => array(
+        0 => $this->randomName(),
+        1 => $this->randomName(),
+      ),
+    );
+    $this->assertRenderedElement($element, '//select');
+
+    $element = array(
+      '#type' => 'file',
+      '#title' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'file'));
+
+    $element = array(
+      '#type' => 'item',
+      '#title' => $this->randomName(),
+      '#markup' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//div[contains(@class, :class) and contains(., :markup)]/label[contains(., :label)]', array(
+      ':class' => 'form-type-item',
+      ':markup' => $element['#markup'],
+      ':label' => $element['#title'],
+    ));
+
+    $element = array(
+      '#type' => 'hidden',
+      '#title' => $this->randomName(),
+      '#value' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'hidden'));
+
+    $element = array(
+      '#type' => 'link',
+      '#title' => $this->randomName(),
+      '#href' => $this->randomName(),
+      '#options' => array(
+        'absolute' => TRUE,
+      ),
+    );
+    $this->assertRenderedElement($element, '//a[@href=:href and contains(., :title)]', array(
+      ':href' => url($element['#href'], array('absolute' => TRUE)),
+      ':title' => $element['#title'],
+    ));
+
+    $element = array(
+      '#type' => 'fieldset',
+      '#title' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//fieldset/legend[contains(., :title)]', array(
+      ':title' => $element['#title'],
+    ));
+
+    $element['item'] = array(
+      '#type' => 'item',
+      '#title' => $this->randomName(),
+      '#markup' => $this->randomName(),
+    );
+    $this->assertRenderedElement($element, '//fieldset/div/div[contains(@class, :class) and contains(., :markup)]', array(
+      ':class' => 'form-type-item',
+      ':markup' => $element['item']['#markup'],
+    ));
+  }
+
+  protected function assertRenderedElement(array $element, $xpath, array $xpath_args = array()) {
+    $original_element = $element;
+    $this->drupalSetContent(drupal_render($element));
+    $this->verbose('<pre>' .  check_plain(var_export($original_element, TRUE)) . '</pre>'
+      . '<pre>' .  check_plain(var_export($element, TRUE)) . '</pre>'
+      . '<hr />' . $this->drupalGetContent()
+    );
+
+    // @see DrupalWebTestCase::xpath()
+    $xpath = $this->buildXPathQuery($xpath, $xpath_args);
+    $element += array('#value' => NULL);
+    $this->assertFieldByXPath($xpath, $element['#value'], t('#type @type was properly rendered.', array(
+      '@type' => var_export($element['#type'], TRUE),
+    )));
+  }
 }
 
 /**
-- 
GitLab