form.inc 45.7 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
102
103
104
105
    $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;
    }
    drupal_process_form($args[0], $form);
  }

  return drupal_render_form($args[0], $form);
106
107
}

108

109
110
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
/**
 * 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.
148
149
 * @param $form
 *   An associative array containing the structure of the form.
150
151
 * @return
 *   The path to redirect the user to upon completion.
152
 */
153
function drupal_process_form($form_id, &$form) {
154
  global $form_values, $form_submitted, $user, $form_button_counter;
155
  static $saved_globals = array();
156
157
158
  // 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.
159
160
  array_push($saved_globals, array($form_values, $form_submitted, $form_button_counter));

161
  $form_values = array();
Dries's avatar
Dries committed
162
  $form_submitted = FALSE;
163
  $form_button_counter = array(0, 0);
Steven Wittens's avatar
Steven Wittens committed
164

165
  drupal_prepare_form($form_id, $form);
166
  if (($form['#programmed']) || (!empty($_POST) && (($_POST['form_id'] == $form_id) || ($_POST['form_id'] == $form['#base'])))) {
167
    drupal_validate_form($form_id, $form);
168
169
170
    // 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.
171
172
    if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) {
      $redirect = drupal_submit_form($form_id, $form);
173
174
175
      if (!$form['#programmed']) {
        drupal_redirect_form($form, $redirect);
      }
176
177
178
    }
  }

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

/**
 * 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.
 */
196
function drupal_prepare_form($form_id, &$form) {
197
  $form['#type'] = 'form';
198
199
200
201
202
203
204
205
206

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

207
208
209
210
211
212
213
214
215
216
217
  // 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',
    );
  }

218
219
220
221
222
223
224
  // 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'];
  }

225
  if (isset($form['#token'])) {
226
227
228
    // 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.
229
230
    if (variable_get('cache', 0) && !$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET') {
      unset($form['#token']);
231
    }
232
233
234
235
236
    else {
      // Make sure that a private key is set:
      if (!variable_get('drupal_private_key', '')) {
        variable_set('drupal_private_key', mt_rand());
      }
237

238
239
      $form['form_token'] = array('#type' => 'hidden', '#default_value' => md5(session_id() . $form['#token'] . variable_get('drupal_private_key', '')));
    }
240
  }
241

242
  if (isset($form_id)) {
243
    $form['form_id'] = array('#type' => 'hidden', '#value' => $form_id, '#id' => str_replace('_', '-', "edit-$form_id"));
244
  }
245
246
247
  if (!isset($form['#id'])) {
    $form['#id'] = $form_id;
  }
Steven Wittens's avatar
Steven Wittens committed
248

249
  $form += _element_info('form');
250

Dries's avatar
Dries committed
251
252
  if (!isset($form['#validate'])) {
    if (function_exists($form_id .'_validate')) {
253
      $form['#validate'] = array($form_id .'_validate' => array());
Dries's avatar
Dries committed
254
    }
255
256
    elseif (function_exists($base .'_validate')) {
      $form['#validate'] = array($base .'_validate' => array());
Dries's avatar
Dries committed
257
258
259
    }
  }

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

271
272
  foreach (module_implements('form_alter') as $module) {
    $function = $module .'_form_alter';
273
    $function($form_id, $form);
274
275
  }

276
  $form = form_builder($form_id, $form);
277
278
}

279
280
281
282
283
284
285
286
287
288
289
290

/**
 * 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.
 *
 */
291
function drupal_validate_form($form_id, $form) {
292
  global $form_values;
293
294
295
296
297
  static $validated_forms = array();

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

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

308
  _form_validate($form, $form_id);
309
  $validated_forms[$form_id] = TRUE;
310
311
}

312
313
314
315
316
317
318
319
320
321
322
323
324
325
/**
 * 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.
 *
 */
326
function drupal_submit_form($form_id, $form) {
327
328
  global $form_values;
  $default_args = array($form_id, &$form_values);
329

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

345
346
347
348
349
350
351
352
353
354
355
356
357
/**
 * 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.
 *
 */
358
function drupal_render_form($form_id, &$form) {
359
  // Don't override #theme if someone already set it.
360
361
362
363
  if (isset($form['#base'])) {
    $base = $form['#base'];
  }

364
365
366
367
  if (!isset($form['#theme'])) {
    if (theme_get_function($form_id)) {
      $form['#theme'] = $form_id;
    }
368
369
    elseif (theme_get_function($base)) {
      $form['#theme'] = $base;
370
371
372
373
374
375
376
377
378
379
380
    }
  }

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

381
  $output = drupal_render($form);
382
383
384
385
386
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
  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);
    }
  }
}

415
function _form_validate($elements, $form_id = NULL) {
416
417
418
419
420
421
  // Recurse through all children.
  foreach (element_children($elements) as $key) {
    if (isset($elements[$key]) && $elements[$key]) {
      _form_validate($elements[$key]);
    }
  }
422
  /* Validate the current input */
423
  if (!$elements['#validated']) {
424
    if (isset($elements['#needs_validation'])) {
425
426
427
428
      // 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') {
429
        form_error($elements, t('!name field is required.', array('!name' => $elements['#title'])));
430
      }
431
432
433
434
435
436
437
438
439
440
441
442
443
444

      // 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.'));
445
              watchdog('form', t('Illegal choice %choice in !name element.', array('%choice' => $v, '!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'])), WATCHDOG_ERROR);
446
            }
447
448
          }
        }
449
450
        elseif (!isset($options[$elements['#value']])) {
          form_error($elements, t('An illegal choice has been detected. Please contact the site administrator.'));
451
          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);
452
        }
453
454
455
456
      }
    }

    // User-applied checks.
Dries's avatar
Dries committed
457
    if (isset($elements['#validate'])) {
458
459
460
461
462
      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);
463
        }
464
465
        if (function_exists($function))  {
          call_user_func_array($function, $args);
466
467
468
        }
      }
    }
469
    $elements['#validated'] = TRUE;
470
471
472
  }
}

473
474
475
476
477
/**
 * 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.
 */
478
function form_set_error($name = NULL, $message = '') {
479
480
481
  static $form = array();
  if (isset($name) && !isset($form[$name])) {
    $form[$name] = $message;
482
483
484
    if ($message) {
      drupal_set_message($message, 'error');
    }
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
  }
  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];
  }
}

514
515
516
/**
 * Flag an element as having an error.
 */
517
function form_error(&$element, $message = '') {
518
  $element['#error'] = TRUE;
519
  form_set_error(implode('][', $element['#parents']), $message);
520
521
522
}

/**
523
524
525
526
527
528
 * 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
529
530
 *   A unique string identifying the form for validation, submission,
 *   theming, and hook_form_alter functions.
531
532
 * @param $form
 *   An associative array containing the structure of the form.
533
 */
534
function form_builder($form_id, $form) {
535
  global $form_values, $form_submitted, $form_button_counter;
536

537
538
539
  // Initialize as unprocessed.
  $form['#processed'] = FALSE;

540
  /* Use element defaults */
541
  if ((!empty($form['#type'])) && ($info = _element_info($form['#type']))) {
542
    // overlay $info onto $form, retaining preexisting keys in $form
543
544
545
    $form += $info;
  }

546
  if (isset($form['#input']) && $form['#input']) {
547
    if (!isset($form['#name'])) {
548
549
      $name = array_shift($form['#parents']);
      $form['#name'] = $name;
550
551
552
553
554
555
556
      if ($form['#type'] == 'file') {
        // to make it easier to handle $_FILES in file.inc, we place all
        // file fields in the 'files' array. Also, we do not support
        // nested file names
        $form['#name'] = 'files['. $form['#name'] .']';
      }
      elseif (count($form['#parents'])) {
557
558
559
        $form['#name'] .= '['. implode('][', $form['#parents']) .']';
      }
      array_unshift($form['#parents'], $name);
560
561
    }
    if (!isset($form['#id'])) {
562
      $form['#id'] = 'edit-'. implode('-', $form['#parents']);
563
    }
564

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

569
    if (!isset($form['#value']) && !array_key_exists('#value', $form)) {
570
571
      if (($form['#programmed']) || ((!isset($form['#access']) || $form['#access']) && isset($form['#post']) && ($form['#post']['form_id'] == $form_id))) {
        $edit = $form['#post'];
572
573
574
        foreach ($form['#parents'] as $parent) {
          $edit = isset($edit[$parent]) ? $edit[$parent] : NULL;
        }
575
576
577
578
579
580
581
582
583
584
585
586
587
588
        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();
                }
589
              }
590
591
              elseif (isset($edit)) {
                $form['#value'] = $edit;
592
              }
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
              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;
          }
612
613
614
        }
      }
      if (!isset($form['#value'])) {
615
616
617
618
619
620
621
        $function = $form['#type'] . '_value';
        if (function_exists($function)) {
          $function($form);
        }
        else {
          $form['#value'] = $form['#default_value'];
        }
622
      }
623
    }
624
625
626
627
    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
628
      if (isset($form['#post'][$form['#name']]) && $form['#post'][$form['#name']] == $form['#value']) {
629
        $form_submitted = $form_submitted || $form['#executes_submit_callback'];
630
631
632
633
634

        // 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'];
635
636
637
638
      }
    }
  }

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

650
651
652
  // 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.
653
  if (isset($form['#input']) && $form['#input']) {
654
    form_set_value($form, $form['#value']);
655
656
  }

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

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

672
673
    // don't squash existing parents value
    if (!isset($form[$key]['#parents'])) {
674
675
      // 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
676
677
    }

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

686
687
688
689
690
691
  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);
      }
    }
692
    $form['#after_build_done'] = TRUE;
Steven Wittens's avatar
Steven Wittens committed
693
  }
694
695

  return $form;
696
697
}

698
/**
Dries's avatar
Dries committed
699
 * Use this function to make changes to form values in the form validate
700
701
702
703
 * 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
704
 * $form_values['foo']['bar'] to be 'baz'.
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
736
 *
 * @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;
}

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

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

  return $cache[$type];
}

768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
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;
}

787
788
789
790
791
/**
 * Format a dropdown menu or scrolling selection box.
 *
 * @param $element
 *   An associative array containing the properties of the element.
792
 *   Properties used: title, value, options, description, extra, multiple, required
793
794
795
796
797
798
799
800
801
 * @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 = '';
802
  $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : '';
803
  _form_set_class($element, array('form-select'));
804
805
  $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>');
806
807
808
809
810
811
}

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

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

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

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

860
  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";
861
862
863
864
865
866
867
}

/**
 * Format a radio button.
 *
 * @param $element
 *   An associative array containing the properties of the element.
868
 *   Properties used: required, return_value, value, attributes, title, description
869
870
871
872
 * @return
 *   A themed HTML string representing the form item group.
 */
function theme_radio($element) {
873
  _form_set_class($element, array('form-radio'));
874
  $output = '<input type="radio" ';
875
876
877
878
879
880
  $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>';
881
  }
882
883
884

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

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

906
907
908
909
910
911
912
913
914
915
/**
 * 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
916
  return theme('form_element', $element, $element['#children']);
917
918
}

919
920
921
922
/*
 * Expand a password_confirm field into two text boxes.
 */
function expand_password_confirm($element) {
Dries's avatar
Dries committed
923
924
925
926
927
928
929
930
931
932
  $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'],
  );
933
934
935
936
937
938
  $element['#validate'] = array('password_confirm_validate' => array());
  $element['#tree'] = TRUE;

  return $element;
}

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

954
955
956
957
958
959
  // 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);

960
961
962
  return $form;
}

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

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

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

989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
  // 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':
1005
        $options = drupal_map_assoc(range(1, 12), 'map_month');
1006
1007
1008
1009
1010
        break;
      case 'year':
        $options = drupal_map_assoc(range(1900, 2050));
        break;
    }
1011
1012
1013
1014
1015
1016
1017
1018
    $parents = $element['#parents'];
    $parents[] = $type;
    $element[$type] = array(
      '#type' => 'select',
      '#value' => $element['#value'][$type],
      '#attributes' => $element['#attributes'],
      '#options' => $options,
    );
1019
1020
1021
1022
1023
  }

  return $element;
}

1024
1025
1026
1027
1028
1029
1030
1031
1032
/**
 * 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.'));
  }
}

1033
1034
1035
1036
1037
1038
/**
 * 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);
}
1039

1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
/**
 * 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;
}

1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
/**
 * 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;
  }
}

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

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

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

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

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

/**
 * 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) {
1127
  if ($element['#title'] || $element['#description']) {
1128
1129
    unset($element['#id']);
    return theme('form_element', $element, $element['#children']);
1130
1131
  }
  else {
1132
    return $element['#children'];
1133
1134
1135
1136
  }
}

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

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

function theme_button($element) {
1157
1158
1159
1160
1161
1162
1163
  //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'];
  }
1164

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

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

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

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

  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>';
  }

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

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