form.inc 45.6 KB
Newer Older
1
<?php
2
3
// $Id$

4
5
6
/**
 * @defgroup form Form generation
 * @{
7
 * Functions to enable the processing and display of HTML forms.
8
 *
9
10
11
12
13
14
15
16
17
18
19
 * Drupal uses these functions to achieve consistency in its form processing and
 * presentation, while simplifying code and reducing the amount of HTML that
 * must be explicitly generated by modules.
 *
 * The drupal_get_form() function handles retrieving, processing, and
 * displaying a rendered HTML form for modules automatically. For example:
 *
 * // display the user registration form
 * $output = drupal_get_form('user_register');
 *
 * Forms can also be built and submitted programmatically without any user input
20
 * by populating $form['#post'] with values to be submitted. For example:
21
22
23
 *
 * // register a new user
 * $form = drupal_retrieve_form('user_register');
24
25
26
 * $form['#post']['name'] = 'robo-user';
 * $form['#post']['mail'] = 'robouser@example.com';
 * $form['#post']['pass'] = 'password';
27
28
29
30
31
32
33
 * drupal_process_form('user_register', $form);
 *
 * Calling form_get_errors() will list any validation errors that prevented the
 * form from being submitted.
 *
 * For information on the format of the structured arrays used to define forms,
 * and more detailed explanations of the Form API workflow, see the reference at
34
 * http://api.drupal.org/api/HEAD/file/developer/topics/forms_api_reference.html
35
 * and the quickstart guide at
36
 * http://api.drupal.org/api/HEAD/file/developer/topics/forms_api.html
37
38
39
 */

/**
40
41
 * Retrieves a form from a builder function, passes it on for
 * processing, and renders the form or redirects to its destination
42
43
44
 * as appropriate. In multi-step form scenerios, it handles properly
 * processing the values using the previous step's form definition,
 * then rendering the requested step for display.
45
46
 *
 * @param $form_id
47
48
49
50
51
52
53
54
55
56
57
58
 *   The unique string identifying the desired form. If a function
 *   with that name exists, it is called to build the form array.
 *   Modules that need to generate the same form (or very similar forms)
 *   using different $form_ids can implement hook_forms(), which maps
 *   different $form_id values to the proper form building function. Examples
 *   may be found in node_forms(), search_forms(), and user_forms().
 * @param ...
 *   Any additional arguments needed by the form building function.
 * @return
 *   The rendered form.
 */
function drupal_get_form($form_id) {
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
  // In multi-step form scenerios, the incoming $_POST values are not
  // necessarily intended for the current form. We need to build
  // a copy of the previously built form for validation and processing,
  // then go on to the one that was requested if everything works.

  $form_build_id = md5(mt_rand());
  if (isset($_POST['form_build_id']) && isset($_SESSION['form'][$_POST['form_build_id']])) {
    // There's a previously stored multi-step form. We should handle
    // IT first.
    $stored = TRUE;
    $args = $_SESSION['form'][$_POST['form_build_id']];
    $form = call_user_func_array('drupal_retrieve_form', $args);
  }
  else {
    // We're coming in fresh; build things as they would be. If the
    // form's #multistep flag is set, store the build parameters so
    // the same form can be reconstituted for validation.
    $args = func_get_args();
    $form = call_user_func_array('drupal_retrieve_form', $args);
    if (isset($form['#multistep']) && $form['#multistep']) {
      $_SESSION['form'][$form_build_id] = $args;
      $form['#build_id'] = $form_build_id;
    }
    $stored = FALSE;
  }

  // Process the form, submit it, and store any errors if necessary.
  drupal_process_form($args[0], $form);

  if ($stored && !form_get_errors()) {
    // If it's a stored form and there were no errors, we processed the
    // stored form successfully. Now we need to build the form that was
    // actually requested. We always pass in the current $_POST values
    // to the builder function, as values from one stage of a multistep
    // form can determine how subsequent steps are displayed.
    $args = func_get_args();
95
    $args[] = $_POST;
96
97
98
99
100
101
    $form = call_user_func_array('drupal_retrieve_form', $args);
    unset($_SESSION['form'][$_POST['form_build_id']]);
    if (isset($form['#multistep']) && $form['#multistep']) {
      $_SESSION['form'][$form_build_id] = $args;
      $form['#build_id'] = $form_build_id;
    }
102
    // If we're in this part of the code, $_POST always contains
103
104
105
    // values from the previously submitted form. Unset it to avoid
    // any accidental submission of doubled data, then process the form
    // to prep it for rendering.
106
    unset($_POST);
107
108
109
110
    drupal_process_form($args[0], $form);
  }

  return drupal_render_form($args[0], $form);
111
112
}

113

114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
 * Retrieves the structured array that defines a given form.
 *
 * @param $form_id
 *   The unique string identifying the desired form. If a function
 *   with that name exists, it is called to build the form array.
 *   Modules that need to generate the same form (or very similar forms)
 *   using different $form_ids can implement hook_forms(), which maps
 *   different $form_id values to the proper form building function.
 * @param ...
 *   Any additional arguments needed by the form building function.
 */
function drupal_retrieve_form($form_id) {
  static $forms;

  $args = func_get_args();
  array_shift($args);
  if (!function_exists($form_id)) {
    if (!isset($forms)) {
      $forms = module_invoke_all('forms');
    }
    $form_definition = $forms[$form_id];
    if (isset($form_definition['callback arguments'])) {
      $args = array_merge($form_definition['callback arguments'], $args);
    }
    if (isset($form_definition['callback'])) {
      $callback = $form_definition['callback'];
    }
  }
  // $callback comes from a hook_forms() implementation
  return call_user_func_array(isset($callback) ? $callback : $form_id, $args);
}

/**
 * This function is the heart of form API. The form gets built, validated and in
 * appropriate cases, submitted.
 *
 * @param $form_id
 *   The unique string identifying the current form.
153
154
 * @param $form
 *   An associative array containing the structure of the form.
155
156
 * @return
 *   The path to redirect the user to upon completion.
157
 */
158
function drupal_process_form($form_id, &$form) {
159
  global $form_values, $form_submitted, $user, $form_button_counter;
160
  static $saved_globals = array();
161
162
163
  // In some scenerios, this function can be called recursively. Pushing any pre-existing
  // $form_values and form submission data lets us start fresh without clobbering work done
  // in earlier recursive calls.
164
165
  array_push($saved_globals, array($form_values, $form_submitted, $form_button_counter));

166
  $form_values = array();
Dries's avatar
Dries committed
167
  $form_submitted = FALSE;
168
  $form_button_counter = array(0, 0);
Steven Wittens's avatar
Steven Wittens committed
169

170
  drupal_prepare_form($form_id, $form);
171
  if (($form['#programmed']) || (!empty($_POST) && (($_POST['form_id'] == $form_id) || ($_POST['form_id'] == $form['#base'])))) {
172
    drupal_validate_form($form_id, $form);
173
174
175
    // IE does not send a button value when there is only one submit button (and no non-submit buttons)
    // and you submit by pressing enter.
    // In that case we accept a submission without button values.
176
177
    if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) {
      $redirect = drupal_submit_form($form_id, $form);
178
179
180
      if (!$form['#programmed']) {
        drupal_redirect_form($form, $redirect);
      }
181
182
183
    }
  }

184
185
  // We've finished calling functions that alter the global values, so we can
  // restore the ones that were there before this function was called.
186
  list($form_values, $form_submitted, $form_button_counter) = array_pop($saved_globals);
187
  return $redirect;
188
189
190
191
192
193
194
195
196
197
198
199
200
}

/**
 * Prepares a structured form array by adding required elements,
 * executing any hook_form_alter functions, and optionally inserting
 * a validation token to prevent tampering.
 *
 * @param $form_id
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
 * @param $form
 *   An associative array containing the structure of the form.
 */
201
function drupal_prepare_form($form_id, &$form) {
202
  $form['#type'] = 'form';
203
204
205
206
207
208
209
210
211

  if (!isset($form['#post'])) {
    $form['#post'] = $_POST;
    $form['#programmed'] = FALSE;
  }
  else {
    $form['#programmed'] = TRUE;
  }

212
213
214
215
216
217
218
219
220
221
222
  // In multi-step form scenerios, this id is used to identify
  // a unique instance of a particular form for retrieval.
  if (isset($form['#build_id'])) {
    $form['form_build_id'] = array(
      '#type' => 'hidden',
      '#value' => $form['#build_id'],
      '#id' => $form['#build_id'],
      '#name' => 'form_build_id',
    );
  }

223
224
225
226
227
228
229
  // If $base is set, it is used in place of $form_id when constructing validation,
  // submission, and theming functions. Useful for mapping many similar or duplicate
  // forms with different $form_ids to the same processing functions.
  if (isset($form['#base'])) {
    $base = $form['#base'];
  }

230
  if (isset($form['#token'])) {
231
232
233
    // If the page cache is on and an anonymous user issues a GET request,
    // unset the token because the token in the cached page would not match,
    // because the token is based on the session ID.
234
235
    if (variable_get('cache', 0) && !$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET') {
      unset($form['#token']);
236
    }
237
238
239
240
241
    else {
      // Make sure that a private key is set:
      if (!variable_get('drupal_private_key', '')) {
        variable_set('drupal_private_key', mt_rand());
      }
242

243
244
      $form['form_token'] = array('#type' => 'hidden', '#default_value' => md5(session_id() . $form['#token'] . variable_get('drupal_private_key', '')));
    }
245
  }
246

247
  if (isset($form_id)) {
248
    $form['form_id'] = array('#type' => 'hidden', '#value' => $form_id, '#id' => str_replace('_', '-', "edit-$form_id"));
249
  }
250
251
252
  if (!isset($form['#id'])) {
    $form['#id'] = $form_id;
  }
Steven Wittens's avatar
Steven Wittens committed
253

254
  $form += _element_info('form');
255

Dries's avatar
Dries committed
256
257
  if (!isset($form['#validate'])) {
    if (function_exists($form_id .'_validate')) {
258
      $form['#validate'] = array($form_id .'_validate' => array());
Dries's avatar
Dries committed
259
    }
260
261
    elseif (function_exists($base .'_validate')) {
      $form['#validate'] = array($base .'_validate' => array());
Dries's avatar
Dries committed
262
263
264
    }
  }

265
266
  if (!isset($form['#submit'])) {
    if (function_exists($form_id .'_submit')) {
267
268
      // we set submit here so that it can be altered but use reference for
      // $form_values because it will change later
269
      $form['#submit'] = array($form_id .'_submit' => array());
Dries's avatar
Dries committed
270
    }
271
272
    elseif (function_exists($base .'_submit')) {
      $form['#submit'] = array($base .'_submit' => array());
Dries's avatar
Dries committed
273
274
275
    }
  }

276
277
  foreach (module_implements('form_alter') as $module) {
    $function = $module .'_form_alter';
278
    $function($form_id, $form);
279
280
  }

281
  $form = form_builder($form_id, $form);
282
283
}

284
285
286
287
288
289
290
291
292
293
294
295

/**
 * Validates user-submitted form data from a global variable using
 * the validate functions defined in a structured form array.
 *
 * @param $form_id
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
 * @param $form
 *   An associative array containing the structure of the form.
 *
 */
296
function drupal_validate_form($form_id, $form) {
297
  global $form_values;
298
299
300
301
302
  static $validated_forms = array();

  if (isset($validated_forms[$form_id])) {
    return;
  }
303

304
  // If the session token was set by drupal_prepare_form(), ensure that it
305
  // matches the current user's session
306
  if (isset($form['#token'])) {
307
    if ($form_values['form_token'] != md5(session_id() . $form['#token'] . variable_get('drupal_private_key', ''))) {
308
      // setting this error will cause the form to fail validation
309
      form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.'));
310
311
312
    }
  }

313
  _form_validate($form, $form_id);
314
  $validated_forms[$form_id] = TRUE;
315
316
}

317
318
319
320
321
322
323
324
325
326
327
328
329
330
/**
 * Processes user-submitted form data from a global variable using
 * the submit functions defined in a structured form array.
 *
 * @param $form_id
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
 * @param $form
 *   An associative array containing the structure of the form.
 * @return
 *   A string containing the path of the page to display when processing
 *   is complete.
 *
 */
331
function drupal_submit_form($form_id, $form) {
332
333
  global $form_values;
  $default_args = array($form_id, &$form_values);
334

335
  if (isset($form['#submit'])) {
336
337
    foreach ($form['#submit'] as $function => $args) {
      if (function_exists($function)) {
338
        $args = array_merge($default_args, (array) $args);
339
340
        // Since we can only redirect to one page, only the last redirect will work
        $redirect = call_user_func_array($function, $args);
341
342
343
        if (isset($redirect)) {
          $goto = $redirect;
        }
Dries's avatar
Dries committed
344
345
      }
    }
346
  }
347
  return $goto;
348
349
}

350
351
352
353
354
355
356
357
358
359
360
361
362
/**
 * Renders a structured form array into themed HTML.
 *
 * @param $form_id
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
 * @param $form
 *   An associative array containing the structure of the form.
 * @return
 *   A string containing the path of the page to display when processing
 *   is complete.
 *
 */
363
function drupal_render_form($form_id, &$form) {
364
  // Don't override #theme if someone already set it.
365
366
367
368
  if (isset($form['#base'])) {
    $base = $form['#base'];
  }

369
370
371
372
  if (!isset($form['#theme'])) {
    if (theme_get_function($form_id)) {
      $form['#theme'] = $form_id;
    }
373
374
    elseif (theme_get_function($base)) {
      $form['#theme'] = $base;
375
376
377
378
379
380
381
382
383
384
385
    }
  }

  if (isset($form['#pre_render'])) {
    foreach ($form['#pre_render'] as $function) {
      if (function_exists($function)) {
        $function($form_id, $form);
      }
    }
  }

386
  $output = drupal_render($form);
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
  return $output;
}

/**
 * Redirect the user to a URL after a form has been processed.
 *
 * @param $form
 *   An associative array containing the structure of the form.
 * @param $redirect
 *   An optional string containing the destination path to redirect
 *   to if none is specified by the form.
 *
 */
function drupal_redirect_form($form, $redirect = NULL) {
  if (isset($redirect)) {
    $goto = $redirect;
  }
  if (isset($form['#redirect'])) {
    $goto = $form['#redirect'];
  }
  if ($goto !== FALSE) {
    if (is_array($goto)) {
      call_user_func_array('drupal_goto', $goto);
    }
    elseif (!isset($goto)) {
      drupal_goto($_GET['q']);
    }
    else {
      drupal_goto($goto);
    }
  }
}

420
function _form_validate($elements, $form_id = NULL) {
421
422
423
424
425
426
  // Recurse through all children.
  foreach (element_children($elements) as $key) {
    if (isset($elements[$key]) && $elements[$key]) {
      _form_validate($elements[$key]);
    }
  }
427
  /* Validate the current input */
428
  if (!$elements['#validated']) {
429
    if (isset($elements['#needs_validation'])) {
430
431
432
433
      // An empty textfield returns '' so we use empty(). An empty checkbox
      // and a textfield could return '0' and empty('0') returns TRUE so we
      // need a special check for the '0' string.
      if ($elements['#required'] && empty($elements['#value']) && $elements['#value'] !== '0') {
434
        form_error($elements, t('!name field is required.', array('!name' => $elements['#title'])));
435
      }
436
437
438
439
440
441
442
443
444
445
446
447
448
449

      // Add legal choice check if element has #options. Can be skipped, but then you must validate your own element.
      if (isset($elements['#options']) && isset($elements['#value']) && !isset($elements['#DANGEROUS_SKIP_CHECK'])) {
        if ($elements['#type'] == 'select') {
          $options = form_options_flatten($elements['#options']);
        }
        else {
          $options = $elements['#options'];
        }
        if (is_array($elements['#value'])) {
          $value = $elements['#type'] == 'checkboxes' ? array_keys(array_filter($elements['#value'])) : $elements['#value'];
          foreach ($value as $v) {
            if (!isset($options[$v])) {
              form_error($elements, t('An illegal choice has been detected. Please contact the site administrator.'));
450
              watchdog('form', t('Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'])), WATCHDOG_ERROR);
451
            }
452
453
          }
        }
454
455
        elseif (!isset($options[$elements['#value']])) {
          form_error($elements, t('An illegal choice has been detected. Please contact the site administrator.'));
456
          watchdog('form', t('Illegal choice %choice in %name element.', array('%choice' => theme_placeholder(check_plain($elements['#value'])), '%name' => theme('placeholder', empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']))), WATCHDOG_ERROR);
457
        }
458
459
460
461
      }
    }

    // User-applied checks.
Dries's avatar
Dries committed
462
    if (isset($elements['#validate'])) {
463
464
465
466
467
      foreach ($elements['#validate'] as $function => $args) {
        $args = array_merge(array($elements), $args);
        // for the full form we hand over a copy of $form_values
        if (isset($form_id)) {
          $args = array_merge(array($form_id, $GLOBALS['form_values']), $args);
468
        }
469
470
        if (function_exists($function))  {
          call_user_func_array($function, $args);
471
472
473
        }
      }
    }
474
    $elements['#validated'] = TRUE;
475
476
477
  }
}

478
479
480
481
482
/**
 * File an error against a form element. If the name of the element is
 * edit[foo][bar] then you may pass either foo or foo][bar as $name
 * foo will set an error for all its children.
 */
483
function form_set_error($name = NULL, $message = '') {
484
485
486
  static $form = array();
  if (isset($name) && !isset($form[$name])) {
    $form[$name] = $message;
487
488
489
    if ($message) {
      drupal_set_message($message, 'error');
    }
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
  }
  return $form;
}

/**
 * Return an associative array of all errors.
 */
function form_get_errors() {
  $form = form_set_error();
  if (!empty($form)) {
    return $form;
  }
}

/**
 * Return the error message filed against the form with the specified name.
 */
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];
  }
}

519
520
521
/**
 * Flag an element as having an error.
 */
522
function form_error(&$element, $message = '') {
523
  $element['#error'] = TRUE;
524
  form_set_error(implode('][', $element['#parents']), $message);
525
526
527
}

/**
528
529
530
531
532
533
 * Adds some required properties to each form element, which are used
 * internally in the form api. This function also automatically assigns
 * the value property from the $edit array, provided the element doesn't
 * already have an assigned value.
 *
 * @param $form_id
534
535
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
536
537
 * @param $form
 *   An associative array containing the structure of the form.
538
 */
539
function form_builder($form_id, $form) {
540
  global $form_values, $form_submitted, $form_button_counter;
541

542
543
544
  // Initialize as unprocessed.
  $form['#processed'] = FALSE;

545
  /* Use element defaults */
546
  if ((!empty($form['#type'])) && ($info = _element_info($form['#type']))) {
547
    // overlay $info onto $form, retaining preexisting keys in $form
548
549
550
    $form += $info;
  }

551
  if (isset($form['#input']) && $form['#input']) {
552
    if (!isset($form['#name'])) {
553
554
555
556
557
558
      $name = array_shift($form['#parents']);
      $form['#name'] = $name;
      if (count($form['#parents'])) {
        $form['#name'] .= '['. implode('][', $form['#parents']) .']';
      }
      array_unshift($form['#parents'], $name);
559
560
    }
    if (!isset($form['#id'])) {
561
      $form['#id'] = 'edit-'. implode('-', $form['#parents']);
562
    }
563

564
565
566
567
    if (isset($form['#disabled']) && $form['#disabled']) {
      $form['#attributes']['disabled'] = 'disabled';
    }

568
    if (!isset($form['#value']) && !array_key_exists('#value', $form)) {
569
570
      if (($form['#programmed']) || ((!isset($form['#access']) || $form['#access']) && isset($form['#post']) && ($form['#post']['form_id'] == $form_id))) {
        $edit = $form['#post'];
571
572
573
        foreach ($form['#parents'] as $parent) {
          $edit = isset($edit[$parent]) ? $edit[$parent] : NULL;
        }
574
575
576
577
578
579
580
581
582
583
584
585
586
587
        if (!$form['#programmed'] || isset($edit)) {
          switch ($form['#type']) {
            case 'checkbox':
              $form['#value'] = !empty($edit) ? $form['#return_value'] : 0;
              break;

            case 'select':
              if (isset($form['#multiple']) && $form['#multiple']) {
                if (isset($edit) && is_array($edit)) {
                  $form['#value'] = drupal_map_assoc($edit);
                }
                else {
                  $form['#value'] = array();
                }
588
              }
589
590
              elseif (isset($edit)) {
                $form['#value'] = $edit;
591
              }
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
              break;

            case 'textfield':
              if (isset($edit)) {
                // Equate $edit to the form value to ensure it's marked for validation
                $edit = str_replace(array("\r", "\n"), '', $edit);
                $form['#value'] = $edit;
              }
              break;

            default:
              if (isset($edit)) {
                $form['#value'] = $edit;
              }
          }
          // Mark all posted values for validation
          if ((isset($form['#value']) && $form['#value'] === $edit) || (isset($form['#required']) && $form['#required'])) {
            $form['#needs_validation'] = TRUE;
          }
611
612
613
        }
      }
      if (!isset($form['#value'])) {
614
615
616
617
618
619
620
        $function = $form['#type'] . '_value';
        if (function_exists($function)) {
          $function($form);
        }
        else {
          $form['#value'] = $form['#default_value'];
        }
621
      }
622
    }
623
624
625
626
    if (isset($form['#executes_submit_callback'])) {
      // Count submit and non-submit buttons
      $form_button_counter[$form['#executes_submit_callback']]++;
      // See if a submit button was pressed
627
      if (isset($form['#post'][$form['#name']]) && $form['#post'][$form['#name']] == $form['#value']) {
628
        $form_submitted = $form_submitted || $form['#executes_submit_callback'];
629
630
631
632
633

        // 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_values[$form['#name']] = $form['#value'];
634
635
636
637
      }
    }
  }

638
  // Allow for elements to expand to multiple elements, e.g. radios, checkboxes and files.
639
  if (isset($form['#process']) && !$form['#processed']) {
640
641
    foreach ($form['#process'] as $process => $args) {
      if (function_exists($process)) {
642
        $args = array_merge(array($form), array($edit), $args);
643
        $form = call_user_func_array($process, $args);
644
645
      }
    }
646
    $form['#processed'] = TRUE;
647
648
  }

649
650
651
  // Set the $form_values key that gets passed to validate and submit.
  // We call this after #process gets called so that #process has a
  // chance to update #value if desired.
652
  if (isset($form['#input']) && $form['#input']) {
653
    form_set_value($form, $form['#value']);
654
655
  }

656
657
658
  // Recurse through all child elements.
  $count  = 0;
  foreach (element_children($form) as $key) {
659
660
    $form[$key]['#post'] = $form['#post'];
    $form[$key]['#programmed'] = $form['#programmed'];
Steven Wittens's avatar
Steven Wittens committed
661
    // don't squash an existing tree value
662
663
664
    if (!isset($form[$key]['#tree'])) {
      $form[$key]['#tree'] = $form['#tree'];
    }
Steven Wittens's avatar
Steven Wittens committed
665

666
667
668
669
670
    // deny access to child elements if parent is denied
    if (isset($form['#access']) && !$form['#access']) {
      $form[$key]['#access'] = FALSE;
    }

671
672
    // don't squash existing parents value
    if (!isset($form[$key]['#parents'])) {
673
674
      // Check to see if a tree of child elements is present. If so, continue down the tree if required.
      $form[$key]['#parents'] = $form[$key]['#tree'] && $form['#tree'] ? array_merge($form['#parents'], array($key)) : array($key);
Steven Wittens's avatar
Steven Wittens committed
675
676
    }

677
    // Assign a decimal placeholder weight to preserve original array order
678
679
680
    if (!isset($form[$key]['#weight'])) {
      $form[$key]['#weight'] = $count/1000;
    }
681
    $form[$key] = form_builder($form_id, $form[$key]);
682
683
684
    $count++;
  }

685
686
687
688
689
690
  if (isset($form['#after_build']) && !isset($form['#after_build_done'])) {
    foreach ($form['#after_build'] as $function) {
      if (function_exists($function)) {
        $form = $function($form, $form_values);
      }
    }
691
    $form['#after_build_done'] = TRUE;
Steven Wittens's avatar
Steven Wittens committed
692
  }
693
694

  return $form;
695
696
}

697
/**
Dries's avatar
Dries committed
698
 * Use this function to make changes to form values in the form validate
699
700
701
702
 * phase, so they will be available in the submit phase in $form_values.
 *
 * Specifically, if $form['#parents'] is array('foo', 'bar')
 * and $value is 'baz' then this function will make
Dries's avatar
Dries committed
703
 * $form_values['foo']['bar'] to be 'baz'.
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
 *
 * @param $form
 *   The form item. Keys used: #parents, #value
 * @param $value
 *   The value for the form item.
 */
function form_set_value($form, $value) {
  global $form_values;
  _form_set_value($form_values, $form, $form['#parents'], $value);
}

/**
 * Helper function for form_set_value().
 *
 * We iterate of $parents and create nested arrays for them
 * in $form_values if needed. Then we insert the value in
 * the right array.
 */
function _form_set_value(&$form_values, $form, $parents, $value) {
  $parent = array_shift($parents);
  if (empty($parents)) {
    $form_values[$parent] = $value;
  }
  else {
    if (!isset($form_values[$parent])) {
      $form_values[$parent] = array();
    }
    _form_set_value($form_values[$parent], $form, $parents, $value);
  }
  return $form;
}

736
737
738
/**
 * Retrieve the default properties for the defined element type.
 */
739
function _element_info($type, $refresh = NULL) {
740
  static $cache;
Steven Wittens's avatar
Steven Wittens committed
741

742
  $basic_defaults = array(
743
744
745
    '#description' => NULL,
    '#attributes' => array(),
    '#required' => FALSE,
Steven Wittens's avatar
Steven Wittens committed
746
    '#tree' => FALSE,
747
    '#parents' => array()
748
  );
749
  if (!isset($cache) || $refresh) {
750
751
752
    $cache = array();
    foreach (module_implements('elements') as $module) {
      $elements = module_invoke($module, 'elements');
753
      if (isset($elements) && is_array($elements)) {
754
        $cache = array_merge_recursive($cache, $elements);
755
756
757
758
      }
    }
    if (sizeof($cache)) {
      foreach ($cache as $element_type => $info) {
759
        $cache[$element_type] = array_merge_recursive($basic_defaults, $info);
760
761
762
763
764
765
766
      }
    }
  }

  return $cache[$type];
}

767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
function form_options_flatten($array, $reset = TRUE) {
  static $return;

  if ($reset) {
    $return = array();
  }

  foreach ($array as $key => $value) {
    if (is_array($value)) {
      form_options_flatten($value, FALSE);
    }
    else {
      $return[$key] = 1;
    }
  }

  return $return;
}

786
787
788
789
790
/**
 * Format a dropdown menu or scrolling selection box.
 *
 * @param $element
 *   An associative array containing the properties of the element.
791
 *   Properties used: title, value, options, description, extra, multiple, required
792
793
794
795
796
797
798
799
800
 * @return
 *   A themed HTML string representing the form element.
 *
 * It is possible to group options together; to do this, change the format of
 * $options to an associative array in which the keys are group labels, and the
 * values are associative arrays in the normal $options format.
 */
function theme_select($element) {
  $select = '';
801
  $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
802
  _form_set_class($element, array('form-select'));
803
804
  $multiple = isset($element['#multiple']) && $element['#multiple'];
  return theme('form_element', $element, '<select name="'. $element['#name'] .''. ($multiple ? '[]' : '') .'"'. ($multiple ? ' multiple="multiple" ' : '') . drupal_attributes($element['#attributes']) .' id="'. $element['#id'] .'" '. $size .'>'. form_select_options($element) .'</select>');
805
806
807
808
809
810
}

function form_select_options($element, $choices = NULL) {
  if (!isset($choices)) {
    $choices = $element['#options'];
  }
811
  // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
812
813
814
  // isset() fails in this situation.
  $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
  $value_is_array = is_array($element['#value']);
815
816
  $options = '';
  foreach ($choices as $key => $choice) {
817
    if (is_array($choice)) {
818
819
820
      $options .= '<optgroup label="'. $key .'">';
      $options .= form_select_options($element, $choice);
      $options .= '</optgroup>';
821
822
    }
    else {
823
824
      $key = (string)$key;
      if ($value_valid && ($element['#value'] == $key || ($value_is_array && in_array($key, $element['#value'])))) {
825
826
827
828
829
        $selected = ' selected="selected"';
      }
      else {
        $selected = '';
      }
830
      $options .= '<option value="'. $key .'"'. $selected .'>'. check_plain($choice) .'</option>';
831
832
    }
  }
833
  return $options;
834
835
836
837
838
839
840
}

/**
 * Format a group of form items.
 *
 * @param $element
 *   An associative array containing the properties of the element.
841
 *   Properties used: attributes, title, value, description, children, collapsible, collapsed
842
843
844
845
 * @return
 *   A themed HTML string representing the form item group.
 */
function theme_fieldset($element) {
846
  if ($element['#collapsible']) {
847
848
    drupal_add_js('misc/collapse.js');

849
850
851
852
    if (!isset($element['#attributes']['class'])) {
      $element['#attributes']['class'] = '';
    }

853
854
855
    $element['#attributes']['class'] .= ' collapsible';
    if ($element['#collapsed']) {
     $element['#attributes']['class'] .= ' collapsed';
856
857
858
    }
  }

859
  return '<fieldset' . drupal_attributes($element['#attributes']) .'>' . ($element['#title'] ? '<legend>'. $element['#title'] .'</legend>' : '') . ($element['#description'] ? '<div class="description">'. $element['#description'] .'</div>' : '') . $element['#children'] . $element['#value'] . "</fieldset>\n";
860
861
862
863
864
865
866
}

/**
 * Format a radio button.
 *
 * @param $element
 *   An associative array containing the properties of the element.
867
 *   Properties used: required, return_value, value, attributes, title, description
868
869
870
871
 * @return
 *   A themed HTML string representing the form item group.
 */
function theme_radio($element) {
872
  _form_set_class($element, array('form-radio'));
873
  $output = '<input type="radio" ';
874
875
876
877
878
879
  $output .= 'name="' . $element['#name'] .'" ';
  $output .= 'value="'. $element['#return_value'] .'" ';
  $output .= ($element['#value'] == $element['#return_value']) ? ' checked="checked" ' : ' ';
  $output .= drupal_attributes($element['#attributes']) .' />';
  if (!is_null($element['#title'])) {
    $output = '<label class="option">'. $output .' '. $element['#title'] .'</label>';
880
  }
881
882
883

  unset($element['#title']);
  return theme('form_element', $element, $output);
884
885
886
887
888
889
890
}

/**
 * Format a set of radio buttons.
 *
 * @param $element
 *   An associative array containing the properties of the element.
891
 *   Properties used: title, value, options, description, required and attributes.
892
893
894
895
 * @return
 *   A themed HTML string representing the radio button set.
 */
function theme_radios($element) {
896
  if ($element['#title'] || $element['#description']) {
897
898
    unset($element['#id']);
    return theme('form_element', $element, $element['#children']);
899
900
  }
  else {
901
    return $element['#children'];
902
903
904
  }
}

905
906
907
908
909
910
911
912
913
914
/**
 * Format a password_confirm item.
 *
 * @param $element
 *   An associative array containing the properties of the element.
 *   Properties used: title, value, id, required, error.
 * @return
 *   A themed HTML string representing the form item.
 */
function theme_password_confirm($element) {
Dries's avatar
Dries committed
915
  return theme('form_element', $element, $element['#children']);
916
917
}

918
919
920
921
/*
 * Expand a password_confirm field into two text boxes.
 */
function expand_password_confirm($element) {
Dries's avatar
Dries committed
922
923
924
925
926
927
928
929
930
931
  $element['pass1'] =  array(
    '#type' => 'password',
    '#title' => t('Password'),
    '#value' => $element['#value']['pass1'],
  );
  $element['pass2'] =  array(
    '#type' => 'password',
    '#title' => t('Confirm password'),
    '#value' => $element['#value']['pass2'],
  );
932
933
934
935
936
937
  $element['#validate'] = array('password_confirm_validate' => array());
  $element['#tree'] = TRUE;

  return $element;
}

938
/**
939
 * Validate password_confirm element.
940
 */
941
function password_confirm_validate($form) {
942
943
  $pass1 = trim($form['pass1']['#value']);
  if (!empty($pass1)) {
944
    $pass2 = trim($form['pass2']['#value']);
945
    if ($pass1 != $pass2) {
946
      form_error($form, t('The specified passwords do not match.'));
947
    }
948
  }
949
  elseif ($form['#required'] && !empty($form['#post'])) {
950
    form_error($form, t('Password field is required.'));
951
  }
952

953
954
955
956
957
958
  // Password field must be converted from a two-element array into a single
  // string regardless of validation results.
  form_set_value($form['pass1'], NULL);
  form_set_value($form['pass2'], NULL);
  form_set_value($form, $pass1);

959
960
961
  return $form;
}

962
/**
963
 * Format a date selection element.
964
965
966
 *
 * @param $element
 *   An associative array containing the properties of the element.
967
 *   Properties used: title, value, options, description, required and attributes.
968
 * @return
969
 *   A themed HTML string representing the date selection boxes.
970
971
 */
function theme_date($element) {
972
  return theme('form_element', $element, '<div class="container-inline">'. $element['#children'] .'</div>');
973
974
975
}

/**
976
 * Roll out a single date element.
977
978
979
 */
function expand_date($element) {
  // Default to current date
980
981
  if (!isset($element['#value'])) {
    $element['#value'] = array('day' => format_date(time(), 'custom', 'j'),
982
983
984
985
                            'month' => format_date(time(), 'custom', 'n'),
                            'year' => format_date(time(), 'custom', 'Y'));
  }

986
987
  $element['#tree'] = TRUE;

988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
  // Determine the order of day, month, year in the site's chosen date format.
  $format = variable_get('date_format_short', 'm/d/Y');
  $sort = array();
  $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j'));
  $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M'));
  $sort['year'] = strpos($format, 'Y');
  asort($sort);
  $order = array_keys($sort);

  // Output multi-selector for date
  foreach ($order as $type) {
    switch ($type) {
      case 'day':
        $options = drupal_map_assoc(range(1, 31));
        break;
      case 'month':
1004
        $options = drupal_map_assoc(range(1, 12), 'map_month');
1005
1006
1007
1008
1009
        break;
      case 'year':
        $options = drupal_map_assoc(range(1900, 2050));
        break;
    }
1010
1011
1012
1013
1014
1015
1016
1017
    $parents = $element['#parents'];
    $parents[] = $type;
    $element[$type] = array(
      '#type' => 'select',
      '#value' => $element['#value'][$type],
      '#attributes' => $element['#attributes'],
      '#options' => $options,
    );
1018
1019
1020
1021
1022
  }

  return $element;
}

1023
1024
1025
1026
1027
1028
1029
1030
1031
/**
 * Validates the date type to stop dates like February 30, 2006.
 */
function date_validate($form) {
  if (!checkdate($form['#value']['month'], $form['#value']['day'], $form['#value']['year'])) {
    form_error($form, t('The specified date is invalid.'));
  }
}

1032
1033
1034
1035
1036
1037
/**
 * Helper function for usage with drupal_map_assoc to display month names.
 */
function map_month($month) {
  return format_date(gmmktime(0, 0, 0, $month, 2, 1970), 'custom', 'M', 0);
}
1038

1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
/**
 * Helper function to load value from default value for checkboxes
 */
function checkboxes_value(&$form) {
  $value = array();
  foreach ((array)$form['#default_value'] as $key) {
    $value[$key] = 1;
  }
  $form['#value'] = $value;
}

1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
/**
 * If no default value is set for weight select boxes, use 0.
 */
function weight_value(&$form) {
  if (isset($form['#default_value'])) {
    $form['#value'] = $form['#default_value'];
  }
  else {
    $form['#value'] = 0;
  }
}

1062
/**
1063
1064
 * Roll out a single radios element to a list of radios,
 * using the options array as index.
1065
1066
 */
function expand_radios($element) {
1067
1068
  if (count($element['#options']) > 0) {
    foreach ($element['#options'] as $key => $choice) {
1069
      if (!isset($element[$key])) {
1070
        $element[$key] = array('#type' => 'radio', '#title' => $choice, '#return_value' => $key, '#default_value' => $element['#default_value'], '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#spawned' => TRUE);
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
      }
    }
  }
  return $element;
}

/**
 * Format a form item.
 *
 * @param $element
 *   An associative array containing the properties of the element.
1082
 *   Properties used:  title, value, description, required, error
1083
1084
1085
1086
 * @return
 *   A themed HTML string representing the form item.
 */
function theme_item($element) {
1087
  return theme('form_element', $element, $element['#value'] . $element['#children']);
1088
1089
1090
1091
1092
1093
1094
}

/**
 * Format a checkbox.
 *
 * @param $element
 *   An associative array containing the properties of the element.
1095
 *   Properties used:  title, value, return_value, description, required
1096
1097
1098
1099
 * @return
 *   A themed HTML string representing the checkbox.
 */
function theme_checkbox($element) {
1100
  _form_set_class($element, array('form-checkbox'));
1101
1102
  $checkbox = '<input ';
  $checkbox .= 'type="checkbox" ';
1103
1104
1105
  $checkbox .= 'name="'. $element['#name'] .'" ';
  $checkbox .= 'id="'. $element['#id'].'" ' ;
  $checkbox .= 'value="'. $element['#return_value'] .'" ';
1106
  $checkbox .= $element['#value'] ? ' checked="checked" ' : ' ';
1107
1108
1109
1110
  $checkbox .= drupal_attributes($element['#attributes']) . ' />';

  if (!is_null($element['#title'])) {
    $checkbox = '<label class="option">'. $checkbox .' '. $element['#title'] .'</label>';
1111
1112
  }

1113
1114
  unset($element['#title']);
  return theme('form_element', $element, $checkbox);
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
}

/**
 * Format a set of checkboxes.
 *
 * @param $element
 *   An associative array containing the properties of the element.
 * @return
 *   A themed HTML string representing the checkbox set.
 */
function theme_checkboxes($element) {
1126
  if ($element['#title'] || $element['#description']) {
1127
1128
    unset($element['#id']);
    return theme('form_element', $element, $element['#children']);
1129
1130
  }
  else {
1131
    return $element['#children'];
1132
1133
1134
1135
  }
}

function expand_checkboxes($element) {
1136
  $value = is_array($element['#value']) ? $element['#value'] : array();
Steven Wittens's avatar
Steven Wittens committed
1137
  $element['#tree'] = TRUE;
1138
1139
1140
  if (count($element['#options']) > 0) {
    if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
      $element['#default_value'] = array();
1141
    }
1142
    foreach ($element['#options'] as $key => $choice) {
1143
      if (!isset($element[$key])) {
1144
        $element[$key] = array('#type' => 'checkbox', '#processed' => TRUE, '#title' => $choice, '#return_value' => $key, '#default_value' => isset($value[$key]), '#attributes' => $element['#attributes']);
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
      }
    }
  }
  return $element;
}

function theme_submit($element) {
  return theme('button', $element);
}

function theme_button($element) {
1156
1157
1158
1159
1160
1161
1162
  //Make sure not to overwrite classes
  if (isset($element['#attributes']['class'])) {
    $element['#attributes']['class'] = 'form-'. $element['#button_type'] .' '. $element['#attributes']['class'];
  }
  else {
    $element['#attributes']['class'] = 'form-'. $element['#button_type'];
  }
1163

1164
  return '<input type="submit" '. (empty($element['#name']) ? '' : 'name="'. $element['#name'] .'" ') .'value="'. check_plain($element['#value']) .'" '. drupal_attributes($element['#attributes']) ." />\n";
1165
1166
1167
1168
1169
1170
1171
}

/**
 * Format a hidden form field.
 *
 * @param $element
 *   An associative array containing the properties of the element.
1172
 *   Properties used:  value, edit
1173
1174
1175
1176
 * @return
 *   A themed HTML string representing the hidden form field.
 */
function theme_hidden($element) {
1177
  return '<input type="hidden" name="'. $element['#name'] . '" id="'. $element['#id'] . '" value="'. check_plain($element['#value']) ."\" " . drupal_attributes($element['#attributes']) ." />\n";
1178
1179
1180
1181
1182
1183
1184
}

/**
 * Format a textfield.
 *
 * @param $element
 *   An associative array containing the properties of the element.
1185
 *   Properties used:  title, value, description, size, maxlength, required, attributes autocomplete_path
1186
1187
1188
1189
 * @return
 *   A themed HTML string representing the textfield.
 */
function theme_textfield($element) {
1190
  $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
1191
  $class = array('form-text');
1192
  $extra = '';
1193
1194
  $output = '';

1195
  if ($element['#autocomplete_path']) {
1196
    drupal_add_js('misc/autocomplete.js');
1197
    $class[] = 'form-autocomplete';
1198
    $extra =  '<input class="autocomplete" type="hidden" id="'. $element['#id'] .'-autocomplete" value="'. check_url(url($element['#autocomplete_path'], NULL, NULL, TRUE)) .'" disabled="disabled" />';
1199
  }
1200
  _form_set_class($element, $class);
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211

  if (isset($element['#field_prefix'])) {
    $output .= '<span class="field-prefix">'. $element['#field_prefix'] .'</span> ';
  }

  $output .= '<input type="text" maxlength="'. $element['#maxlength'] .'" name="'. $element['#name'] .'" id="'. $element['#id'] .'" '. $size .' value="'. check_plain($element['#value']) .'"'. drupal_attributes($element['#attributes']) .' />';

  if (isset($element['#field_suffix'])) {
    $output .= ' <span class="field-suffix">'. $element['#field_suffix'] .'</span>';
  }

1212
  return theme('form_element', $element, $output). $extra;
1213
1214
1215
1216
1217
1218
1219
}

/**
 * Format a form.
 *
 * @param $element
 *   An associative array containing the properties of the element.
1220
 *   Properties used: action, method, attributes, children
1221
1222
1223
1224
 * @return
 *   A themed HTML string representing the form.
 */
function theme_form($element) {