summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDries Buytaert2010-07-02 12:37:57 (GMT)
committerDries Buytaert2010-07-02 12:37:57 (GMT)
commitf3304854d0e10763937b02aa4bc7f454e9455b9b (patch)
treec586d7b8bd4771bdc54cd0932127416477df2819
parent5b55646e2a5266c084d5e4af77ecb0d63d648d50 (diff)
- Patch #736298 by effulgentsia, eojthebrave, andypost, robeano: fixed Refactor file.module to use proper button click detection, enabling FAPI to improve button click detection security.
-rw-r--r--includes/form.inc124
-rw-r--r--modules/file/file.field.inc37
-rw-r--r--modules/file/file.module100
-rw-r--r--modules/file/tests/file.test80
-rw-r--r--modules/simpletest/tests/form.test92
-rw-r--r--modules/simpletest/tests/form_test.module5
6 files changed, 333 insertions, 105 deletions
diff --git a/includes/form.inc b/includes/form.inc
index 3d60afe..e33a19d 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -1337,10 +1337,83 @@ function form_error(&$element, $message = '') {
}
/**
- * Walk through the structured form array, adding any required
- * properties to each element and mapping the incoming input
- * data to the proper elements. Also, execute any #process handlers
- * attached to a specific element.
+ * Walk through the structured form array, adding any required properties to
+ * each element and mapping the incoming input data to the proper elements.
+ * Also, execute any #process handlers attached to a specific element.
+ *
+ * This is one of the three primary functions that recursively iterates a form
+ * array. This one does it for completing the form building process. The other
+ * two are _form_validate() (invoked via drupal_validate_form() and used to
+ * invoke validation logic for each element) and drupal_render() (for rendering
+ * each element). Each of these three pipelines provides ample opportunity for
+ * modules to customize what happens. For example, during this function's life
+ * cycle, the following functions get called for each element:
+ * - $element['#value_callback']: A function that implements how user input is
+ * mapped to an element's #value property. This defaults to a function named
+ * 'form_type_TYPE_value' where TYPE is $element['#type'].
+ * - $element['#process']: An array of functions called after user input has
+ * been mapped to the element's #value property. These functions can be used
+ * to dynamically add child elements: for example, for the 'date' element
+ * type, one of the functions in this array is form_process_date(), which adds
+ * the individual 'year', 'month', 'day', etc. child elements. These functions
+ * can also be used to set additional properties or implement special logic
+ * other than adding child elements: for example, for the 'fieldset' element
+ * type, one of the functions in this array is form_process_fieldset(), which
+ * adds the attributes and JavaScript needed to make the fieldset collapsible
+ * if the #collapsible property is set. The #process functions are called in
+ * preorder traversal, meaning they are called for the parent element first,
+ * then for the child elements.
+ * - $element['#after_build']: An array of functions called after form_builder()
+ * is done with its processing of the element. These are called in postorder
+ * traversal, meaning they are called for the child elements first, then for
+ * the parent element.
+ * There are similar properties containing callback functions invoked by
+ * _form_validate() and drupal_render(), appropriate for those operations.
+ *
+ * Developers are strongly encouraged to integrate the functionality needed by
+ * their form or module within one of these three pipelines, using the
+ * appropriate callback property, rather than implementing their own recursive
+ * traversal of a form array. This facilitates proper integration between
+ * multiple modules. For example, module developers are familiar with the
+ * relative order in which hook_form_alter() implementations and #process
+ * functions run. A custom traversal function that affects the building of a
+ * form is likely to not integrate with hook_form_alter() and #process in the
+ * expected way. Also, deep recursion within PHP is both slow and memory
+ * intensive, so it is best to minimize how often it's done.
+ *
+ * As stated above, each element's #process functions are executed after its
+ * #value has been set. This enables those functions to execute conditional
+ * logic based on the current value. However, all of form_builder() runs before
+ * drupal_validate_form() is called, so during #process function execution, the
+ * element's #value has not yet been validated, so any code that requires
+ * validated values must reside within a submit handler.
+ *
+ * As a security measure, user input is used for an element's #value only if the
+ * element exists within $form, is not disabled (as per the #disabled property),
+ * and can be accessed (as per the #access property, except that forms submitted
+ * using drupal_form_submit() bypass #access restrictions). When user input is
+ * ignored due to #disabled and #access restrictions, the element's default
+ * value is used.
+ *
+ * Because of the preorder traversal, where #process functions of an element run
+ * before user input for its child elements is processed, and because of the
+ * Form API security of user input processing with respect to #access and
+ * #disabled described above, this generally means that #process functions
+ * should not use an element's (unvalidated) #value to affect the #disabled or
+ * #access of child elements. Use-cases where a developer may be tempted to
+ * implement such conditional logic usually fall into one of two categories:
+ * - Where user input from the current submission must affect the structure of a
+ * form, including properties like #access and #disabled that affect how the
+ * next submission needs to be processed, a multi-step workflow is needed.
+ * This is most commonly implemented with a submit handler setting persistent
+ * data within $form_state based on *validated* values in
+ * $form_state['values'] and setting $form_state['rebuild']. The form building
+ * functions must then be implmented to use the $form_state data to rebuild
+ * the form with the structure appropriate for the new state.
+ * - Where user input must affect the rendering of the form without affecting
+ * its structure, the necessary conditional rendering logic should reside
+ * within functions that run during the rendering phase (#pre_render, #theme,
+ * #theme_wrappers, and #post_render).
*
* @param $form_id
* A unique string identifying the form for validation, submission,
@@ -1572,19 +1645,20 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
}
}
+ // With JavaScript or other easy hacking, input can be submitted even for
+ // elements with #access=FALSE or #disabled=TRUE. For security, these must
+ // not be processed. Forms that set #disabled=TRUE on an element do not
+ // expect input for the element, and even forms submitted with
+ // drupal_form_submit() must not be able to get around this. Forms that set
+ // #access=FALSE on an element usually allow access for some users, so forms
+ // submitted with drupal_form_submit() may bypass access restriction and be
+ // treated as high-privelege users instead.
+ $process_input = empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])));
+
// Set the element's #value property.
if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
$value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
-
- // With JavaScript or other easy hacking, input can be submitted even for
- // elements with #access=FALSE or #disabled=TRUE. For security, these must
- // not be processed. Forms that set #disabled=TRUE on an element do not
- // expect input for the element, and even forms submitted with
- // drupal_form_submit() must not be able to get around this. Forms that set
- // #access=FALSE on an element usually allow access for some users, so forms
- // submitted with drupal_form_submit() may bypass access restriction and be
- // treated as high-privelege users instead.
- if (empty($element['#disabled']) && ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access'])))) {
+ if ($process_input) {
// Get the input for the current element. NULL values in the input need to
// be explicitly distinguished from missing input. (see below)
$input = $form_state['input'];
@@ -1647,18 +1721,10 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
}
// Determine which element (if any) triggered the submission of the form and
- // keep track of all the buttons in the form for form_state_values_clean().
- // @todo We need to add a #access check here, so that someone can't fake the
- // click of a button they shouldn't have access to, but first we need to
- // fix file.module's managed_file element pipeline to handle the click of
- // the remove button in a submit handler instead of in a #process function.
- // During the first run of form_builder() after the form is submitted,
- // #process functions need to return the expanded element with child
- // elements' #access properties matching what they were when the form was
- // displayed to the user, since that is what we are processing input for.
- // Changes to the form (like toggling the upload/remove button) need to wait
- // until form rebuild: http://drupal.org/node/736298.
- if (!empty($form_state['input'])) {
+ // keep track of all the clickable buttons in the form for
+ // form_state_values_clean(). Enforce the same input processing restrictions
+ // as above.
+ if ($process_input) {
// Detect if the element triggered the submission via AJAX.
if (_form_element_triggered_scripted_submission($element, $form_state)) {
$form_state['triggering_element'] = $element;
@@ -1671,11 +1737,7 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) {
// All buttons in the form need to be tracked for
// form_state_values_clean() and for the form_builder() code that handles
// a form submission containing no button information in $_POST.
- // @todo When #access is checked in an outer if statement (see above), it
- // won't need to be checked here.
- if ($form_state['programmed'] || !isset($element['#access']) || $element['#access']) {
- $form_state['buttons'][] = $element;
- }
+ $form_state['buttons'][] = $element;
if (_form_button_was_clicked($element, $form_state)) {
$form_state['triggering_element'] = $element;
}
diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc
index 12420d8..e0d452b 100644
--- a/modules/file/file.field.inc
+++ b/modules/file/file.field.inc
@@ -767,32 +767,45 @@ function theme_file_widget_multiple($variables) {
continue;
}
- // Render all the buttons in the field as an "operation".
- $operations = '';
+ // Delay rendering of the buttons, so that they can be rendered later in the
+ // "operations" column.
+ $operations_elements = array();
foreach (element_children($element[$key]) as $sub_key) {
if (isset($element[$key][$sub_key]['#type']) && $element[$key][$sub_key]['#type'] == 'submit') {
- $operations .= drupal_render($element[$key][$sub_key]);
+ hide($element[$key][$sub_key]);
+ $operations_elements[] = &$element[$key][$sub_key];
}
}
- // Render the "Display" option in its own own column.
+ // Delay rendering of the "Display" option and the weight selector, so that
+ // each can be rendered later in its own column.
+ if ($element['#display_field']) {
+ hide($element[$key]['display']);
+ }
+ hide($element[$key]['_weight']);
+
+ // Render everything else together in a column, without the normal wrappers.
+ $element[$key]['#theme_wrappers'] = array();
+ $information = drupal_render($element[$key]);
+
+ // Render the previously hidden elements, using render() instead of
+ // drupal_render(), to undo the earlier hide().
+ $operations = '';
+ foreach ($operations_elements as $operation_element) {
+ $operations .= render($operation_element);
+ }
$display = '';
if ($element['#display_field']) {
unset($element[$key]['display']['#title']);
$display = array(
- 'data' => drupal_render($element[$key]['display']),
+ 'data' => render($element[$key]['display']),
'class' => array('checkbox'),
);
}
-
- // Render the weight in its own column.
$element[$key]['_weight']['#attributes']['class'] = array($weight_class);
- $weight = drupal_render($element[$key]['_weight']);
-
- // Render everything else together in a column, without the normal wrappers.
- $element[$key]['#theme_wrappers'] = array();
- $information = drupal_render($element[$key]);
+ $weight = render($element[$key]['_weight']);
+ // Arrange the row with all of the rendered columns.
$row = array();
$row[] = $information;
if ($element['#display_field']) {
diff --git a/modules/file/file.module b/modules/file/file.module
index 2f73d72..8d923ac 100644
--- a/modules/file/file.module
+++ b/modules/file/file.module
@@ -65,6 +65,7 @@ function file_element_info() {
'#process' => array('file_managed_file_process'),
'#value_callback' => 'file_managed_file_value',
'#element_validate' => array('file_managed_file_validate'),
+ '#pre_render' => array('file_managed_file_pre_render'),
'#theme' => 'file_managed_file',
'#theme_wrappers' => array('form_element'),
'#progress_indicator' => 'throbber',
@@ -384,25 +385,6 @@ function file_managed_file_process($element, &$form_state, $form) {
'#weight' => -5,
);
- // @todo It is not good to call these private functions. This should be
- // refactored so that the file deletion happens during a submit handler,
- // and form changes affected by that (such as toggling the upload and remove
- // buttons) happens during the 2nd run of this function that is triggered by
- // a form rebuild: http://drupal.org/node/736298.
- if (_form_button_was_clicked($element['remove_button'], $form_state) || _form_element_triggered_scripted_submission($element['remove_button'], $form_state)) {
- // If it's a temporary file we can safely remove it immediately, otherwise
- // it's up to the implementing module to clean up files that are in use.
- if ($element['#file'] && $element['#file']->status == 0) {
- file_delete($element['#file']);
- }
- $element['#file'] = FALSE;
- $fid = 0;
- }
-
- // Set access on the buttons.
- $element['upload_button']['#access'] = empty($fid);
- $element['remove_button']['#access'] = !empty($fid);
-
$element['fid'] = array(
'#type' => 'hidden',
'#value' => $fid,
@@ -436,7 +418,6 @@ function file_managed_file_process($element, &$form_state, $form) {
'#name' => 'files[' . implode('_', $element['#parents']) . ']',
'#type' => 'file',
'#size' => 22,
- '#access' => empty($fid),
'#theme_wrappers' => array(),
'#weight' => -10,
);
@@ -565,13 +546,50 @@ function file_managed_file_validate(&$element, &$form_state) {
}
/**
- * Submit handler for non-JavaScript uploads.
+ * Submit handler for upload and remove buttons of managed_file elements.
*/
function file_managed_file_submit($form, &$form_state) {
- // Do not redirect and leave the page after uploading a file. This keeps
- // all the current form values in place. The file is saved by the
- // #value_callback on the form element.
- $form_state['redirect'] = FALSE;
+ // Determine whether it was the upload or the remove button that was clicked,
+ // and set $element to the managed_file element that contains that button.
+ $parents = $form_state['triggering_element']['#array_parents'];
+ $button_key = array_pop($parents);
+ $element = $form;
+ foreach ($parents as $parent) {
+ $element = $element[$parent];
+ }
+
+ // No action is needed here for the upload button, because all file uploads on
+ // the form are processed by file_managed_file_value() regardless of which
+ // button was clicked. Action is needed here for the remove button, because we
+ // only remove a file in response to its remove button being clicked.
+ if ($button_key == 'remove_button') {
+ // If it's a temporary file we can safely remove it immediately, otherwise
+ // it's up to the implementing module to clean up files that are in use.
+ if ($element['#file'] && $element['#file']->status == 0) {
+ file_delete($element['#file']);
+ }
+ // Update both $form_state['values'] and $form_state['input'] to reflect
+ // that the file has been removed, so that the form is rebuilt correctly.
+ // $form_state['values'] must be updated in case additional submit handlers
+ // run, and for form building functions that run during the rebuild, such as
+ // when the managed_file element is part of a field widget.
+ // $form_state['input'] must be updated so that file_managed_file_value()
+ // has correct information during the rebuild. The Form API provides no
+ // equivalent of form_set_value() for updating $form_state['input'], so
+ // inline that implementation with the same logic that form_set_value()
+ // uses.
+ $values_element = $element['#extended'] ? $element['fid'] : $element;
+ form_set_value($values_element, NULL, $form_state);
+ _form_set_value($form_state['input'], $values_element, $values_element['#parents'], NULL);
+ }
+
+ // Set the form to rebuild so that $form is correctly updated in response to
+ // processing the file removal. Since this function did not change $form_state
+ // if the upload button was clicked, a rebuild isn't necessary in that
+ // situation and setting $form_state['redirect'] to FALSE would suffice.
+ // However, we choose to always rebuild, to keep the form processing workflow
+ // consistent between the two buttons.
+ $form_state['rebuild'] = TRUE;
}
/**
@@ -626,6 +644,38 @@ function theme_file_managed_file($variables) {
}
/**
+ * #pre_render callback to hide display of the upload or remove controls.
+ *
+ * Upload controls are hidden when a file is already uploaded. Remove controls
+ * are hidden when there is no file attached. Controls are hidden here instead
+ * of in file_managed_file_process(), because #access for these buttons depends
+ * on the managed_file element's #value. See the documentation of form_builder()
+ * for more detailed information about the relationship between #process,
+ * #value, and #access.
+ *
+ * Because #access is set here, it affects display only and does not prevent
+ * JavaScript or other untrusted code from submitting the form as though access
+ * were enabled. The form processing functions for these elements should not
+ * assume that the buttons can't be "clicked" just because they are not
+ * displayed.
+ *
+ * @see file_managed_file_process()
+ * @see form_builder()
+ */
+function file_managed_file_pre_render($element) {
+ // If we already have a file, we don't want to show the upload controls.
+ if (!empty($element['#value']['fid'])) {
+ $element['upload']['#access'] = FALSE;
+ $element['upload_button']['#access'] = FALSE;
+ }
+ // If we don't already have a file, there is nothing to remove.
+ else {
+ $element['remove_button']['#access'] = FALSE;
+ }
+ return $element;
+}
+
+/**
* Returns HTML for a link to a file.
*
* @param $variables
diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test
index b228cb3..ee01ffa 100644
--- a/modules/file/tests/file.test
+++ b/modules/file/tests/file.test
@@ -14,7 +14,7 @@ class FileFieldTestCase extends DrupalWebTestCase {
function setUp() {
parent::setUp('file');
- $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content'));
+ $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer content types', 'administer nodes', 'bypass node access'));
$this->drupalLogin($this->admin_user);
}
@@ -202,6 +202,84 @@ class FileFieldTestCase extends DrupalWebTestCase {
}
}
+
+/**
+ * Test class to test file field upload and remove buttons, with and without AJAX.
+ */
+class FileFieldWidgetTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field widget test',
+ 'description' => 'Test upload and remove buttons, with and without AJAX.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests upload and remove buttons, with and without AJAX.
+ *
+ * @todo This function currently only tests the "remove" button of a single-
+ * valued field. Tests should be added for the "upload" button and for each
+ * button of a multi-valued field. Tests involving multiple AJAX steps on
+ * the same page will become easier after http://drupal.org/node/789186
+ * lands. Testing the "upload" button in AJAX context requires more
+ * investigation into how jQuery uploads files, so that drupalPostAJAX() can
+ * emulate that correctly.
+ */
+ function testWidget() {
+ // Use 'page' instead of 'article', so that the 'article' image field does
+ // not conflict with this test. If in the future the 'page' type gets its
+ // own default file or image field, this test can be made more robust by
+ // using a custom node type.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('text');
+
+ foreach (array('nojs', 'js') as $type) {
+ // Create a new node with the uploaded file and ensure it got uploaded
+ // successfully.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, t('New file saved to disk on node creation.'));
+
+ // Ensure the edit page has a remove button instead of an upload button.
+ $this->drupalGet("node/$nid/edit");
+ $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), t('Node with file does not display the "Upload" button.'));
+ $this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), t('Node with file displays the "Remove" button.'));
+
+ // "Click" the remove button (emulating either a nojs or js submission).
+ switch ($type) {
+ case 'nojs':
+ $this->drupalPost(NULL, array(), t('Remove'));
+ break;
+ case 'js':
+ // @todo This can be simplified after http://drupal.org/node/789186
+ // lands.
+ preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches);
+ $settings = drupal_json_decode($matches[1]);
+ $button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
+ $button_id = (string) $button[0]['id'];
+ $this->drupalPostAJAX(NULL, array(), array((string) $button[0]['name'] => (string) $button[0]['value']), $settings['ajax'][$button_id]['url'], array(), array(), NULL, $settings['ajax'][$button_id]);
+ break;
+ }
+
+ // Ensure the page now has an upload button instead of a remove button.
+ $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), t('After clicking the "Remove" button, it is no longer displayed.'));
+ $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), t('After clicking the "Remove" button, the "Upload" button is displayed.'));
+
+ // Save the node and ensure it does not have the file.
+ $this->drupalPost(NULL, array(), t('Save'));
+ $node = node_load($nid, NULL, TRUE);
+ $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), t('File was successfully removed from the node.'));
+ }
+ }
+}
+
/**
* Test class to test file handling with node revisions.
*/
diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test
index fb46d94..8821ec9 100644
--- a/modules/simpletest/tests/form.test
+++ b/modules/simpletest/tests/form.test
@@ -996,14 +996,14 @@ class FormsProgrammaticTestCase extends DrupalWebTestCase {
}
/**
- * Test that FAPI correctly determines $form_state['clicked_button'].
+ * Test that FAPI correctly determines $form_state['triggering_element'].
*/
-class FormsClickedButtonTestCase extends DrupalWebTestCase {
+class FormsTriggeringElementTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
- 'name' => 'Form clicked button determination',
- 'description' => 'Test the determination of $form_state[\'clicked_button\'].',
+ 'name' => 'Form triggering element determination',
+ 'description' => 'Test the determination of $form_state[\'triggering_element\'].',
'group' => 'Form API',
);
}
@@ -1013,59 +1013,85 @@ class FormsClickedButtonTestCase extends DrupalWebTestCase {
}
/**
- * Test the determination of $form_state['clicked_button'] when no button
+ * Test the determination of $form_state['triggering_element'] when no button
* information is included in the POST data, as is sometimes the case when
* the ENTER key is pressed in a textfield in Internet Explorer.
*/
function testNoButtonInfoInPost() {
$path = 'form-test/clicked-button';
$edit = array();
- $form_id = 'form-test-clicked-button';
+ $form_html_id = 'form-test-clicked-button';
// Ensure submitting a form with no buttons results in no
- // $form_state['clicked_button'] and the form submit handler not running.
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path, $edit, NULL, array(), array(), $form_id);
- $this->assertText('There is no clicked button.', t('$form_state[\'clicked_button\'] set to NULL.'));
+ // $form_state['triggering_element'] and the form submit handler not
+ // running.
+ $this->drupalPost($path, $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('There is no clicked button.', t('$form_state[\'triggering_element\'] set to NULL.'));
$this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.'));
// Ensure submitting a form with one or more submit buttons results in
- // $form_state['clicked_button'] being set to the first one the user has
+ // $form_state['triggering_element'] being set to the first one the user has
// access to. An argument with 'r' in it indicates a restricted
// (#access=FALSE) button.
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/s', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to only button.'));
+ $this->drupalPost($path . '/s', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to only button.'));
$this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/s/s', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+
+ $this->drupalPost($path . '/s/s', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.'));
$this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/rs/s', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button2.', t('$form_state[\'clicked_button\'] set to first available button.'));
+
+ $this->drupalPost($path . '/rs/s', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button2.', t('$form_state[\'triggering_element\'] set to first available button.'));
$this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
// Ensure submitting a form with buttons of different types results in
- // $form_state['clicked_button'] being set to the first button, regardless
- // of type. For the FAPI 'button' type, this should result in the submit
- // handler not executing. The types are 's'(ubmit), 'b'(utton), and
+ // $form_state['triggering_element'] being set to the first button,
+ // regardless of type. For the FAPI 'button' type, this should result in the
+ // submit handler not executing. The types are 's'(ubmit), 'b'(utton), and
// 'i'(mage_button).
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/s/b/i', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+ $this->drupalPost($path . '/s/b/i', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.'));
$this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/b/s/i', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+
+ $this->drupalPost($path . '/b/s/i', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.'));
$this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.'));
- drupal_static_reset('drupal_html_id');
- $this->drupalPost($path . '/i/s/b', $edit, NULL, array(), array(), $form_id);
- $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.'));
+
+ $this->drupalPost($path . '/i/s/b', $edit, NULL, array(), array(), $form_html_id);
+ $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.'));
$this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.'));
}
-}
+ /**
+ * Test that $form_state['triggering_element'] does not get set to a button
+ * with #access=FALSE.
+ */
+ function testAttemptAccessControlBypass() {
+ $path = 'form-test/clicked-button';
+ $form_html_id = 'form-test-clicked-button';
+
+ // Retrieve a form where 'button1' has #access=FALSE and 'button2' doesn't.
+ $this->drupalGet($path . '/rs/s');
+
+ // Submit the form with 'button1=button1' in the POST data, which someone
+ // trying to get around security safeguards could easily do. We have to do
+ // a little trickery here, to work around the safeguards in drupalPost(): by
+ // renaming the text field that is in the form to 'button1', we can get the
+ // data we want into $_POST.
+ $elements = $this->xpath('//form[@id="' . $form_html_id . '"]//input[@name="text"]');
+ $elements[0]['name'] = 'button1';
+ $this->drupalPost(NULL, array('button1' => 'button1'), NULL, array(), array(), $form_html_id);
+
+ // Ensure that $form_state['triggering_element'] was not set to the
+ // restricted button. Do this with both a negative and positive assertion,
+ // because negative assertions alone can be brittle. See
+ // testNoButtonInfoInPost() for why the triggering element gets set to
+ // 'button2'.
+ $this->assertNoText('The clicked button is button1.', t('$form_state[\'triggering_element\'] not set to a restricted button.'));
+ $this->assertText('The clicked button is button2.', t('$form_state[\'triggering_element\'] not set to a restricted button.'));
+ }
+}
/**
* Tests rebuilding of arbitrary forms by altering them.
diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module
index 0b9284a..09cea65 100644
--- a/modules/simpletest/tests/form_test.module
+++ b/modules/simpletest/tests/form_test.module
@@ -1114,8 +1114,8 @@ function form_test_clicked_button($form, &$form_state) {
* Form validation handler for the form_test_clicked_button() form.
*/
function form_test_clicked_button_validate($form, &$form_state) {
- if (isset($form_state['clicked_button'])) {
- drupal_set_message(t('The clicked button is %name.', array('%name' => $form_state['clicked_button']['#name'])));
+ if (isset($form_state['triggering_element'])) {
+ drupal_set_message(t('The clicked button is %name.', array('%name' => $form_state['triggering_element']['#name'])));
}
else {
drupal_set_message('There is no clicked button.');
@@ -1129,7 +1129,6 @@ function form_test_clicked_button_submit($form, &$form_state) {
drupal_set_message('Submit handler for form_test_clicked_button executed.');
}
-
/**
* Implements hook_form_FORM_ID_alter() for the registration form.
*/