Projet

Général

Profil

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

root / drupal7 / sites / all / modules / webform / includes / webform.webformconditionals.inc @ 8c72e82a

1
<?php
2

    
3
/**
4
 * @file
5
 * Conditional engine to process dependencies within the webform's conditionals.
6
 */
7

    
8
/**
9
 * Performs analysis and topological sorting on the conditionals.
10
 */
11
class WebformConditionals {
12

    
13
  // Define constants for the result of an analysis of the conditionals on
14
  // a page for a given set of input values. Determines whether the component
15
  // is always hidden, always shown, or may or may not be shown depending upon
16
  // other values on the same page. In the last case, the component needs to be
17
  // rendered on the page because at least one source component is on the same
18
  // page. The field will be hidden with JavaScript.
19
  const componentHidden = 0;
20
  const componentShown = 1;
21
  const componentDependent = 2;
22

    
23
  protected static $conditionals = array();
24

    
25
  protected $node;
26
  protected $topologicalOrder;
27
  protected $pageMap;
28
  protected $childrenMap;
29
  protected $visibilityMap;
30
  protected $requiredMap;
31
  protected $setMap;
32
  protected $markupMap;
33

    
34
  public $errors;
35

    
36
  /**
37
   * Creates and caches a WebformConditional for a given node.
38
   */
39
  static function factory($node) {
40
    if (!isset(self::$conditionals[$node->nid])) {
41
      self::$conditionals[$node->nid] = new WebformConditionals($node);
42
    }
43
    return self::$conditionals[$node->nid];
44
  }
45

    
46
  /**
47
   * Constructs a WebformConditional.
48
   */
49
  function __construct($node) {
50
    $this->node = $node;
51
  }
52

    
53
  /**
54
   * Sorts the conditionals into topological order.
55
   *
56
   * The "nodes" of the list are the conditionals, not the components that
57
   * they operate upon.
58
   *
59
   * The webform components must already be sorted into component tree order
60
   * before calling this method.
61
   *
62
   * See http://en.wikipedia.org/wiki/Topological_sorting
63
   */
64
  protected function topologicalSort() {
65
    $components = $this->node->webform['components'];
66
    $conditionals = $this->node->webform['conditionals'];
67
    $errors = array();
68

    
69
    // Generate a component to conditional map for conditional targets.
70
    $cid_to_target_rgid = array();
71
    $cid_hidden = array();
72
    foreach ($conditionals as $rgid => $conditional) {
73
      foreach ($conditional['actions'] as $aid => $action) {
74
        $target_id = $action['target'];
75
        $cid_to_target_rgid[$target_id][$rgid] = $rgid;
76
        if ($action['action'] == 'show') {
77
          $cid_hidden[$target_id] = isset($cid_hidden[$target_id]) ? $cid_hidden[$target_id] + 1 : 1;
78
          if ($cid_hidden[$target_id] == 2) {
79
            $component = $components[$target_id];
80
            $errors[$component['page_num']][] = t('More than one conditional hides or shows component "@name".',
81
                                                  array('@name' => $component['name']));
82
          }
83
        }
84
      }
85
    }
86

    
87
    // Generate T-Orders for each page
88
    $new_entry = array('in' => array(), 'out' => array(), 'rgid' => array());
89
    $page_num = 0;
90
    // If the first component is a page break, then no component is on page 1. Create empty arrays for page 1.
91
    $sorted = array(1 => array());
92
    $page_map = array(1 => array());
93
    $component = reset($components);
94
    while ($component) {
95
      $cid = $component['cid'];
96

    
97
      // Start a new page, if needed
98
      if ($component['page_num'] > $page_num ) {
99
        $page_num = $component['page_num'];
100
        // Create an empty list that will contain the sorted elements.
101
        // This list is known as L in the literature.
102
        $sorted[$page_num] = array();
103

    
104
        // Create an empty list of dependency nodes for this page.
105
        $nodes = array();
106
      }
107

    
108
      // Create the pageMap as a side benefit of generating the t-sort.
109
      $page_map[$page_num][$cid] = $cid;
110

    
111
      // Process component by adding it's conditional data to a component-tree-traversal order an index of:
112
      // - incoming dependencies = the source components for the conditions for this target component and
113
      // - outgoing dependencies = components which depend upon the target component
114
      // Note: Surprisingly, 0 is a valid rgid, as well as a valid rid. Use -1 as a semaphore.
115
      if (isset($cid_to_target_rgid[$cid])) {
116
        // The component is the target of conditional(s)
117
        foreach ($cid_to_target_rgid[$cid] as $rgid) {
118
          $conditional = $conditionals[$rgid];
119
          if (!isset($nodes[$cid])) {
120
            $nodes[$cid] = $new_entry;
121
          }
122
          $nodes[$cid]['rgid'][$rgid] = $rgid;
123
          foreach ($conditional['rules'] as $rule) {
124
            if ($rule['source_type'] == 'component') {
125
              $source_id = $rule['source'];
126
              if (!isset($nodes[$source_id])) {
127
                $nodes[$source_id] = $new_entry;
128
              }
129
              $nodes[$cid]['in'][$source_id] = $source_id;
130
              $nodes[$source_id]['out'][$cid] = $cid;
131
              $source_component = $components[$source_id];
132
              $source_pid = $source_component['pid'];
133
              if ($source_pid) {
134
                if (!isset($nodes[$source_pid])) {
135
                  $nodes[$source_pid] = $new_entry;
136
                }
137
                // The rule source is within a parent fieldset. Create a dependency on the parent.
138
                $nodes[$source_pid]['out'][$source_id] = $source_id;
139
                $nodes[$source_id]['in'][$source_pid] = $source_pid;
140
              }
141
              if ($source_component['page_num'] > $page_num) {
142
                $errors[$page_num][] = t('A forward reference from page @from, %from to %to was found.',
143
                  array(
144
                    '%from' => $source_component['name'],
145
                    '@from' => $source_component['page_num'],
146
                    '%to' => $component['name'],
147
                  ));
148
              }
149
              elseif ($source_component['page_num'] == $page_num && $component['type'] == 'pagebreak') {
150
                $errors[$page_num][] = t('The page break %to can\'t be controlled by %from on the same page.',
151
                  array(
152
                    '%from' => $source_component['name'],
153
                    '%to' => $component['name'],
154
                  ));
155
              }
156
            }
157
          }
158
        }
159
      }
160

    
161
      // Fetch the next component, if any.
162
      $component = next($components);
163

    
164
      // Finish any previous page already processed.
165
      if (!$component || $component['page_num'] > $page_num) {
166

    
167
        // Create a set of all components which have are not dependent upon anything.
168
        // This list is known as S in the literature.
169
        $start_nodes = array();
170
        foreach ($nodes as $id => $n) {
171
          if (!$n['in']) {
172
            $start_nodes[] = $id;
173
          }
174
        }
175

    
176
        // Process the start nodes, removing each one in turn from the queue.
177
        while ($start_nodes) {
178
          $id = array_shift($start_nodes);
179
          // If the node represents an actual conditional, it can now be added
180
          // to the end of the sorted order because anything it depends upon has
181
          // already been calcuated.
182
          if ($nodes[$id]['rgid']) {
183
            foreach ($nodes[$id]['rgid'] as $rgid) {
184
              $sorted[$page_num][] = array(
185
                'cid' => $id,
186
                'rgid' => $rgid,
187
                'name' => $components[$id]['name'],
188
              );
189

    
190
            }
191
          }
192

    
193
          // Any other nodes that depend upon this node may now have their dependency
194
          // on this node removed, since it has now been calculated.
195
          foreach ($nodes[$id]['out'] as $out_id) {
196
            unset($nodes[$out_id]['in'][$id]);
197
            if (!$nodes[$out_id]['in']) {
198
              $start_nodes[] = $out_id;
199
            }
200
          }
201

    
202
          // All out-going dependencies have been handled.
203
          $nodes[$id]['out'] = array();
204
        }
205

    
206
        // Check for a cyclic graph (circular dependency)
207
        foreach ($nodes as $id => $n) {
208
          if ($n['in'] || $n['out']) {
209
            $errors[$page_num][] = t('A circular reference involving %name was found.',
210
              array('%name' => $components[$id]['name']));
211
          }
212
        }
213

    
214
      } // End finshing previous page
215

    
216
    } // End component loop
217

    
218
    // Create an empty page map for the preview page.
219
    $page_map[$page_num + 1] = array();
220

    
221
    $this->topologicalOrder = $sorted;
222
    $this->errors = $errors;
223
    $this->pageMap = $page_map;
224
  }
225

    
226
  /**
227
   * Returns the (possibly cached) topological sort order.
228
   */
229
  function getOrder() {
230
    if (!$this->topologicalOrder) {
231
      $this->topologicalSort();
232
    }
233
    return $this->topologicalOrder;
234
  }
235

    
236
  /**
237
   * Returns an index of components by page number.
238
   */
239
  function getPageMap() {
240
    if (!$this->pageMap) {
241
      $this->topologicalSort();
242
    }
243
    return $this->pageMap;
244
  }
245

    
246
  /**
247
   * Displays and error messages from the previously-generated sort order.
248
   *
249
   * User's who can't fix the webform are shown a single, simplified message.
250
   */
251
  function reportErrors() {
252
    $this->getOrder();
253
    if ($this->errors) {
254
      if (webform_node_update_access($this->node)) {
255
        foreach ($this->errors as $page_num => $page_errors) {
256
          drupal_set_message(format_plural(count($page_errors),
257
            'Conditional error on page @num:',
258
            'Conditional errors on page @num:',
259
            array('@num' => $page_num)) .
260
            '<br /><ul><li>' . implode('</li><li>', $page_errors) . '</li></ul>', 'warning');
261
        }
262
      }
263
      else {
264
        drupal_set_message(t('This form is improperly configured. Contact the administrator.'), 'warning');
265
      }
266
    }
267
  }
268

    
269
  /**
270
   * Creates and caches a map of the children of a each component.
271
   *
272
   * Called after the component tree has been made and then flattened again.
273
   * Alas, the children data is removed when the tree is flattened. The
274
   * components are indexecd by cid but in tree order. Because cid's are
275
   * numeric, they may not appear in IDE's or debuggers in their actual order.
276
   */
277
  function getChildrenMap() {
278
    if (!$this->childrenMap) {
279
      $map = array();
280
      foreach ($this->node->webform['components'] as $cid => $component) {
281
        $pid = $component['pid'];
282
        if ($pid) {
283
          $map[$pid][] = $cid;
284
        }
285
      }
286

    
287
      $this->childrenMap = $map;
288
    }
289
    return $this->childrenMap;
290
  }
291

    
292
  /**
293
   * Deletes the value of the given component, plus any descendents.
294
   */
295
  protected function deleteFamily(&$input_values, $parent_id, &$page_visiblity_page) {
296
    if (isset($input_values[$parent_id])) {
297
      $input_values[$parent_id] = NULL;
298
    }
299
    if (isset($this->childrenMap[$parent_id])) {
300
      foreach ($this->childrenMap[$parent_id] as $child_id) {
301
        $page_visiblity_page[$child_id] = $page_visiblity_page[$parent_id];
302
        $this->deleteFamily($input_values, $child_id, $page_visiblity_page);
303
      }
304
    }
305
  }
306

    
307
  protected $stackPointer;
308
  protected $resultStack;
309

    
310
  /**
311
   * Initializes an execution stack for a conditional group's rules and
312
   * sub-conditional rules.
313
   */
314
  function executionStackInitialize($andor) {
315
    $this->stackPointer = -1;
316
    $this->resultStack = array();
317
    $this->executionStackPush($andor);
318
  }
319

    
320
  /**
321
   * Starts a new subconditional for the given and/or operator.
322
   */
323
  function executionStackPush($andor) {
324
    $this->resultStack[++$this->stackPointer] = array(
325
      'results' => array(),
326
      'andor' => $andor,
327
    );
328
  }
329

    
330
  /**
331
   * Adds a rule's result to the current sub-condtional.
332
   */
333
  function executionStackAccumulate($result) {
334
    $this->resultStack[$this->stackPointer]['results'][] = $result;
335
  }
336

    
337
  /**
338
   * Finishes a sub-conditional and adds the result to the parent stack frame.
339
   */
340
  function executionStackPop() {
341
    // Calculate the and/or result.
342
    $stack_frame = $this->resultStack[$this->stackPointer];
343
    // Pop stack and protect against stack underflow.
344
    $this->stackPointer = max(0, $this->stackPointer - 1);
345
    $conditional_results = $stack_frame['results'];
346
    $filtered_results = array_filter($conditional_results);
347
    return $stack_frame['andor'] === 'or'
348
              ? count($filtered_results) > 0
349
              : count($filtered_results) === count($conditional_results);
350
  }
351

    
352
  /**
353
   * Executes the conditionals on a submission, removing any data which should
354
   * be hidden.
355
   */
356
  function executeConditionals($input_values, $page_num = 0) {
357
    $this->getOrder();
358
    $this->getChildrenMap();
359
    if (!$this->visibilityMap || $page_num == 0) {
360
      // Create a new visitibily map, with all components shown.
361
      $this->visibilityMap = $this->pageMap;
362
      array_walk_recursive($this->visibilityMap, function (&$status) {
363
        $status = WebformConditionals::componentShown;
364
      });
365
      // Create empty required, set, and markup maps.
366
      $this->requiredMap = array_fill(1, count($this->pageMap), array());
367
      $this->setMap = $this->requiredMap;
368
      $this->markupMap = $this->requiredMap;
369
    }
370
    else {
371
      array_walk($this->visibilityMap[$page_num], function (&$status) {
372
        $status = WebformConditionals::componentShown;
373
      });
374
      $this->requiredMap[$page_num] = array();
375
      $this->setMap[$page_num] = array();
376
      $this->markupMap[$page_num] = array();
377
    }
378

    
379
    module_load_include('inc', 'webform', 'includes/webform.conditionals');
380

    
381
    $components = $this->node->webform['components'];
382
    $conditionals = $this->node->webform['conditionals'];
383
    $operators = webform_conditional_operators();
384
    $targetLocked = array();
385

    
386
    $first_page = $page_num ? $page_num : 1;
387
    $last_page = $page_num ? $page_num : count($this->topologicalOrder);
388
    for ($page = $first_page; $page <= $last_page; $page++) {
389
      foreach ($this->topologicalOrder[$page] as $conditional_spec) {
390

    
391
        $conditional = $conditionals[$conditional_spec['rgid']];
392
        $source_page_nums = array();
393

    
394
        // Execute each comparison callback.
395
        $this->executionStackInitialize($conditional['andor']);
396
        foreach ($conditional['rules'] as $rule) {
397
          switch ($rule['source_type']) {
398
            case 'component':
399
              $source_component = $components[$rule['source']];
400
              $source_cid = $source_component['cid'];
401

    
402
              $source_values = array();
403
              if (isset($input_values[$source_cid])) {
404
                $component_value = $input_values[$source_cid];
405
                // For select_or_other components, use only the select values because $source_values must not be a nested array.
406
                // During preview, the array is already flattened.
407
                if ($source_component['type'] === 'select' &&
408
                    !empty($source_component['extra']['other_option']) &&
409
                    isset($component_value['select'])) {
410
                  $component_value = $component_value['select'];
411
                }
412
                $source_values = is_array($component_value) ? $component_value : array($component_value);
413
              }
414

    
415
              // Determine the operator and callback.
416
              $conditional_type = webform_component_property($source_component['type'], 'conditional_type');
417
              $operator_info = $operators[$conditional_type];
418

    
419
              // Perform the comparison callback and build the results for this group.
420
              $comparison_callback = $operator_info[$rule['operator']]['comparison callback'];
421
              // Contrib caching, such as entitycache, may have loaded the node
422
              // without building it. It is possible that the component include file
423
              // hasn't been included yet. See #2529246.
424
              webform_component_include($source_component['type']);
425

    
426
              // Load missing include files for conditional types.
427
              // In the case of the 'string', 'date', and 'time' conditional types, it is
428
              // not necessary to load their include files for conditional behavior
429
              // because the required functions are already loaded
430
              // in webform.conditionals.inc.
431
              switch ($conditional_type) {
432
                case 'numeric':
433
                  module_load_include('inc', 'webform', 'components/number');
434
                  break;
435
                case 'select':
436
                  module_load_include('inc', 'webform', 'components/select');
437
                  break;
438
              }
439

    
440
              $this->executionStackAccumulate($comparison_callback($source_values, $rule['value'], $source_component));
441

    
442
              // Record page number to later determine any intra-page dependency on this source.
443
              $source_page_nums[$source_component['page_num']] = $source_component['page_num'];
444
              break;
445
            case 'conditional_start':
446
              $this->executionStackPush($rule['operator']);
447
              break;
448
            case 'conditional_end':
449
              $this->executionStackAccumulate($this->executionStackPop());
450
              break;
451
          }
452
        }
453
        $conditional_result = $this->executionStackPop();
454

    
455
        foreach ($conditional['actions'] as $action) {
456
          $action_result = $action['invert'] ? !$conditional_result : $conditional_result;
457
          $target = $action['target'];
458
          $page_num = $components[$target]['page_num'];
459
          switch ($action['action']) {
460
            case 'show':
461
              if (!$action_result) {
462
                $this->visibilityMap[$page_num][$target] = in_array($page_num, $source_page_nums) ? self::componentDependent : self::componentHidden;
463
                $this->deleteFamily($input_values, $target, $this->visibilityMap[$page_num]);
464
                $targetLocked[$target] = TRUE;
465
              }
466
              break;
467
            case 'require':
468
              $this->requiredMap[$page_num][$target] = $action_result;
469
              break;
470
            case 'set':
471
              if ($components[$target]['type'] == 'markup') {
472
                $this->markupMap[$page_num][$target] = FALSE;
473
              }
474
              if ($action_result && empty($targetLocked[$target])) {
475
                if ($components[$target]['type'] == 'markup') {
476
                  $this->markupMap[$page_num][$target] = $action['argument'];
477
                }
478
                else {
479
                  $input_values[$target] = isset($input_values[$target]) && is_array($input_values[$target])
480
                                              ? array($action['argument'])
481
                                              : $action['argument'];
482
                  $this->setMap[$page_num][$target] = TRUE;
483
                }
484
              }
485
              break;
486
          }
487
        }
488

    
489

    
490
      } // End conditinal loop
491
    } // End page loop
492

    
493
    return $input_values;
494
  }
495

    
496
  /**
497
   * Returns whether the conditionals have been executed yet.
498
   */
499
  function isExecuted() {
500
    return (boolean)($this->visibilityMap);
501
  }
502

    
503
  /**
504
   * Returns whether a given component is always hidden, always shown, or might
505
   * be shown depending upon other sources on the same page.
506
   *
507
   * Assumes that the conditionals have already been executed on the given page.
508
   *
509
   * @param integer $cid
510
   *   The component id of the component whose visibilty is being sought.
511
   * @param integer $page_num
512
   *   The page number that the component is on.
513
   * @return integer
514
   *   self::componentHidden, ...Shown, or ...Dependent.
515
   */
516
  function componentVisibility($cid, $page_num) {
517
    if (!$this->visibilityMap) {
518
      // The conditionals have not yet been executed on a submission.
519
      $this->executeConditionals(array(), 0);
520
      watchdog('webform', 'WebformConditionals::componentVisibility called prior to evaluating a submission.', array(), WATCHDOG_ERROR);
521
    }
522
    return isset($this->visibilityMap[$page_num][$cid]) ? $this->visibilityMap[$page_num][$cid] : self::componentShown;
523
  }
524

    
525
  /**
526
   * Returns whether a given page should be displayed. This requires any
527
   * conditional for the page itself to be shown, plus at least one component
528
   * within the page must be shown too. The first and preview pages are always
529
   * shown, however.
530
   *
531
   * @param integer $page_num
532
   *   The page number that the component is on.
533
   * @return integer
534
   *   self::componentHidden or  ...Shown.
535
   */
536
  function pageVisibility($page_num) {
537
    $result = self::componentHidden;
538
    if ($page_num == 1 || empty($this->visibilityMap[$page_num])) {
539
      $result = self::componentShown;
540
    }
541
    elseif (($page_map = $this->pageMap[$page_num]) && $this->componentVisibility(reset($page_map), $page_num)) {
542
      while ($cid = next($page_map)) {
543
        if ($this->componentVisibility($cid, $page_num) != self::componentHidden) {
544
          $result = self::componentShown;
545
          break;
546
        }
547
      }
548
    }
549
    return $result;
550
  }
551

    
552
  /**
553
   * Returns whether a given component is always required, always optional, or
554
   * unchanged by conditional logic.
555
   *
556
   * Assumes that the conditionals have already been executed on the given page.
557
   *
558
   * @param integer $cid
559
   *   The component id of the component whose required state is being sought.
560
   * @param integer $page_num
561
   *   The page number that the component is on.
562
   * @return boolean
563
   *   Whether the component is required based on conditionals.
564
   */
565
  function componentRequired($cid, $page_num) {
566
    if (!$this->requiredMap) {
567
      // The conditionals have not yet been executed on a submission.
568
      $this->executeConditionals(array(), 0);
569
      watchdog('webform', 'WebformConditionals::componentRequired called prior to evaluating a submission.', array(), WATCHDOG_ERROR);
570
    }
571
    return isset($this->requiredMap[$page_num][$cid]) ? $this->requiredMap[$page_num][$cid] : NULL;
572
  }
573

    
574
    /**
575
   * Returns whether a given component has been set by conditional logic.
576
   *
577
   * Assumes that the conditionals have already been executed on the given page.
578
   *
579
   * @param integer $cid
580
   *   The component id of the component whose set state is being sought.
581
   * @param integer $page_num
582
   *   The page number that the component is on.
583
   * @return boolean
584
   *   Whether the component was set based on conditionals.
585
   */
586
  function componentSet($cid, $page_num) {
587
    if (!$this->setMap) {
588
      // The conditionals have not yet been executed on a submission.
589
      $this->executeConditionals(array(), 0);
590
      watchdog('webform', 'WebformConditionals::componentSet called prior to evaluating a submission.', array(), WATCHDOG_ERROR);
591
    }
592
    return isset($this->setMap[$page_num][$cid]) ? $this->setMap[$page_num][$cid] : NULL;
593
  }
594

    
595
    /**
596
   * Returns the calculated markup as set by conditional logic.
597
   *
598
   * Assumes that the conditionals have already been executed on the given page.
599
   *
600
   * @param integer $cid
601
   *   The component id of the component whose set state is being sought.
602
   * @param integer $page_num
603
   *   The page number that the component is on.
604
   * @return string
605
   *   The conditional markup, or NULL if none.
606
   */
607
  function componentMarkup($cid, $page_num) {
608
    if (!$this->markupMap) {
609
      // The conditionals have not yet been executed on a submission.
610
      $this->executeConditionals(array(), 0);
611
      watchdog('webform', 'WebformConditionals::componentMarkup called prior to evaluating a submission.', array(), WATCHDOG_ERROR);
612
    }
613
    return isset($this->markupMap[$page_num][$cid]) ? $this->markupMap[$page_num][$cid] : NULL;
614
  }
615

    
616
}