Projet

Général

Profil

Paste
Télécharger (40,8 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / webform / includes / webform.components.inc @ ca0757b9

1
<?php
2

    
3
/**
4
 * @file
5
 * Webform module component handling.
6
 */
7

    
8
/**
9
 * Provides interface and database handling for editing components of a webform.
10
 *
11
 * @author Nathan Haug <nate@lullabot.com>
12
 */
13

    
14
/**
15
 * Overview page of all components for this webform.
16
 */
17
function webform_components_page($node, $page_number = 1) {
18
  $output = drupal_get_form('webform_components_form', $node);
19

    
20
  return array(
21
    '#theme' => 'webform_components_page',
22
    '#node' => $node,
23
    '#form' => $output,
24
  );
25
}
26

    
27
/**
28
 * Theme the output of the main components page.
29
 *
30
 * This theming provides a way to toggle between the editing modes if Form
31
 * Builder module is available.
32
 */
33
function theme_webform_components_page($variables) {
34
  $node = $variables['node'];
35
  $form = $variables['form'];
36

    
37
  return drupal_render($form);
38
}
39

    
40
/**
41
 * The table-based listing of all components for this webform.
42
 */
43
function webform_components_form($form, $form_state, $node) {
44
  $form = array(
45
    '#tree' => TRUE,
46
    '#node' => $node,
47
    'components' => array(),
48
  );
49

    
50
  $form['nid'] = array(
51
    '#type' => 'value',
52
    '#value' => $node->nid,
53
  );
54

    
55
  $options = array();
56
  foreach ($node->webform['components'] as $cid => $component) {
57
    $options[$cid] = check_plain($component['name']);
58
    $form['components'][$cid]['cid'] = array(
59
      '#type' => 'hidden',
60
      '#default_value' => $component['cid'],
61
    );
62
    $form['components'][$cid]['pid'] = array(
63
      '#type' => 'hidden',
64
      '#default_value' => $component['pid'],
65
    );
66
    $form['components'][$cid]['weight'] = array(
67
      '#type' => 'textfield',
68
      '#size' => 4,
69
      '#title' => t('Weight'),
70
      '#default_value' => $component['weight'],
71
    );
72
    $form['components'][$cid]['mandatory'] = array(
73
      '#type' => 'checkbox',
74
      '#title' => t('Mandatory'),
75
      '#default_value' => $component['mandatory'],
76
      '#access' => webform_component_feature($component['type'], 'required'),
77
    );
78
    if (!isset($max_weight) || $component['weight'] > $max_weight) {
79
      $max_weight = $component['weight'];
80
    }
81
  }
82

    
83
  $form['add']['name'] = array(
84
    '#type' => 'textfield',
85
    '#size' => 24,
86
    '#maxlength' => 255,
87
  );
88

    
89
  $form['add']['type'] = array(
90
    '#type' => 'select',
91
    '#options' => webform_component_options(),
92
    '#weight' => 3,
93
    '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['type'] : 'textfield',
94
  );
95
  $form['add']['mandatory'] = array(
96
    '#type' => 'checkbox',
97
  );
98
  $form['add']['cid'] = array(
99
    '#type' => 'hidden',
100
    '#default_value' => '',
101
  );
102
  $form['add']['pid'] = array(
103
    '#type' => 'hidden',
104
    '#default_value' => (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) ? $node->webform['components'][$_GET['cid']]['pid'] : 0,
105
  );
106
  $form['add']['weight'] = array(
107
    '#type' => 'textfield',
108
    '#size' => 4,
109
    '#delta' => count($node->webform['components']) > 10 ? count($node->webform['components']) : 10,
110
  );
111

    
112
  if (isset($_GET['cid']) && isset($node->webform['components'][$_GET['cid']])) {
113
    // Make the new component appear by default directly after the one that was
114
    // just added.
115
    $form['add']['weight']['#default_value'] = $node->webform['components'][$_GET['cid']]['weight'] + 1;
116
    foreach (array_keys($node->webform['components']) as $cid) {
117
      // Adjust all later components also, to make sure none of them have the
118
      // same weight as the new component.
119
      if ($form['components'][$cid]['weight']['#default_value'] >= $form['add']['weight']['#default_value']) {
120
        $form['components'][$cid]['weight']['#default_value']++;
121
      }
122
    }
123
  }
124
  else {
125
    // If no component was just added, the new component should appear by
126
    // default at the end of the list.
127
    $form['add']['weight']['#default_value'] = isset($max_weight) ? $max_weight + 1 : 0;
128
  }
129

    
130
  $form['add']['add'] = array(
131
    '#type' => 'submit',
132
    '#value' => t('Add'),
133
    '#weight' => 45,
134
    '#validate' => array('webform_components_form_add_validate', 'webform_components_form_validate'),
135
    '#submit' => array('webform_components_form_add_submit'),
136
  );
137

    
138
  $form['actions'] = array('#type' => 'actions');
139
  $form['actions']['submit'] = array(
140
    '#type' => 'submit',
141
    '#value' => t('Save'),
142
    '#weight' => 45,
143
    '#access' => count($node->webform['components']) > 0,
144
  );
145

    
146
  return $form;
147
}
148

    
149
/**
150
 * Theme the node components form. Use a table to organize the components.
151
 *
152
 * @param $form
153
 *   The form array.
154
 * @return
155
 *   Formatted HTML form, ready for display.
156
 */
157
function theme_webform_components_form($variables) {
158
  $form = $variables['form'];
159

    
160
  $form['components']['#attached']['library'][] = array('webform', 'admin');
161

    
162
  // TODO: Attach these. See http://drupal.org/node/732022.
163
  drupal_add_tabledrag('webform-components', 'order', 'sibling', 'webform-weight');
164
  drupal_add_tabledrag('webform-components', 'match', 'parent', 'webform-pid', 'webform-pid', 'webform-cid');
165

    
166
  $node = $form['#node'];
167

    
168
  $header = array(t('Label'), t('Type'), t('Value'), t('Mandatory'), t('Weight'), array('data' => t('Operations'), 'colspan' => 3));
169
  $rows = array();
170

    
171
  // Add a row containing form elements for a new item.
172
  unset($form['add']['name']['#title'], $form['add_type']['#description']);
173
  $form['add']['name']['#attributes']['rel'] = t('New component name');
174
  $form['add']['name']['#attributes']['class'] = array('webform-default-value');
175
  $form['add']['cid']['#attributes']['class'] = array('webform-cid');
176
  $form['add']['pid']['#attributes']['class'] = array('webform-pid');
177
  $form['add']['weight']['#attributes']['class'] = array('webform-weight');
178
  $row_data = array(
179
    drupal_render($form['add']['name']),
180
    drupal_render($form['add']['type']),
181
    '',
182
    drupal_render($form['add']['mandatory']),
183
    drupal_render($form['add']['cid']) . drupal_render($form['add']['pid']) . drupal_render($form['add']['weight']),
184
    array('colspan' => 3, 'data' => drupal_render($form['add']['add'])),
185
  );
186
  $add_form = array('data' => $row_data, 'class' => array('draggable', 'webform-add-form'));
187
  $form_rendered = FALSE;
188

    
189
  if (!empty($node->webform['components'])) {
190
    $component_tree = array();
191
    $page_count = 1;
192
    _webform_components_tree_build($node->webform['components'], $component_tree, 0, $page_count);
193
    $component_tree = _webform_components_tree_sort($component_tree);
194
    // Build the table rows.
195
    function _webform_components_form_rows($node, $cid, $component, $level, &$form, &$rows, &$add_form) {
196
      // Create presentable values.
197
      if (drupal_strlen($component['value']) > 30) {
198
        $component['value'] = drupal_substr($component['value'], 0, 30);
199
        $component['value'] .= '...';
200
      }
201
      $component['value'] = check_plain($component['value']);
202

    
203
      // Remove individual titles from the mandatory and weight fields.
204
      unset($form['components'][$cid]['mandatory']['#title']);
205
      unset($form['components'][$cid]['pid']['#title']);
206
      unset($form['components'][$cid]['weight']['#title']);
207

    
208
      // Add special classes for weight and parent fields.
209
      $form['components'][$cid]['cid']['#attributes']['class'] = array('webform-cid');
210
      $form['components'][$cid]['pid']['#attributes']['class'] = array('webform-pid');
211
      $form['components'][$cid]['weight']['#attributes']['class'] = array('webform-weight');
212

    
213
      // Build indentation for this row.
214
      $indents = '';
215
      for ($n = 1; $n <= $level; $n++) {
216
        $indents .= '<div class="indentation">&nbsp;</div>';
217
      }
218

    
219
      // Add each component to a table row.
220
      $row_data = array(
221
        $indents . filter_xss($component['name']),
222
        $form['add']['type']['#options'][$component['type']],
223
        ($component['value'] == '') ? '-' : $component['value'],
224
        drupal_render($form['components'][$cid]['mandatory']),
225
        drupal_render($form['components'][$cid]['cid']) . drupal_render($form['components'][$cid]['pid']) . drupal_render($form['components'][$cid]['weight']),
226
        l(t('Edit'), 'node/' . $node->nid . '/webform/components/' . $cid, array('query' => drupal_get_destination())),
227
        l(t('Clone'), 'node/' . $node->nid . '/webform/components/' . $cid . '/clone', array('query' => drupal_get_destination())),
228
        l(t('Delete'), 'node/' . $node->nid . '/webform/components/' . $cid . '/delete', array('query' => drupal_get_destination())),
229
      );
230
      $row_class = array('draggable');
231
      if (!webform_component_feature($component['type'], 'group')) {
232
        $row_class[] = 'tabledrag-leaf';
233
      }
234
      if ($component['type'] == 'pagebreak') {
235
        $row_class[] = 'tabledrag-root';
236
        $row_class[] = 'webform-pagebreak';
237
        $row_data[0] = array('class' => array('webform-pagebreak'), 'data' => $row_data[0]);
238
      }
239
      $rows[] = array('data' => $row_data, 'class' => $row_class);
240
      if (isset($component['children']) && is_array($component['children'])) {
241
        foreach ($component['children'] as $cid => $component) {
242
          _webform_components_form_rows($node, $cid, $component, $level + 1, $form, $rows, $add_form);
243
        }
244
      }
245

    
246
      // Add the add form if this was the last edited component.
247
      if (isset($_GET['cid']) && $component['cid'] == $_GET['cid'] && $add_form) {
248
        $add_form['data'][0] = $indents . $add_form['data'][0];
249
        $rows[] = $add_form;
250
        $add_form = FALSE;
251
      }
252
    }
253
    foreach ($component_tree['children'] as $cid => $component) {
254
      _webform_components_form_rows($node, $cid, $component, 0, $form, $rows, $add_form);
255
    }
256
  }
257
  else {
258
    $rows[] = array(array('data' => t('No Components, add a component below.'), 'colspan' => 9));
259
  }
260

    
261
  // Append the add form if not already printed.
262
  if ($add_form) {
263
    $rows[] = $add_form;
264
  }
265

    
266
  $output = '';
267
  $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'webform-components')));
268
  $output .= drupal_render_children($form);
269
  return $output;
270
}
271

    
272
/**
273
 * Validate handler for webform_components_form().
274
 */
275
function webform_components_form_validate($form, &$form_state) {
276
  // Check that no two components end up with the same form key.
277
  $duplicates = array();
278
  $parents = array();
279
  if (isset($form_state['values']['components'])) {
280
    foreach ($form_state['values']['components'] as $cid => $component) {
281
      $form_key = $form['#node']->webform['components'][$cid]['form_key'];
282
      if (isset($parents[$component['pid']]) && ($existing = array_search($form_key, $parents[$component['pid']])) && $existing !== FALSE) {
283
        if (!isset($duplicates[$form_key])) {
284
          $duplicates[$form_key] = array($existing);
285
        }
286
        $duplicates[$form_key][] = $cid;
287
      }
288
      $parents[$component['pid']][$cid] = $form_key;
289
    }
290
  }
291

    
292
  if (!empty($duplicates)) {
293
    $error = t('The form order failed to save because the following elements have same form keys and are under the same parent. Edit each component and give them a unique form key, then try moving them again.');
294
    $items = array();
295
    foreach ($duplicates as $form_key => $cids) {
296
      foreach ($cids as $cid) {
297
        $items[] = _webform_filter_xss($form['#node']->webform['components'][$cid]['name']);
298
      }
299
    }
300

    
301
    form_error($form['components'], $error . theme('item_list', array('items' => $items)));
302
  }
303
}
304

    
305
/**
306
 * Validate handler for webform_component_form() when adding a new component.
307
 */
308
function webform_components_form_add_validate($form, &$form_state) {
309
  // Check that the entered component name is valid.
310
  if (drupal_strlen(trim($form_state['values']['add']['name'])) <= 0) {
311
    form_error($form['add']['name'], t('When adding a new component, the name field is required.'));
312
  }
313
}
314

    
315
/**
316
 * Submit handler for webform_components_form() to save component order.
317
 */
318
function webform_components_form_submit($form, &$form_state) {
319
  $node = node_load($form_state['values']['nid']);
320

    
321
  // Update all mandatory and weight values.
322
  $changes = FALSE;
323
  foreach ($node->webform['components'] as $cid => $component) {
324
    if ($component['pid'] != $form_state['values']['components'][$cid]['pid'] || $component['weight'] != $form_state['values']['components'][$cid]['weight'] || $component['mandatory'] != $form_state['values']['components'][$cid]['mandatory']) {
325
      $changes = TRUE;
326
      $node->webform['components'][$cid]['weight'] = $form_state['values']['components'][$cid]['weight'];
327
      $node->webform['components'][$cid]['mandatory'] = $form_state['values']['components'][$cid]['mandatory'];
328
      $node->webform['components'][$cid]['pid'] = $form_state['values']['components'][$cid]['pid'];
329
    }
330
  }
331

    
332
  if ($changes) {
333
    node_save($node);
334
  }
335

    
336
  drupal_set_message(t('The component positions and mandatory values have been updated.'));
337
}
338

    
339
/**
340
 * Submit handler for webform_components_form() that adds a new component.
341
 */
342
function webform_components_form_add_submit($form, &$form_state) {
343
  $node = node_load($form_state['values']['nid']);
344

    
345
  $component = $form_state['values']['add'];
346
  $form_state['redirect'] = array('node/' . $node->nid . '/webform/components/new/' . $component['type'], array('query' => array('name' => $component['name'], 'mandatory' => $component['mandatory'], 'pid' => $component['pid'], 'weight' => $component['weight'])));
347
}
348

    
349
/**
350
 * Form to configure a webform component.
351
 */
352
function webform_component_edit_form($form, $form_state, $node, $component, $clone = FALSE) {
353
  drupal_set_title(t('Edit component: @name', array('@name' => $component['name'])), PASS_THROUGH);
354

    
355
  $form['#attached']['library'][] = array('webform', 'admin');
356
  $form['#tree'] = TRUE;
357

    
358
  // Print the correct field type specification.
359
  // We always need: name and description.
360
  $form['type'] = array(
361
    '#type' => 'value',
362
    '#value' => $component['type'],
363
  );
364
  $form['nid'] = array(
365
    '#type' => 'value',
366
    '#value' => $node->nid,
367
  );
368
  $form['cid'] = array(
369
    '#type' => 'value',
370
    '#value' => isset($component['cid']) ? $component['cid'] : NULL,
371
  );
372
  $form['clone'] = array(
373
    '#type' => 'value',
374
    '#value' => $clone,
375
  );
376

    
377
  if (webform_component_feature($component['type'], 'title')) {
378
    $form['name'] = array(
379
      '#type' => 'textfield',
380
      '#default_value' => $component['name'],
381
      '#title' => t('Label'),
382
      '#description' => t('This is used as a descriptive label when displaying this form element.'),
383
      '#required' => TRUE,
384
      '#weight' => -10,
385
      '#maxlength' => 255,
386
    );
387
  }
388

    
389
  $form['form_key'] = array(
390
    '#type' => 'textfield',
391
    '#default_value' => empty($component['form_key']) ? _webform_safe_name($component['name']) : $component['form_key'],
392
    '#title' => t('Field Key'),
393
    '#description' => t('Enter a machine readable key for this form element. May contain only alphanumeric characters and underscores. This key will be used as the name attribute of the form element. This value has no effect on the way data is saved, but may be helpful if doing custom form processing.'),
394
    '#required' => TRUE,
395
    '#weight' => -9,
396
  );
397

    
398
  $form['extra'] = array();
399
  if (webform_component_feature($component['type'], 'description')) {
400
    $form['extra']['description'] = array(
401
      '#type' => 'textarea',
402
      '#default_value' => isset($component['extra']['description']) ? $component['extra']['description'] : '',
403
      '#title' => t('Description'),
404
      '#description' => t('A short description of the field used as help for the user when he/she uses the form.') . theme('webform_token_help'),
405
      '#weight' => -1,
406
    );
407
  }
408

    
409
  // Display settings.
410
  $form['display'] = array(
411
    '#type' => 'fieldset',
412
    '#title' => t('Display'),
413
    '#collapsible' => TRUE,
414
    '#collapsed' => FALSE,
415
    '#weight' => 8,
416
  );
417
  if (webform_component_feature($component['type'], 'title_display')) {
418
    if (webform_component_feature($component['type'], 'title_inline')) {
419
      $form['display']['title_display'] = array(
420
        '#type' => 'select',
421
        '#title' => t('Label display'),
422
        '#default_value' => !empty($component['extra']['title_display']) ? $component['extra']['title_display'] : 'before',
423
        '#options' => array(
424
          'before' => t('Above'),
425
          'inline' => t('Inline'),
426
          'none' => t('None'),
427
        ),
428
        '#description' => t('Determines the placement of the component\'s label.'),
429
      );
430
    }
431
    else {
432
      $form['display']['title_display'] = array(
433
        '#type' => 'checkbox',
434
        '#title' => t('Hide label'),
435
        '#default_value' => strcmp($component['extra']['title_display'], 'none') === 0,
436
        '#return_value' => 'none',
437
        '#description' => t('Do not display the label of this component.'),
438
      );
439
    }
440
    $form['display']['title_display']['#weight'] = 8;
441
    $form['display']['title_display']['#parents'] = array('extra', 'title_display');
442
  }
443

    
444
  if (webform_component_feature($component['type'], 'private')) {
445
    // May user mark fields as Private?
446
    $form['display']['private'] = array(
447
      '#type' => 'checkbox',
448
      '#title' => t('Private'),
449
      '#default_value' => ($component['extra']['private'] == '1' ? TRUE : FALSE),
450
      '#description' => t('Private fields are shown only to users with results access.'),
451
      '#weight' => 45,
452
      '#parents' => array('extra', 'private'),
453
      '#disabled' => empty($node->nid) ? FALSE : !webform_results_access($node),
454
    );
455
  }
456

    
457
  // Validation settings.
458
  $form['validation'] = array(
459
    '#type' => 'fieldset',
460
    '#title' => t('Validation'),
461
    '#collapsible' => TRUE,
462
    '#collapsed' => FALSE,
463
    '#weight' => 5,
464
  );
465
  if (webform_component_feature($component['type'], 'required')) {
466
    $form['validation']['mandatory'] = array(
467
      '#type' => 'checkbox',
468
      '#title' => t('Mandatory'),
469
      '#default_value' => ($component['mandatory'] == '1' ? TRUE : FALSE),
470
      '#description' => t('Check this option if the user must enter a value.'),
471
      '#weight' => -1,
472
      '#parents' => array('mandatory'),
473
    );
474
  }
475

    
476
  // Position settings, only shown if JavaScript is disabled.
477
  $form['position'] = array(
478
    '#type' => 'fieldset',
479
    '#title' => t('Position'),
480
    '#collapsible' => TRUE,
481
    '#collapsed' => TRUE,
482
    '#tree' => FALSE,
483
    '#weight' => 20,
484
    '#attributes' => array('class' => array('webform-position')),
485
  );
486

    
487
  $options = array('0' => t('Root'));
488
  foreach ($node->webform['components'] as $existing_cid => $value) {
489
    if (webform_component_feature($value['type'], 'group') && (!isset($component['cid']) || $existing_cid != $component['cid'])) {
490
      $options[$existing_cid] = $value['name'];
491
    }
492
  }
493
  $form['position']['pid'] = array(
494
    '#type' => 'select',
495
    '#title' => t('Parent'),
496
    '#default_value' => $component['pid'],
497
    '#description' => t('Optional. You may organize your form by placing this component inside another fieldset.'),
498
    '#options' => $options,
499
    '#access' => count($options) > 1,
500
    '#weight' => 3,
501
  );
502
  $form['position']['weight'] = array(
503
    '#type' => 'textfield',
504
    '#size' => 4,
505
    '#title' => t('Weight'),
506
    '#default_value' => $component['weight'],
507
    '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
508
    '#weight' => 4,
509
  );
510

    
511
  // Add conditional fields.
512
  $conditional_components = array();
513
  $counter = 0;
514
  $last_pagebreak_slice = 0;
515
  foreach ($node->webform['components'] as $cid => $test_component) {
516
    // Only components before the pagebreak can be considered.
517
    if ($test_component['type'] == 'pagebreak') {
518
      $last_pagebreak_slice = $counter;
519
    }
520
    if (isset($component['cid']) && $cid == $component['cid']) {
521
      break;
522
    }
523
    if (webform_component_feature($test_component['type'], 'conditional')) {
524
      $conditional_components[$cid] = $test_component;
525
      $counter++;
526
    }
527
  }
528
  if ($component['type'] != 'pagebreak') {
529
    $fieldset_description = t('Create a rule to control whether or not to skip this page.');
530
  }
531
  else {
532
    $fieldset_description = t('Create a rule to control whether or not to show this form element.');
533
  }
534
  $conditional_components = array_slice($conditional_components, 0, $last_pagebreak_slice, TRUE);
535
  $form['conditional'] = array(
536
    '#weight' => 10,
537
    '#type' => 'fieldset',
538
    '#title' => t('Conditional rules'),
539
    '#collapsible' => TRUE,
540
    '#collapsed' => TRUE,
541
    '#description' => t('Create a rule to control whether or not to show this form element.'),
542
    '#tree' => FALSE,
543
  );
544
  $form['conditional']['extra'] = array(
545
    '#tree' => TRUE,
546
  );
547
  $form['conditional']['extra']['conditional_component'] = array(
548
    '#type' => 'select',
549
    '#title' => t('Component'),
550
    '#options' => webform_component_list($node, $conditional_components, FALSE, TRUE),
551
    '#description' => t('Select another component to decide whether to show or hide this component. You can only select components occurring before the most recent pagebreak.'),
552
    '#default_value' => $component['extra']['conditional_component'],
553
  );
554
  $form['conditional']['extra']['conditional_operator'] = array(
555
    '#type' => 'select',
556
    '#title' => t('Operator'),
557
    '#options' => array(
558
      '=' => t('Is one of'),
559
      '!=' => t('Is not one of')
560
    ),
561
    '#description' => t('Determines whether the list below is inclusive or exclusive.'),
562
    '#default_value' => $component['extra']['conditional_operator'],
563
  );
564
  $form['conditional']['extra']['conditional_values'] = array(
565
    '#type' => 'textarea',
566
    '#title' => t('Values'),
567
    '#description' => t('List values, one per line, that will trigger this action. If you leave this blank, this component will always display.'),
568
    '#default_value' => $component['extra']['conditional_values'],
569
  );
570
  if (empty($conditional_components)) {
571
    $form['conditional']['#access'] = FALSE;
572
  }
573

    
574
  // Add the fields specific to this component type:
575
  $additional_form_elements = (array) webform_component_invoke($component['type'], 'edit', $component);
576
  if (empty($additional_form_elements)) {
577
    drupal_set_message(t('The webform component of type @type does not have an edit function defined.', array('@type' => $component['type'])));
578
  }
579

    
580
  // Merge the additional fields with the current fields:
581
  if (isset($additional_form_elements['extra'])) {
582
    $form['extra'] = array_merge($form['extra'], $additional_form_elements['extra']);
583
    unset($additional_form_elements['extra']);
584
  }
585
  if (isset($additional_form_elements['position'])) {
586
    $form['position'] = array_merge($form['position'], $additional_form_elements['position']);
587
    unset($additional_form_elements['position']);
588
  }
589
  if (isset($additional_form_elements['display'])) {
590
    $form['display'] = array_merge($form['display'], $additional_form_elements['display']);
591
    unset($additional_form_elements['display']);
592
  }
593
  if (isset($additional_form_elements['validation'])) {
594
    $form['validation'] = array_merge($form['validation'], $additional_form_elements['validation']);
595
    unset($additional_form_elements['validation']);
596
  }
597
  elseif (count(element_children($form['validation'])) == 0) {
598
    unset($form['validation']);
599
  }
600
  $form = array_merge($form, $additional_form_elements);
601

    
602
  // Add the submit button.
603
  $form['actions'] = array('#type' => 'actions');
604
  $form['actions']['submit'] = array(
605
    '#type' => 'submit',
606
    '#value' => t('Save component'),
607
    '#weight' => 50,
608
  );
609

    
610
  // Remove fieldsets without any child form controls.
611
  foreach ($form as $group_key => $group) {
612
    if (isset($group['#type']) && $group['#type'] === 'fieldset' && !element_children($group)) {
613
      unset($form[$group_key]);
614
    }
615
  }
616

    
617
  return $form;
618
}
619

    
620
/**
621
 * Field name validation for the webform unique key. Must be alphanumeric.
622
 */
623
function webform_component_edit_form_validate($form, &$form_state) {
624
  $node = node_load($form_state['values']['nid']);
625

    
626
  if (!preg_match('/^[a-z0-9_]+$/i', $form_state['values']['form_key'])) {
627
    form_set_error('form_key', t('The field key %field_key is invalid. Please include only lowercase alphanumeric characters and underscores.', array('%field_key' => $form_state['values']['form_key'])));
628
  }
629

    
630
  foreach ($node->webform['components'] as $cid => $component) {
631
    if (($component['cid'] != $form_state['values']['cid'] || $form_state['values']['clone']) && ($component['pid'] == $form_state['values']['pid']) && (strcasecmp($component['form_key'], $form_state['values']['form_key']) == 0)) {
632
      form_set_error('form_key', t('The field key %field_key is already in use by the field labeled %existing_field. Please use a unique key.', array('%field_key' => $form_state['values']['form_key'], '%existing_field' => $component['name'])));
633
    }
634
  }
635
}
636

    
637
/**
638
 * Submit handler for webform_component_edit_form().
639
 */
640
function webform_component_edit_form_submit($form, &$form_state) {
641
  // Ensure a webform record exists.
642
  $node = node_load($form_state['values']['nid']);
643
  webform_ensure_record($node);
644

    
645
  // Remove empty extra values.
646
  if (isset($form_state['values']['extra'])) {
647
    foreach ($form_state['values']['extra'] as $key => $value) {
648
      if ($value  === '' && !isset($form['display'][$key]['#options'][''])) {
649
        unset($form_state['values']['extra'][$key]);
650
      }
651
    }
652
  }
653

    
654
  // Remove empty attribute values.
655
  if (isset($form_state['values']['extra']['attributes'])) {
656
    foreach ($form_state['values']['extra']['attributes'] as $key => $value) {
657
      if ($value === '') {
658
        unset($form_state['values']['extra']['attributes'][$key]);
659
      }
660
    }
661
  }
662

    
663
  if ($form_state['values']['clone']) {
664
    webform_component_clone($node, $form_state['values']);
665
    drupal_set_message(t('Component %name cloned.', array('%name' => $form_state['values']['name'])));
666
  }
667
  elseif (!empty($form_state['values']['cid'])) {
668
    webform_component_update($form_state['values']);
669
    drupal_set_message(t('Component %name updated.', array('%name' => $form_state['values']['name'])));
670
  }
671
  else {
672
    $cid = webform_component_insert($form_state['values']);
673
    drupal_set_message(t('New component %name added.', array('%name' => $form_state['values']['name'])));
674
  }
675

    
676
  // Since Webform components have been updated but the node itself has not
677
  // been saved, it is necessary to explicitly clear the cache to make sure
678
  // the updated webform is visible to anonymous users.
679
  cache_clear_all();
680

    
681
  // Clear the entity cache if Entity Cache module is installed.
682
  if (module_exists('entitycache')) {
683
    cache_clear_all($node->nid, 'cache_entity_node');
684
  }
685

    
686
  $form_state['redirect'] = array('node/' . $node->nid . '/webform/components', isset($cid) ? array('query' => array('cid' => $cid)) : array());
687
}
688

    
689
/**
690
 * Form to confirm deletion of a component.
691
 */
692
function webform_component_delete_form($form, $form_state, $node, $component) {
693
  $cid = $component['cid'];
694

    
695
  $form = array();
696
  $form['node'] = array(
697
    '#type' => 'value',
698
    '#value' => $node,
699
  );
700
  $form['component'] = array(
701
    '#type' => 'value',
702
    '#value' => $component,
703
  );
704

    
705
  if (webform_component_feature($node->webform['components'][$cid]['type'], 'group')) {
706
    $question = t('Delete the %name fieldset?', array('%name' => $node->webform['components'][$cid]['name']));
707
    $description = t('This will immediately delete the %name fieldset and all children elements within %name from the %webform webform. This cannot be undone.', array('%name' => $node->webform['components'][$cid]['name'], '%webform' => $node->title));
708
  }
709
  else {
710
    $question = t('Delete the %name component?', array('%name' => $node->webform['components'][$cid]['name']));
711
    $description = t('This will immediately delete the %name component from the %webform webform. This cannot be undone.', array('%name' => $node->webform['components'][$cid]['name'], '%webform' => $node->title));
712
  }
713

    
714
  return confirm_form($form, $question, 'node/' . $node->nid . '/webform/components', $description, t('Delete'));
715
}
716

    
717
/**
718
 * Submit handler for webform_component_delete_form().
719
 */
720
function webform_component_delete_form_submit($form, &$form_state) {
721
  // Delete the component.
722
  $node = $form_state['values']['node'];
723
  $component = $form_state['values']['component'];
724
  webform_component_delete($node, $component);
725
  drupal_set_message(t('Component %name deleted.', array('%name' => $component['name'])));
726

    
727
  // Check if this webform still contains any information.
728
  unset($node->webform['components'][$component['cid']]);
729
  webform_check_record($node);
730

    
731
  // Since Webform components have been updated but the node itself has not
732
  // been saved, it is necessary to explicitly clear the cache to make sure
733
  // the updated webform is visible to anonymous users.
734
  cache_clear_all();
735

    
736
  // Clear the entity cache if Entity Cache module is installed.
737
  if (module_exists('entitycache')) {
738
    cache_clear_all($node->nid, 'cache_entity_node');
739
  }
740

    
741
  $form_state['redirect'] = 'node/' . $node->nid . '/webform/components';
742
}
743

    
744
/**
745
 * Insert a new component into the database.
746
 *
747
 * @param $component
748
 *   A full component containing fields from the component form.
749
 */
750
function webform_component_insert(&$component) {
751
  // Allow modules to modify the component before saving.
752
  foreach (module_implements('webform_component_presave') as $module) {
753
    $function = $module . '_webform_component_presave';
754
    $function($component);
755
  }
756

    
757
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
758
  $component['mandatory'] = isset($component['mandatory']) ? $component['mandatory'] : 0;
759
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
760

    
761
  if (!isset($component['cid'])) {
762
    if (lock_acquire('webform_component_insert_' . $component['nid'], 5)) {
763
      $next_id_query = db_select('webform_component')->condition('nid', $component['nid']);
764
      $next_id_query->addExpression('MAX(cid) + 1', 'cid');
765
      $component['cid'] = $next_id_query->execute()->fetchField();
766
      if ($component['cid'] == NULL) {
767
        $component['cid'] = 1;
768
      }
769
      lock_release('webform_component_insert_' . $component['nid']);
770
    }
771
    else {
772
      watchdog('webform', 'A Webform component could not be saved because a timeout occurred while trying to acquire a lock for the node. Details: <pre>@component</pre>', array('@component' => print_r($component, TRUE)));
773
      return FALSE;
774
    }
775
  }
776

    
777
  $query = db_insert('webform_component')
778
    ->fields(array(
779
      'nid' => $component['nid'],
780
      'cid' => $component['cid'],
781
      'pid' => $component['pid'],
782
      'form_key' => $component['form_key'],
783
      'name' => $component['name'],
784
      'type' => $component['type'],
785
      'value' => (string) $component['value'],
786
      'extra' => serialize($component['extra']),
787
      'mandatory' => $component['mandatory'],
788
      'weight' => $component['weight'],
789
    ))
790
    ->execute();
791

    
792
  // Post-insert actions.
793
  module_invoke_all('webform_component_insert', $component);
794

    
795
  return $component['cid'];
796
}
797

    
798
/**
799
 * Update an existing component with new values.
800
 *
801
 * @param $component
802
 *   A full component containing a nid, cid, and all other fields from the
803
 *   component form. Additional properties are stored in the extra array.
804
 */
805
function webform_component_update($component) {
806
  // Allow modules to modify the component before saving.
807
  foreach (module_implements('webform_component_presave') as $module) {
808
    $function = $module . '_webform_component_presave';
809
    $function($component);
810
  }
811

    
812
  $component['value'] = isset($component['value']) ? $component['value'] : NULL;
813
  $component['mandatory'] = isset($component['mandatory']) ? $component['mandatory'] : 0;
814
  $component['extra']['private'] = isset($component['extra']['private']) ? $component['extra']['private'] : 0;
815
  db_update('webform_component')
816
    ->fields(array(
817
      'pid' => $component['pid'],
818
      'form_key' => $component['form_key'],
819
      'name' => $component['name'],
820
      'type' => $component['type'],
821
      'value' => isset($component['value']) ? $component['value'] : '',
822
      'extra' => serialize($component['extra']),
823
      'mandatory' => $component['mandatory'],
824
      'weight' => $component['weight']
825
    ))
826
    ->condition('nid', $component['nid'])
827
    ->condition('cid', $component['cid'])
828
    ->execute();
829

    
830
  // Post-update actions.
831
  module_invoke_all('webform_component_update', $component);
832
}
833

    
834
function webform_component_delete($node, $component) {
835
  // Check if a delete function is available for this component. If so,
836
  // load all submissions and allow the component to delete each one.
837

    
838
  webform_component_include($component['type']);
839
  $delete_function = '_webform_delete_' . $component['type'];
840
  if (function_exists($delete_function)) {
841
    module_load_include('inc', 'webform', 'includes/webform.submissions');
842
    $submissions = webform_get_submissions($node->nid);
843
    foreach ($submissions as $submission) {
844
      if (isset($submission->data[$component['cid']])) {
845
        webform_component_invoke($component['type'], 'delete', $component, $submission->data[$component['cid']]['value']);
846
      }
847
    }
848
  }
849

    
850
  // Remove database entries.
851
  db_delete('webform_component')
852
    ->condition('nid', $node->nid)
853
    ->condition('cid', $component['cid'])
854
    ->execute();
855
  db_delete('webform_submitted_data')
856
    ->condition('nid', $node->nid)
857
    ->condition('cid', $component['cid'])
858
    ->execute();
859

    
860
  // Delete all elements under this element.
861
  $result = db_select('webform_component', 'c')
862
    ->fields('c')
863
    ->condition('nid', $node->nid)
864
    ->condition('pid', $component['cid'])
865
    ->execute();
866
  foreach ($result as $row) {
867
    $child_component = $node->webform['components'][$row->cid];
868
    webform_component_delete($node, $child_component);
869
  }
870

    
871
  // Post-delete actions.
872
  module_invoke_all('webform_component_delete', $component);
873
}
874

    
875
/**
876
 * Recursively insert components into the database.
877
 *
878
 * @param $node
879
 *   The node object containing the current webform.
880
 * @param $component
881
 *   A full component containing fields from the component form.
882
 */
883
function webform_component_clone(&$node, &$component) {
884
  $original_cid = $component['cid'];
885
  $component['cid'] = NULL;
886
  $new_cid = webform_component_insert($component);
887
  $component['cid'] = $new_cid;
888
  if (webform_component_feature($component['type'], 'group')) {
889
    foreach ($node->webform['components'] as $cid => $child_component) {
890
      if ($child_component['pid'] == $original_cid) {
891
        $child_component['pid'] = $new_cid;
892
        webform_component_clone($node, $child_component);
893
      }
894
    }
895
  }
896
  return $new_cid;
897
}
898

    
899
/**
900
 * Check if a component has a particular feature.
901
 *
902
 * @see hook_webform_component_info()
903
 */
904
function webform_component_feature($type, $feature) {
905
  $components = webform_components();
906
  $defaults = array(
907
    'csv' => TRUE,
908
    'default_value' => TRUE,
909
    'description' => TRUE,
910
    'email' => TRUE,
911
    'email_address' => FALSE,
912
    'email_name' => FALSE,
913
    'required' => TRUE,
914
    'title' => TRUE,
915
    'title_display' => TRUE,
916
    'title_inline' => TRUE,
917
    'conditional' => TRUE,
918
    'spam_analysis' => FALSE,
919
    'group' => FALSE,
920
    'attachment' => FALSE,
921
    'private' => TRUE,
922
  );
923
  return isset($components[$type]['features'][$feature]) ? $components[$type]['features'][$feature] : !empty($defaults[$feature]);
924
}
925

    
926
/**
927
 * Create a list of components suitable for a select list.
928
 *
929
 * @param $node
930
 *   The webform node.
931
 * @param $component_filter
932
 *   Either an array of components, or a string containing a feature name (csv,
933
 *   email, required, conditional) on which this list of components will be
934
 *   restricted.
935
 * @param $indent
936
 *   Indent components placed under fieldsets with hyphens.
937
 * @param $optgroups
938
 *   Determine if pagebreaks should be converted to option groups in the
939
 *   returned list of options.
940
 */
941
function webform_component_list($node, $component_filter = NULL, $indent = TRUE, $optgroups = FALSE) {
942
  $options = array();
943
  $page_names = array();
944

    
945
  $components = is_array($component_filter) ? $component_filter : $node->webform['components'];
946
  $feature = is_string($component_filter) ? $component_filter : NULL;
947

    
948
  foreach ($components as $cid => $component) {
949
    if (!isset($feature) || webform_component_feature($component['type'], $feature) || ($indent && webform_component_feature($component['type'], 'group'))) {
950
      $prefix = '';
951
      $page_num = $component['page_num'];
952
      $page_index = 'p' . $page_num;
953
      if ($indent && ($parent_count = count(webform_component_parent_keys($node, $component)) - 1)) {
954
        $prefix = str_repeat('-', $parent_count);
955
      }
956
      if ($optgroups && $component['type'] == 'pagebreak') {
957
        $page_names[$page_index] = $component['name'];
958
      }
959
      elseif ($optgroups && $page_num > 1) {
960
        $options[$page_index][$cid] = $prefix . $component['name'];
961
      }
962
      else {
963
        $options[$cid] = $prefix . $component['name'];
964
      }
965
    }
966
  }
967

    
968
  // Convert page breaks into optgroups.
969
  if ($optgroups) {
970
    $grouped_options = $options;
971
    $options = array();
972
    foreach ($grouped_options as $key => $values) {
973
      if (is_array($values) && isset($page_names[$key])) {
974
        $options[$page_names[$key]] = $values;
975
      }
976
      else {
977
        $options[$key] = $values;
978
      }
979
    }
980
  }
981

    
982
  return $options;
983
}
984

    
985
/**
986
 * A Form API process function to expand a component list into checkboxes.
987
 */
988
function webform_component_select($element) {
989
  // Split the select list into checkboxes.
990
  foreach ($element['#options'] as $key => $label) {
991
    $label_length = strlen($label);
992
    $label = preg_replace('/^(\-)+/', '', $label);
993
    $indents = $label_length - strlen($label);
994
    $element[$key] = array(
995
      '#title' => $label,
996
      '#type' => 'checkbox',
997
      '#default_value' => array_search($key, $element['#value']) !== FALSE,
998
      '#return_value' => $key,
999
      '#parents' => array_merge($element['#parents'], array($key)),
1000
      '#indent' => $indents,
1001
    );
1002
  }
1003

    
1004
  $element['#theme_wrappers'] = array();
1005
  $element['#type'] = 'webform_component_select';
1006
  $element['#theme'] = 'webform_component_select';
1007
  $element['#attached'] = array(
1008
    'library' => array(
1009
      array('webform', 'admin'),
1010
    ),
1011
    'js' => array(
1012
      'misc/tableselect.js' => array(),
1013
    ),
1014
  );
1015

    
1016
  return $element;
1017
}
1018

    
1019
/**
1020
 * Theme the contents of a Webform component select element.
1021
 */
1022
function theme_webform_component_select($variables) {
1023
  $element = $variables['element'];
1024

    
1025
  $rows = array();
1026
  $header = array();
1027
  if (!isset($element['#all_checkbox']) || $element['#all_checkbox']) {
1028
    $header = array(array('class' => array('select-all'), 'data' => ' ' . t('Include all components')));
1029
  }
1030
  foreach (element_children($element) as $key) {
1031
    $rows[] = array(
1032
      theme('indentation', array('size' => $element[$key]['#indent'])) . drupal_render($element[$key]),
1033
    );
1034
  }
1035

    
1036
  $element['#type'] = 'fieldset';
1037
  $element['#value'] = NULL;
1038
  $element['#attributes']['class'] = array('webform-component-select-table');
1039
  if (!isset($element['#collapsible']) || $element['#collapsible']) {
1040
    $element['#attributes']['class'][] = 'collapsible';
1041
  }
1042
  if (!isset($element['#collapsed']) || $element['#collapsed']) {
1043
    $element['#attributes']['class'][] = 'collapsed';
1044
  }
1045

    
1046
  if (empty($rows)) {
1047
    $element['#children'] = t('No available components.');
1048
  }
1049
  else {
1050
    $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows)) . '</div>';
1051
  }
1052

    
1053
  return theme('fieldset', array('element' => $element));
1054
}
1055

    
1056
/**
1057
 * Find a components parents within a node.
1058
 */
1059
function webform_component_parent_keys($node, $component) {
1060
  $parents = array($component['form_key']);
1061
  $pid = $component['pid'];
1062
  while ($pid) {
1063
    $parents[] = $node->webform['components'][$pid]['form_key'];
1064
    $pid = $node->webform['components'][$pid]['pid'];
1065
  }
1066
  return array_reverse($parents);
1067
}
1068

    
1069
/**
1070
 * Populate a component with the defaults for that type.
1071
 */
1072
function webform_component_defaults(&$component) {
1073
  $defaults = webform_component_invoke($component['type'], 'defaults');
1074
  drupal_alter('webform_component_defaults', $defaults, $component['type']);
1075
  if (!empty($defaults)) {
1076
    foreach ($defaults as $key => $default) {
1077
      if (!isset($component[$key])) {
1078
        $component[$key] = $default;
1079
      }
1080
    }
1081
    foreach ($defaults['extra'] as $extra => $default) {
1082
      if (!isset($component['extra'][$extra])) {
1083
        $component['extra'][$extra] = $default;
1084
      }
1085
    }
1086
    $component['extra'] += array(
1087
      'conditional_component' => '',
1088
      'conditional_operator' => '=',
1089
      'conditional_values' => '',
1090
    );
1091
  }
1092
}
1093

    
1094
/**
1095
 * Validate an element value is unique with no duplicates in the database.
1096
 */
1097
function webform_validate_unique($element, $form_state) {
1098
  if ($element['#value'] !== '') {
1099
    $nid = $form_state['values']['details']['nid'];
1100
    $sid = $form_state['values']['details']['sid'];
1101
    $query = db_select('webform_submitted_data')
1102
      ->fields('webform_submitted_data', array('sid'))
1103
      ->condition('nid', $nid)
1104
      ->condition('cid', $element['#webform_component']['cid'])
1105
      ->condition('data', $element['#value'])
1106
      ->range(0, 1); // More efficient than using countQuery() for data checks.
1107
    if ($sid) {
1108
      $query->condition('sid', $sid, '<>');
1109
    }
1110
    $count = $query->execute()->fetchField();
1111
    if ($count) {
1112
      form_error($element, t('The value %value has already been submitted once for the %title field. You may have already submitted this form, or you need to use a different value.', array('%value' => $element['#value'], '%title' => $element['#title'])));
1113
    }
1114
  }
1115
}