summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/form.inc124
1 files changed, 93 insertions, 31 deletions
diff --git a/includes/form.inc b/includes/form.inc
index 3d60afee3..e33a19d8d 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;
}