Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.25
diff -u -p -r1.25 ajax.inc
--- includes/ajax.inc	27 Jan 2010 11:19:11 -0000	1.25
+++ includes/ajax.inc	28 Jan 2010 06:37:01 -0000
@@ -293,28 +293,12 @@ function ajax_get_form() {
  * The Form API #ajax property can be set both for buttons and other input
  * elements.
  *
- * ajax_process_form() defines an additional 'formPath' JavaScript setting
- * that is used by Drupal.ajax.prototype.beforeSubmit() to automatically inject
- * an additional field 'ajax_triggering_element' to the submitted form values,
- * which contains the array #parents of the element in the form structure.
- * This additional field allows ajax_form_callback() to determine which
- * element triggered the action, as non-submit form elements do not
- * provide this information in $form_state['clicked_button'], which can
- * also be used to determine triggering element, but only submit-type
- * form elements.
- *
  * This function is also the canonical example of how to implement
  * #ajax['path']. If processing is required that cannot be accomplished with
  * a callback, re-implement this function and set #ajax['path'] to the
  * enhanced function.
  */
 function ajax_form_callback() {
-  // Find the triggering element, which was set up for us on the client side.
-  if (!empty($_REQUEST['ajax_triggering_element'])) {
-    $triggering_element_path = $_REQUEST['ajax_triggering_element'];
-    // Remove the value for form validation.
-    unset($_REQUEST['ajax_triggering_element']);
-  }
   list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
 
   // Build, validate and if possible, submit the form.
@@ -324,32 +308,11 @@ function ajax_form_callback() {
   // drupal_process_form() set up.
   $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
 
-  // $triggering_element_path in a simple form might just be 'myselect', which
-  // would mean we should use the element $form['myselect']. For nested form
-  // elements we need to recurse into the form structure to find the triggering
-  // element, so we can retrieve the #ajax['callback'] from it.
-  if (!empty($triggering_element_path)) {
-    if (!isset($form['#access']) || $form['#access']) {
-      $triggering_element = $form;
-      foreach (explode('/', $triggering_element_path) as $key) {
-        if (!empty($triggering_element[$key]) && (!isset($triggering_element[$key]['#access']) || $triggering_element[$key]['#access'])) {
-          $triggering_element = $triggering_element[$key];
-        }
-        else {
-          // We did not find the $triggering_element or do not have #access,
-          // so break out and do not provide it.
-          $triggering_element = NULL;
-          break;
-        }
-      }
-    }
-  }
-  if (empty($triggering_element)) {
-    $triggering_element = $form_state['clicked_button'];
-  }
-  // Now that we have the element, get a callback if there is one.
-  if (!empty($triggering_element)) {
-    $callback = $triggering_element['#ajax']['callback'];
+  // As part of drupal_process_form(), the element that triggered the form
+  // submission is determined, and in the case of AJAX, it might not be a
+  // button. This lets us route to the appropriate callback.
+  if (!empty($form_state['triggering_element'])) {
+    $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
   if (!empty($callback) && function_exists($callback)) {
     return $callback($form, $form_state);
@@ -483,13 +446,48 @@ function ajax_process_form($element, &$f
       'speed' => 'none',
       'method' => 'replace',
       'progress' => array('type' => 'throbber'),
-      'formPath' => implode('/', $element['#array_parents']),
     );
 
-    // Process special settings.
+    // Change path to url.
     $settings['url'] = isset($settings['path']) ? url($settings['path']) : url('system/ajax');
     unset($settings['path']);
-    $settings['button'] = isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE;
+
+    // Add special data to $settings['submit'] so that when this element
+    // triggers an AJAX submission, Drupal's form processing can determine which
+    // element triggered it.
+    if (isset($settings['trigger_as'])) {
+      // An element can add a 'trigger_as' key within #ajax to make the element
+      // submit as though another one (for example, a non-button can use this
+      // to submit the form as though a button were clicked). To emulate a
+      // button, this should be set to an array with the button name as the key
+      // and the button value as the value. To emulate a non-button or image
+      // button, this should be set to just the element's name.
+      if (is_array($settings['trigger_as'])) {
+        // There should only be one key/value pair, but PHP doesn't provide a
+        // convenient syntax for getting them other than with a foreach
+        // statement.
+        foreach ($settings['trigger_as'] as $name => $value) {
+          $settings['submit']['_triggering_element_name'] = $name;
+          $settings['submit']['_triggering_element_value'] = $value;
+          break;
+        }
+      }
+      else {
+        $settings['submit']['_triggering_element_name'] = $settings['trigger_as'];
+      }
+    }
+    else {
+      // Most of the time, elements can submit as themselves, in which case the
+      // 'trigger_as' key isn't needed, and the element's name is used.
+      $settings['submit']['_triggering_element_name'] = $element['#name'];
+      // If the element is a (non-image) button, its name may not identify it
+      // uniquely, in which case a match on value is also needed. @see
+      // _form_button_was_clicked().
+      if (isset($element['#button_type']) && empty($element['#has_garbage_value'])) {
+        $settings['submit']['_triggering_element_value'] = $element['#value'];
+      }
+    }
+    unset($settings['trigger_as']);
 
     // Convert a simple #ajax['progress'] string into an array.
     if (is_string($settings['progress'])) {
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.427
diff -u -p -r1.427 form.inc
--- includes/form.inc	25 Jan 2010 10:38:34 -0000	1.427
+++ includes/form.inc	28 Jan 2010 06:37:01 -0000
@@ -286,13 +286,13 @@ function form_state_defaults() {
 /**
  * Retrieves a form, caches it and processes it again.
  *
- * If your AHAH callback simulates the pressing of a button, then your AHAH
- * callback will need to do the same as what drupal_get_form would do when the
+ * If your AJAX callback simulates the pressing of a button, then your AJAX
+ * callback will need to do the same as what drupal_get_form() would do when the
  * button is pressed: get the form from the cache, run drupal_process_form over
- * it and then if it needs rebuild, run drupal_rebuild_form over it. Then send
+ * it and then if it needs rebuild, run drupal_rebuild_form() over it. Then send
  * back a part of the returned form.
- * $form_state['clicked_button']['#array_parents'] will help you to find which
- * part.
+ * $form_state['triggering_element']['#array_parents'] will help you to find
+ * which part.
  *
  * @param $form_id
  *   The unique string identifying the desired form. If a function
@@ -405,6 +405,7 @@ function form_state_keys_no_cache() {
     'temporary',
     // Internal properties defined by form processing.
     'buttons',
+    'triggering_element',
     'clicked_button',
     'complete form',
     'groups',
@@ -936,14 +937,29 @@ function _form_validate(&$elements, &$fo
     // to form_set_error() be suppressed and not result in a form error, so
     // that a button that implements low-risk functionality (such as "Previous"
     // or "Add more") that doesn't require all user input to be valid can still
-    // have its submit handlers triggered. The clicked button's
+    // have its submit handlers triggered. The triggering element's
     // #limit_validation_errors property contains the information for which
     // errors are needed, and all other errors are to be suppressed. The
-    // #limit_validation_errors property is ignored if the button doesn't also
-    // define its own submit handlers, because it's too large a security risk to
-    // have any invalid user input when executing form-level submit handlers.
-    if (isset($form_state['clicked_button']['#limit_validation_errors']) && isset($form_state['clicked_button']['#submit'])) {
-      form_set_error(NULL, '', $form_state['clicked_button']['#limit_validation_errors']);
+    // #limit_validation_errors property is ignored if the element has
+    // #executes_submit_callback set to TRUE but doesn't have a #submit
+    // property, because it's too large a security risk to have any invalid user
+    // input when executing form-level submit handlers.
+    if (isset($form_state['triggering_element']['#limit_validation_errors']) && (empty($form_state['triggering_element']['#executes_submit_callback']) || isset($form_state['triggering_element']['#submit']))) {
+      form_set_error(NULL, '', $form_state['triggering_element']['#limit_validation_errors']);
+    }
+    elseif (!empty($form_state['triggering_element']) && !isset($form_state['triggering_element']['#executes_submit_callback'])) {
+      // By default, buttons have either TRUE or FALSE for
+      // #executes_submit_callback, while non-buttons have NULL for it. A
+      // non-button can set #executes_submit_callback to either TRUE or FALSE to
+      // behave like a button and trigger validation as buttons do. But if it
+      // leaves #executes_submit_callback as NULL and doesn't set
+      // #limit_validation_errors, then by default we suppress all validation
+      // errors, which is safe, because without #executes_submit_callback of
+      // TRUE, no submit handlers will execute. A non-button that wants some
+      // validation to occur must either set #limit_validation_errors or
+      // #executes_submit_callback (to either TRUE or FALSE) in order to
+      // override this default behavior of suppressing all validation errors.
+      form_set_error(NULL, '', array());
     }
     else {
       // As an extra security measure, explicitly turn off error suppression.
@@ -1303,22 +1319,50 @@ function form_builder($form_id, $element
     $element['#after_build_done'] = TRUE;
   }
 
-  // Now that we've processed everything, we can go back to handle the funky
-  // Internet Explorer button-click scenario.
-  _form_builder_ie_cleanup($element, $form_state);
-
   // If there is a file element, we need to flip a flag so later the
   // form encoding can be set.
   if (isset($element['#type']) && $element['#type'] == 'file') {
     $form_state['has_file_element'] = TRUE;
   }
 
+  // Final stuff for the form element after form_builder() has run for all other
+  // elements.
   if (isset($element['#type']) && $element['#type'] == 'form') {
-    // We are on the top form.
     // If there is a file element, we set the form encoding.
     if (isset($form_state['has_file_element'])) {
       $element['#attributes']['enctype'] = 'multipart/form-data';
     }
+
+    // Now that we've processed everything, we can go back to handle the funky
+    // Internet Explorer button-click scenario.
+    _form_builder_ie_cleanup($element, $form_state);
+
+    // If the triggering element specifies "button-level" validation and submit
+    // handlers to run instead of the default form-level ones, then add those to
+    // the form state.
+    foreach (array('validate', 'submit') as $type) {
+      if (isset($form_state['triggering_element']['#' . $type])) {
+        $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
+      }
+    }
+
+    // If the triggering element executes submit handlers, then set the form
+    // state key that's needed for those handlers to run.
+    if (!empty($form_state['triggering_element']['#executes_submit_callback'])) {
+      $form_state['submitted'] = TRUE;
+    }
+
+    // Special processing if the triggering element is a button.
+    if (isset($form_state['triggering_element']['#button_type'])) {
+      // Buttons have flat (without brackets) #name, regardless of their
+      // #parents property.
+      $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value'];
+      // Lots of Drupal core code and contributed modules access the
+      // $form_state['clicked_button'] variable instead of the newer
+      // $form_state['triggering_element'] variable.
+      $form_state['clicked_button'] = $form_state['triggering_element'];
+    }
+
     // Update the copy of the complete form for usage in validation handlers.
     $form_state['complete form'] = $element;
   }
@@ -1415,33 +1459,33 @@ function _form_builder_handle_input_elem
     }
   }
 
-  // Determine which button (if any) was clicked to submit the form.
-  // We compare the incoming values with the buttons defined in the form,
-  // and flag the one that matches. We have to do some funky tricks to
-  // deal with Internet Explorer's handling of single-button forms, though.
-  if (!empty($form_state['input']) && isset($element['#executes_submit_callback'])) {
-    // First, accumulate a collection of buttons, divided into two bins:
-    // those that execute full submit callbacks and those that only validate.
-    $button_type = $element['#executes_submit_callback'] ? 'submit' : 'button';
-    $form_state['buttons'][$button_type][] = $element;
-
-    if (_form_button_was_clicked($element, $form_state)) {
-      $form_state['submitted'] = $form_state['submitted'] || $element['#executes_submit_callback'];
-
-      // In most cases, we want to use form_set_value() to manipulate
-      // the global variables. In this special case, we want to make sure that
-      // the value of this element is listed in $form_variables under 'op'.
-      $form_state['values'][$element['#name']] = $element['#value'];
-      $form_state['clicked_button'] = $element;
-
-      if (isset($element['#validate'])) {
-        $form_state['validate_handlers'] = $element['#validate'];
-      }
-      if (isset($element['#submit'])) {
-        $form_state['submit_handlers'] = $element['#submit'];
+  // Determine which element (if any) triggered the submission of the form.
+  if (!empty($form_state['input']) && (!isset($element['#access']) || $element['#access'])) {
+    // If the form was submitted with AJAX or some other scriptable environment,
+    // then the special input key _triggering_element_name identifies the
+    // element. In some cases (mostly, buttons), the name alone is insufficient
+    // to identify the element uniquely, so a match on _triggering_element_value
+    // is also needed.
+    if (!empty($form_state['input']['_triggering_element_name']) && $element['#name'] == $form_state['input']['_triggering_element_name']) {
+      if (empty($form_state['input']['_triggering_element_value']) || $form_state['input']['_triggering_element_value'] == $element['#value']) {
+        $form_state['triggering_element'] = $element;
+      }
+    }
+    // If the form was submitted by the browser rather than via AJAX, then it
+    // can only have been triggered by a button, and we need to determine which
+    // button within the constraints of how browsers provide this information.
+    // To deal with Internet Explorer's handling of single-button forms, we need
+    // to put all the buttons into two bins and call _form_builder_ie_cleanup()
+    // after the entire form has been processed.
+    if (isset($element['#button_type'])) {
+      $button_type = $element['#executes_submit_callback'] ? 'submit' : 'button';
+      $form_state['buttons'][$button_type][] = $element;
+      if (_form_button_was_clicked($element, $form_state)) {
+        $form_state['triggering_element'] = $element;
       }
     }
   }
+
   form_set_value($element, $element['#value'], $form_state);
 }
 
@@ -1483,23 +1527,11 @@ function _form_button_was_clicked($form,
  * as 'clicked' if they are met.
  */
 function _form_builder_ie_cleanup($form, &$form_state) {
-  // Quick check to make sure we're always looking at the full form
-  // and not a sub-element.
-  if (!empty($form['#type']) && $form['#type'] == 'form') {
-    // If we haven't recognized a submission yet, and there's a single
-    // submit button, we know that we've hit the right conditions. Grab
-    // the first one and treat it as the clicked button.
-    if (empty($form_state['submitted']) && !empty($form_state['buttons']['submit']) && empty($form_state['buttons']['button'])) {
-      $button = $form_state['buttons']['submit'][0];
-
-      // Set up all the $form_state information that would have been
-      // populated had the button been recognized earlier.
-      $form_state['submitted'] = TRUE;
-      $form_state['submit_handlers'] = empty($button['#submit']) ? NULL : $button['#submit'];
-      $form_state['validate_handlers'] = empty($button['#validate']) ? NULL : $button['#validate'];
-      $form_state['values'][$button['#name']] = $button['#value'];
-      $form_state['clicked_button'] = $button;
-    }
+  // If we haven't recognized a submission yet, and there's a single
+  // submit button, we know that we've hit the right conditions. Grab
+  // the first one and treat it as the clicked button.
+  if (empty($form_state['submitted']) && !isset($form_state['triggering_element']) && !empty($form_state['buttons']['submit']) && empty($form_state['buttons']['button'])) {
+    $form_state['triggering_element'] = $form_state['buttons']['submit'][0];
   }
 }
 
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.9
diff -u -p -r1.9 ajax.js
--- misc/ajax.js	12 Jan 2010 06:31:22 -0000	1.9
+++ misc/ajax.js	28 Jan 2010 06:37:02 -0000
@@ -98,7 +98,7 @@ Drupal.ajax = function (base, element, e
       type: 'bar',
       message: 'Please wait...'
     },
-    button: {}
+    submit: {}
   };
 
   $.extend(this, defaults, element_settings);
@@ -121,7 +121,7 @@ Drupal.ajax = function (base, element, e
   var ajax = this;
   var options = {
     url: ajax.url,
-    data: ajax.button,
+    data: ajax.submit,
     beforeSerialize: function (element_settings, options) {
       return ajax.beforeSerialize(element_settings, options);
     },
@@ -200,10 +200,6 @@ Drupal.ajax.prototype.beforeSubmit = fun
   // Disable the element that received the change.
   $(this.element).addClass('progress-disabled').attr('disabled', true);
 
-  // Server-side code needs to know what element triggered the call, so it can
-  // find the #ajax binding.
-  form_values.push({ name: 'ajax_triggering_element', value: this.formPath });
-
   // Insert progressbar or throbber.
   if (this.progress.type == 'bar') {
     var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.193
diff -u -p -r1.193 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	15 Jan 2010 03:07:34 -0000	1.193
+++ modules/simpletest/drupal_web_test_case.php	28 Jan 2010 06:37:03 -0000
@@ -1519,11 +1519,13 @@ class DrupalWebTestCase extends DrupalTe
    *     which is likely different than the $path parameter used for retrieving
    *     the initial form. Defaults to 'system/ajax'.
    *   - triggering_element: If the value for the 'path' key is 'system/ajax' or
-   *     another generic AJAX processing path, this needs to be set to the '/'
-   *     separated path to the element within the server's cached $form array.
-   *     The callback for the generic AJAX processing path uses this to find
-   *     the #ajax information for the element, including which specific
-   *     callback to use for processing the request.
+   *     another generic AJAX processing path, this needs to be set to the name
+   *     of the element. If the name doesn't identify the element uniquely, then
+   *     this should instead be an array with a single key/value pair,
+   *     corresponding to the element name and value. The callback for the
+   *     generic AJAX processing path uses this to find the #ajax information
+   *     for the element, including which specific callback to use for
+   *     processing the request.
    * @param $options
    *   Options to be forwarded to url().
    * @param $headers
@@ -1578,7 +1580,19 @@ class DrupalWebTestCase extends DrupalTe
               $post[$key] = urlencode($key) . '=' . urlencode($value);
             }
             if ($ajax && isset($submit['triggering_element'])) {
-              $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($submit['triggering_element']);
+              if (is_array($submit['triggering_element'])) {
+                // There should only be one key/value pair, but PHP doesn't
+                // provide a convenient syntax for getting them other than with
+                // a foreach statement.
+                foreach ($submit['triggering_element'] as $name => $value) {
+                  $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode($name);
+                  $post['_triggering_element_value'] = '_triggering_element_value=' . urlencode($value);
+                  break;
+                }
+              }
+              else {
+                $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode($submit['triggering_element']);
+              }
             }
             $post = implode('&', $post);
           }
Index: modules/simpletest/tests/ajax_forms_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_forms_test.module,v
retrieving revision 1.3
diff -u -p -r1.3 ajax_forms_test.module
--- modules/simpletest/tests/ajax_forms_test.module	12 Dec 2009 23:36:28 -0000	1.3
+++ modules/simpletest/tests/ajax_forms_test.module	28 Jan 2010 06:37:03 -0000
@@ -92,6 +92,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the 'after' command with a callback generating commands.
   $form['after_command_example'] = array(
+    '#name' => 'after_command_example',
     '#value' => t("AJAX 'After': Click to put something after the div"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -102,6 +103,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the 'alert' command.
   $form['alert_command_example'] = array(
+    '#name' => 'alert_command_example',
     '#value' => t("AJAX 'Alert': Click to alert"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -111,6 +113,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the 'append' command.
   $form['append_command_example'] = array(
+    '#name' => 'append_command_example',
     '#value' => t("AJAX 'Append': Click to append something"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -122,6 +125,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the 'before' command.
   $form['before_command_example'] = array(
+    '#name' => 'before_command_example',
     '#value' => t("AJAX 'before': Click to put something before the div"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -132,6 +136,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the 'changed' command without asterisk.
   $form['changed_command_example'] = array(
+    '#name' => 'changed_command_example',
     '#value' => t("AJAX changed: Click to mark div changed."),
     '#type' => 'submit',
     '#ajax' => array(
@@ -141,6 +146,7 @@ function ajax_forms_test_ajax_commands_f
   );
   // Shows the 'changed' command adding the asterisk.
   $form['changed_command_asterisk_example'] = array(
+    '#name' => 'changed_command_asterisk_example',
     '#value' => t("AJAX changed: Click to mark div changed with asterisk."),
     '#type' => 'submit',
     '#ajax' => array(
@@ -150,6 +156,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the AJAX 'css' command.
   $form['css_command_example'] = array(
+    '#name' => 'css_command_example',
     '#value' => t("Set the the '#box' div to be blue."),
     '#type' => 'submit',
     '#ajax' => array(
@@ -162,6 +169,7 @@ function ajax_forms_test_ajax_commands_f
   // Shows the AJAX 'data' command. But there is no use of this information,
   // as this would require a javascript client to use the data.
   $form['data_command_example'] = array(
+    '#name' => 'data_command_example',
     '#value' => t("AJAX data command: Issue command."),
     '#type' => 'submit',
     '#ajax' => array(
@@ -172,6 +180,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the AJAX 'html' command.
   $form['html_command_example'] = array(
+    '#name' => 'html_command_example',
     '#value' => t("AJAX html: Replace the HTML in a selector."),
     '#type' => 'submit',
     '#ajax' => array(
@@ -182,6 +191,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the AJAX 'prepend' command.
   $form['prepend_command_example'] = array(
+    '#name' => 'prepend_command_example',
     '#value' => t("AJAX 'prepend': Click to prepend something"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -192,6 +202,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Shows the AJAX 'remove' command.
   $form['remove_command_example'] = array(
+    '#name' => 'remove_command_example',
     '#value' => t("AJAX 'remove': Click to remove text"),
     '#type' => 'submit',
     '#ajax' => array(
@@ -202,6 +213,7 @@ function ajax_forms_test_ajax_commands_f
 
   // Show off the AJAX 'restripe' command.
   $form['restripe_command_example'] = array(
+    '#name' => 'restripe_command_example',
     '#type' => 'submit',
     '#value' => t("AJAX 'restripe' command"),
     '#ajax' => array(
