Projet

Général

Profil

Paste
Télécharger (47,1 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / rules / ui / ui.core.inc @ 950416da

1
<?php
2

    
3
/**
4
 * @file
5
 * Contains core Rules UI functions.
6
 */
7

    
8
/**
9
 * Plugin UI Interface.
10
 */
11
interface RulesPluginUIInterface {
12

    
13
  /**
14
   * Adds the whole configuration form of this rules configuration.
15
   *
16
   * For rule elements that are part of a configuration this method just adds
17
   * the elements configuration form.
18
   *
19
   * @param $form
20
   *   The form array where to add the form.
21
   * @param $form_state
22
   *   The current form state.
23
   * @param $options
24
   *   An optional array of options with the known keys:
25
   *    - 'show settings': Whether to include the 'settings' fieldset for
26
   *      editing configuration settings like the label or categories. Defaults
27
   *      to FALSE.
28
   *    - 'button': Whether a submit button should be added. Defaults to FALSE.
29
   *    - 'init': Whether the element is about to be configured the first time
30
   *      and the configuration is about to be initialized. Defaults to FALSE.
31
   *    - 'restrict plugins: May be used to restrict the list of rules plugins
32
   *      that may be added to this configuration. For that set an array of
33
   *      valid plugins. Note that conditions and actions are always valid, so
34
   *      just set an empty array for just allowing those.
35
   *    - 'restrict conditions': Optionally set an array of condition names to
36
   *      restrict the conditions that are available for adding.
37
   *    - 'restrict actions': Optionally set an array of action names to
38
   *      restrict the actions that are available to for adding.
39
   *    - 'restrict events': Optionally set an array of event names to restrict
40
   *      the events that are available for adding.
41
   *
42
   * @todo Implement the 'restrict *' options.
43
   */
44
  public function form(&$form, &$form_state, $options = array());
45

    
46
  /**
47
   * Validate the configuration form of this rule element.
48
   *
49
   * @param $form
50
   * @param $form_state
51
   */
52
  public function form_validate($form, &$form_state);
53

    
54
  /**
55
   * Form submit handler for the element configuration form.
56
   *
57
   * Submit the configuration form of this rule element. This makes sure to
58
   * put the updated configuration in the form state. For saving changes
59
   * permanently, just call $config->save() afterwards.
60
   *
61
   * @param $form
62
   * @param $form_state
63
   */
64
  public function form_submit($form, &$form_state);
65

    
66
  /**
67
   * Returns a structured array for rendering this element in overviews.
68
   */
69
  public function buildContent();
70

    
71
  /**
72
   * Returns the help text for editing this plugin.
73
   */
74
  public function help();
75

    
76
  /**
77
   * Returns ui operations for this element.
78
   */
79
  public function operations();
80

    
81
}
82

    
83
/**
84
 * Helper object for mapping elements to ids.
85
 */
86
class RulesElementMap {
87

    
88
  /**
89
   * @var RulesPlugin
90
   */
91
  protected $configuration;
92
  protected $index = array();
93
  protected $counter = 0;
94

    
95
  public function __construct(RulesPlugin $config) {
96
    $this->configuration = $config->root();
97
  }
98

    
99
  /**
100
   * Makes sure each element has an assigned id.
101
   */
102
  public function index() {
103
    foreach ($this->getUnIndexedElements($this->configuration) as $element) {
104
      $id = &$element->property('elementId');
105
      $id = ++$this->counter;
106
      $this->index[$id] = $element;
107
    }
108
  }
109

    
110
  protected function getUnIndexedElements($element, &$unindexed = array()) {
111
    // Remember unindexed elements.
112
    $id = $element->property('elementId');
113
    if (!isset($id)) {
114
      $unindexed[] = $element;
115
    }
116
    else {
117
      // Make sure $this->counter refers to the highest id.
118
      if ($id > $this->counter) {
119
        $this->counter = $id;
120
      }
121
      $this->index[$id] = $element;
122
    }
123
    // Recurse down the tree.
124
    if ($element instanceof RulesContainerPlugin) {
125
      foreach ($element as $child) {
126
        $this->getUnIndexedElements($child, $unindexed);
127
      }
128
    }
129
    return $unindexed;
130
  }
131

    
132
  /**
133
   * Looks up the element with the given id.
134
   */
135
  public function lookup($id) {
136
    if (!$this->index) {
137
      $this->index();
138
    }
139
    return isset($this->index[$id]) ? $this->index[$id] : FALSE;
140
  }
141

    
142
}
143

    
144
/**
145
 * Faces UI extender for all kind of Rules plugins.
146
 *
147
 * Provides various useful methods for any rules UI.
148
 */
149
class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface {
150

    
151
  /**
152
   * @var RulesPlugin
153
   */
154
  protected $element;
155

    
156
  /**
157
   * The base path determines where a Rules overview UI lives.
158
   *
159
   * All forms that want to display Rules (overview) forms need to set this
160
   * variable. This is necessary in order to get correct operation links,
161
   * paths, redirects, breadcrumbs etc. for the form() and overviewTable() methods.
162
   *
163
   * @see RulesUIController
164
   * @see rules_admin_reaction_overview()
165
   * @see rules_admin_components_overview()
166
   */
167
  public static $basePath = NULL;
168

    
169
  /**
170
   * Provide $this->element to make the code more meaningful.
171
   */
172
  public function __construct(FacesExtendable $object) {
173
    parent::__construct($object);
174
    $this->element = $object;
175
  }
176

    
177
  /**
178
   * Returns the form values for the given form, possible being only a part of the whole form.
179
   *
180
   * In case the form is embedded somewhere, this function figures out the
181
   * location of its form values and returns them for further use.
182
   *
183
   * @param $form
184
   *   A form array, or an array of form elements to get the value for.
185
   * @param $form_state
186
   *   The form state as usual.
187
   */
188
  public static function &getFormStateValues($form, &$form_state) {
189
    $values = NULL;
190
    if (isset($form_state['values'])) {
191
      // Assume the top level if parents are not yet set.
192
      $form += array('#parents' => array());
193
      $values = &$form_state['values'];
194
      foreach ($form['#parents'] as $parent) {
195
        $values = &$values[$parent];
196
      }
197
    }
198
    return $values;
199
  }
200

    
201
  /**
202
   * Implements RulesPluginUIInterface. Generates the element edit form.
203
   *
204
   * Note: Make sure that you set RulesPluginUI::$basePath before using this
205
   * method, otherwise paths, links, redirects etc. won't be correct.
206
   */
207
  public function form(&$form, &$form_state, $options = array()) {
208
    self::formDefaults($form, $form_state);
209
    $form_state += array('rules_element' => $this->element);
210

    
211
    // Add the help to the top of the form.
212
    $help = $this->element->help();
213
    $form['help'] = is_array($help) ? $help : array('#markup' => $help);
214

    
215
    // We use $form_state['element_settings'] to store the settings of both
216
    // parameter modes. That way one can switch between the parameter modes
217
    // without losing the settings of those.
218
    $form_state += array('element_settings' => $this->element->settings);
219
    $settings = $this->element->settings + $form_state['element_settings'];
220

    
221
    $form['parameter'] = array(
222
      '#tree' => TRUE,
223
    );
224

    
225
    foreach ($this->element->pluginParameterInfo() as $name => $parameter) {
226
      if ($parameter['type'] == 'hidden') {
227
        continue;
228
      }
229

    
230
      $form['parameter'][$name] = array(
231
        '#type' => 'fieldset',
232
        '#title' => check_plain($parameter['label']),
233
        '#description' => filter_xss(isset($parameter['description']) ? $parameter['description'] : ''),
234
      );
235

    
236
      // Init the parameter input mode.
237
      $form_state['parameter_mode'][$name] = !isset($form_state['parameter_mode'][$name]) ? NULL : $form_state['parameter_mode'][$name];
238
      $form['parameter'][$name] += $this->getParameterForm($name, $parameter, $settings, $form_state['parameter_mode'][$name]);
239
    }
240

    
241
    // Provide a form for editing the label and name of provided variables.
242
    $settings = $this->element->settings;
243
    foreach ($this->element->pluginProvidesVariables() as $var_name => $var_info) {
244
      $form['provides'][$var_name] = array(
245
        '#type' => 'fieldset',
246
        '#title' => check_plain($var_info['label']),
247
        '#description' => filter_xss(isset($var_info['description']) ? $var_info['description'] : ''),
248
      );
249
      $form['provides'][$var_name]['label'] = array(
250
        '#type' => 'textfield',
251
        '#title' => t('Variable label'),
252
        '#default_value' => isset($settings[$var_name . ':label']) ? $settings[$var_name . ':label'] : $var_info['label'],
253
        '#required' => TRUE,
254
      );
255
      $form['provides'][$var_name]['var'] = array(
256
        '#type' => 'textfield',
257
        '#title' => t('Variable name'),
258
        '#default_value' => isset($settings[$var_name . ':var']) ? $settings[$var_name . ':var'] : $var_name,
259
        '#description' => t('The variable name must contain only lowercase letters, numbers, and underscores and must be unique in the current scope.'),
260
        '#element_validate' => array('rules_ui_element_machine_name_validate'),
261
        '#required' => TRUE,
262
      );
263
    }
264
    if (!empty($form['provides'])) {
265
      $help = '<div class="description">' . t('Adjust the names and labels of provided variables, but note that renaming of already utilized variables invalidates the existing uses.') . '</div>';
266
      $form['provides'] += array(
267
        '#tree' => TRUE,
268
        '#prefix' => '<h4 class="rules-form-heading">' . t('Provided variables') . '</h4>' . $help,
269
      );
270
    }
271

    
272
    // Add settings form, if specified.
273
    if (!empty($options['show settings'])) {
274
      $this->settingsForm($form, $form_state);
275
    }
276
    // Add submit button, if specified.
277
    if (!empty($options['button'])) {
278
      $form['submit'] = array(
279
        '#type' => 'submit',
280
        '#value' => t('Save'),
281
        '#weight' => 10,
282
      );
283
    }
284
  }
285

    
286
  /**
287
   * Actually generates the parameter form for the given data type.
288
   */
289
  protected function getParameterForm($name, $info, $settings, &$mode) {
290
    $class = $this->getDataTypeClass($info['type'], $info);
291
    $supports_input_mode = in_array('RulesDataDirectInputFormInterface', class_implements($class));
292

    
293
    // Init the mode.
294
    if (!isset($mode)) {
295
      if (isset($settings[$name . ':select'])) {
296
        $mode = 'selector';
297
      }
298
      elseif (isset($settings[$name]) && $supports_input_mode) {
299
        $mode = 'input';
300
      }
301
      elseif (isset($info['restriction'])) {
302
        $mode = $info['restriction'];
303
      }
304
      else {
305
        // Allow the parameter to define the 'default mode' and fallback to the
306
        // data type default.
307
        $mode = !empty($info['default mode']) ? $info['default mode'] : call_user_func(array($class, 'getDefaultMode'));
308
      }
309
    }
310

    
311
    // For translatable parameters, pre-populate an internal translation source
312
    // key so data type forms or input evaluators (i18n) may show a suitable
313
    // help message.
314
    if (drupal_multilingual() && !empty($info['translatable'])) {
315
      $parameter = $this->element->pluginParameterInfo();
316
      $info['custom translation language'] = !empty($parameter['language']);
317
    }
318

    
319
    // Add the parameter form.
320
    if ($mode == 'input' && $supports_input_mode) {
321
      $form['settings'] = call_user_func(array($class, 'inputForm'), $name, $info, $settings, $this->element);
322
    }
323
    else {
324
      $form['settings'] = call_user_func(array($class, 'selectionForm'), $name, $info, $settings, $this->element);
325
    }
326

    
327
    // Add a link for switching the input mode when JS is enabled and a button
328
    // to switch it without JavaScript, in case switching is possible.
329
    if ($supports_input_mode && empty($info['restriction'])) {
330
      $value = $mode == 'selector' ? t('Switch to the direct input mode') : t('Switch to data selection');
331

    
332
      $form['switch_button'] = array(
333
        '#type' => 'submit',
334
        '#name' => 'param_' . $name,
335
        '#attributes' => array('class' => array('rules-switch-button')),
336
        '#parameter' => $name,
337
        '#value' => $value,
338
        '#submit' => array('rules_ui_parameter_replace_submit'),
339
        '#ajax' => rules_ui_form_default_ajax('none'),
340
        // Do not validate!
341
        '#limit_validation_errors' => array(),
342
      );
343
    }
344
    return $form;
345
  }
346

    
347
  /**
348
   * Implements RulesPluginUIInterface.
349
   */
350
  public function form_validate($form, &$form_state) {
351
    $this->form_extract_values($form, $form_state);
352
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
353

    
354
    if (isset($form_values['provides'])) {
355
      $vars = $this->element->availableVariables();
356
      foreach ($form_values['provides'] as $name => $values) {
357
        if (isset($vars[$values['var']])) {
358
          form_error($form['provides'][$name]['var'], t('The variable name %name is already taken.', array('%name' => $values['var'])));
359
        }
360
      }
361
    }
362
    // Settings have been updated, so process them now.
363
    $this->element->processSettings(TRUE);
364

    
365
    // Make sure the current user really has access to configure this element
366
    // as well as the used input evaluators and data processors.
367
    if (!user_access('bypass rules access') && !$this->element->root()->access()) {
368
      form_set_error('', t('Access violation! You have insufficient access permissions to edit this configuration.'));
369
    }
370
    if (!empty($form['settings'])) {
371
      $this->settingsFormValidate($form, $form_state);
372
    }
373
  }
374

    
375
  /**
376
   * Applies the values of the form to the element.
377
   */
378
  public function form_extract_values($form, &$form_state) {
379
    $this->element->settings = array();
380
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
381
    if (isset($form_values['parameter'])) {
382
      foreach ($form_values['parameter'] as $name => $values) {
383
        $this->element->settings += $values['settings'];
384
      }
385
    }
386
    if (isset($form_values['provides'])) {
387
      foreach ($form_values['provides'] as $name => $values) {
388
        $this->element->settings[$name . ':label'] = $values['label'];
389
        $this->element->settings[$name . ':var'] = $values['var'];
390
      }
391
    }
392
    if (!empty($form['settings'])) {
393
      $this->settingsFormExtractValues($form, $form_state);
394
    }
395
  }
396

    
397
  /**
398
   * Implements RulesPluginUIInterface.
399
   */
400
  public function form_submit($form, &$form_state) {
401
    if (!empty($form['settings'])) {
402
      $this->settingsFormSubmit($form, $form_state);
403
    }
404
    $this->element->save();
405
  }
406

    
407
  /**
408
   * Adds the configuration settings form (label, tags, description, ..).
409
   */
410
  public function settingsForm(&$form, &$form_state) {
411
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
412
    // Add the settings in a separate fieldset below.
413
    $form['settings'] = array(
414
      '#type' => 'fieldset',
415
      '#title' => t('Settings'),
416
      '#collapsible' => TRUE,
417
      '#collapsed' => empty($form_values['settings']['vars']['more']),
418
      '#weight' => 5,
419
      '#tree' => TRUE,
420
    );
421
    $form['settings']['label'] = array(
422
      '#type' => 'textfield',
423
      '#title' => t('Name'),
424
      '#default_value' => $this->element->label(),
425
      '#required' => TRUE,
426
      '#weight' => -5,
427
    );
428
    // @todo For Drupal 8 use "owner" for generating machine names and
429
    // module only for the modules providing default configurations.
430
    if (!empty($this->element->module) && !empty($this->element->name) && $this->element->module == 'rules' && strpos($this->element->name, 'rules_') === 0) {
431
      // Remove the Rules module prefix from the machine name.
432
      $machine_name = substr($this->element->name, strlen($this->element->module) + 1);
433
    }
434
    else {
435
      $machine_name = $this->element->name;
436
    }
437
    $form['settings']['name'] = array(
438
      '#type' => 'machine_name',
439
      '#default_value' => isset($machine_name) ? $machine_name : '',
440
      // The string 'rules_' is pre-pended to machine names, so the
441
      // maxlength must be less than the field length of 64 characters.
442
      '#maxlength' => 58,
443
      '#disabled' => entity_has_status('rules_config', $this->element, ENTITY_IN_CODE) && !(isset($form_state['op']) && $form_state['op'] == 'clone'),
444
      '#machine_name' => array(
445
        'exists' => 'rules_config_load',
446
        'source' => array('settings', 'label'),
447
      ),
448
      '#required' => TRUE,
449
      '#description' => t('The machine-readable name of this configuration is used by rules internally to identify the configuration. This name must contain only lowercase letters, numbers, and underscores and must be unique.'),
450
    );
451
    $form['settings']['tags'] = array(
452
      '#type' => 'textfield',
453
      '#title' => t('Tags'),
454
      '#default_value' => isset($this->element->tags) ? drupal_implode_tags($this->element->tags) : '',
455
      '#autocomplete_path' => 'admin/config/workflow/rules/autocomplete_tags',
456
      '#description' => t('Tags associated with this configuration, used for filtering in the admin interface. Separate multiple tags with commas.'),
457
    );
458

    
459
    // Show a form for editing variables for components.
460
    if (($plugin_info = $this->element->pluginInfo()) && !empty($plugin_info['component'])) {
461
      if ($this->element->hasStatus(ENTITY_IN_CODE)) {
462
        $description = t('The variables used by the component. They can not be edited for configurations that are provided in code.');
463
      }
464
      else {
465
        $description = t('Variables are normally input <em>parameters</em> for the component – data that should be available for the component to act on. Additionally, action components may <em>provide</em> variables back to the caller. Each variable must have a specified data type, a label and a unique machine readable name containing only lowercase alphanumeric characters and underscores. See <a href="@url">the online documentation</a> for more information about variables.',
466
          array('@url' => rules_external_help('variables'))
467
        );
468
      }
469
      $form['settings']['vars'] = array(
470
        '#prefix' => '<div id="rules-component-variables">',
471
        '#suffix' => '</div>',
472
        '#tree' => TRUE,
473
        '#element_validate' => array('rules_ui_element_variable_form_validate'),
474
        '#theme' => 'rules_ui_variable_form',
475
        '#title' => t('Variables'),
476
        '#description' => $description,
477
        // Variables can not be edited on configurations in code.
478
        '#disabled' => $this->element->hasStatus(ENTITY_IN_CODE),
479
      );
480

    
481
      $weight = 0;
482
      $provides = $this->element->providesVariables();
483
      foreach ($this->element->componentVariables() as $name => $var_info) {
484
        $form['settings']['vars']['items'][$name] = RulesPluginUI::getVariableForm($name, $var_info, isset($provides[$name]));
485
        $form['settings']['vars']['items'][$name]['weight']['#default_value'] = $weight++;
486
      }
487
      // Always add three empty forms.
488
      for ($i = 0; $i < 3; $i++) {
489
        $form['settings']['vars']['items'][$i] = RulesPluginUI::getVariableForm();
490
        $form['settings']['vars']['items'][$i]['weight']['#default_value'] = $weight++;
491
      }
492
      $form['settings']['vars']['more'] = array(
493
        '#type' => 'submit',
494
        '#value' => t('Add more'),
495
        // Enable AJAX once #756762 is fixed.
496
        // '#ajax' => rules_ui_form_default_ajax('none'),
497
        '#limit_validation_errors' => array(array('vars')),
498
        '#submit' => array('rules_form_submit_rebuild'),
499
      );
500
      if (!empty($this->element->id)) {
501
        // Display a setting to manage access.
502
        $form['settings']['access'] = array(
503
          '#weight' => 50,
504
        );
505
        $plugin_type = $this->element instanceof RulesActionInterface ? t('action') : t('condition');
506
        $form['settings']['access']['access_exposed'] = array(
507
          '#type' => 'checkbox',
508
          '#title' => t('Configure access for using this component with a permission.'),
509
          '#default_value' => !empty($this->element->access_exposed),
510
          '#description' => t('By default, the @plugin-type for using this component may be only used by users that have access to configure the component. If checked, access is determined by a permission instead.', array('@plugin-type' => $plugin_type)),
511
        );
512
        $form['settings']['access']['permissions'] = array(
513
          '#type' => 'container',
514
          '#states' => array(
515
            'visible' => array(
516
              ':input[name="settings[access][access_exposed]"]' => array('checked' => TRUE),
517
            ),
518
          ),
519
        );
520
        $form['settings']['access']['permissions']['matrix'] = $this->settingsFormPermissionMatrix();
521
      }
522
    }
523

    
524
    // @todo Attach field form thus description.
525
  }
526

    
527
  /**
528
   * Provides a matrix permission for the component based in the existing roles.
529
   *
530
   * @return
531
   *   Form elements with the matrix of permissions for a component.
532
   */
533
  protected function settingsFormPermissionMatrix() {
534
    $form['#theme'] = 'user_admin_permissions';
535
    $status = array();
536
    $options = array();
537

    
538
    $role_names = user_roles();
539
    $role_permissions = user_role_permissions($role_names);
540
    $component_permission = rules_permissions_by_component(array($this->element));
541
    $component_permission_name = key($component_permission);
542

    
543
    $form['permission'][$component_permission_name] = array(
544
      '#type' => 'item',
545
      '#markup' => $component_permission[$component_permission_name]['title'],
546
    );
547
    $options[$component_permission_name] = '';
548
    foreach ($role_names as $rid => $name) {
549
      if (isset($role_permissions[$rid][$component_permission_name])) {
550
        $status[$rid][] = $component_permission_name;
551
      }
552
    }
553

    
554
    // Build the checkboxes for each role.
555
    foreach ($role_names as $rid => $name) {
556
      $form['checkboxes'][$rid] = array(
557
        '#type' => 'checkboxes',
558
        '#options' => $options,
559
        '#default_value' => isset($status[$rid]) ? $status[$rid] : array(),
560
        '#attributes' => array('class' => array('rid-' . $rid)),
561
      );
562
      $form['role_names'][$rid] = array('#markup' => check_plain($name), '#tree' => TRUE);
563
    }
564

    
565
    // Attach the default permissions page JavaScript.
566
    $form['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.permissions.js';
567

    
568
    return $form;
569
  }
570

    
571
  public function settingsFormExtractValues($form, &$form_state) {
572
    $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state);
573
    $this->element->label = $form_values['label'];
574
    // If the name was changed we have to redirect to the URL that contains
575
    // the new name, instead of rebuilding on the old URL with the old name.
576
    if ($form['settings']['name']['#default_value'] != $form_values['name']) {
577
      $module = isset($this->element->module) ? $this->element->module : 'rules';
578
      $this->element->name = $module . '_' . $form_values['name'];
579
      $form_state['redirect'] = RulesPluginUI::path($this->element->name, 'edit', $this->element);
580
    }
581
    $this->element->tags = empty($form_values['tags']) ? array() : drupal_explode_tags($form_values['tags']);
582

    
583
    if (isset($form_values['vars']['items'])) {
584
      $vars = &$this->element->componentVariables();
585
      $vars = array();
586
      if ($this->element instanceof RulesActionContainer) {
587
        $provides = &$this->element->componentProvidesVariables();
588
        $provides = array();
589
      }
590

    
591
      usort($form_values['vars']['items'], 'rules_element_sort_helper');
592
      foreach ($form_values['vars']['items'] as $item) {
593
        if ($item['type'] && $item['name'] && $item['label']) {
594
          $vars[$item['name']] = array('label' => $item['label'], 'type' => $item['type']);
595
          if (!$item['usage'][0]) {
596
            $vars[$item['name']]['parameter'] = FALSE;
597
          }
598
          if ($item['usage'][1] && isset($provides)) {
599
            $provides[] = $item['name'];
600
          }
601
        }
602
      }
603
      // Disable FAPI persistence for the variable form so renumbering works.
604
      $input = &$form_state['input'];
605
      foreach ($form['settings']['#parents'] as $parent) {
606
        $input = &$input[$parent];
607
      }
608
      unset($input['vars']);
609
    }
610
    $this->element->access_exposed = isset($form_values['access']['access_exposed']) ? $form_values['access']['access_exposed'] : FALSE;
611
  }
612

    
613
  public function settingsFormValidate($form, &$form_state) {
614
    $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state);
615
    if ($form['settings']['name']['#default_value'] != $form_values['name'] && rules_config_load($this->element->name)) {
616
      form_error($form['settings']['name'], t('The machine-readable name %name is already taken.', array('%name' => $form_values['name'])));
617
    }
618
  }
619

    
620
  public function settingsFormSubmit($form, &$form_state) {
621
    if (isset($form_state['values']['settings']['access']) && !empty($this->element->access_exposed)) {
622
      // Save the permission matrix.
623
      foreach ($form_state['values']['settings']['access']['permissions']['matrix']['checkboxes'] as $rid => $value) {
624
        user_role_change_permissions($rid, $value);
625
      }
626
    }
627
  }
628

    
629
  /**
630
   * Returns the form for configuring the info of a single variable.
631
   */
632
  public function getVariableForm($name = '', $info = array(), $provided = FALSE) {
633
    $form['type'] = array(
634
      '#type' => 'select',
635
      '#options' => array(0 => '--') + RulesPluginUI::getOptions('data'),
636
      '#default_value' => isset($info['type']) ? $info['type'] : 0,
637
    );
638
    $form['label'] = array(
639
      '#type' => 'textfield',
640
      '#size' => 40,
641
      '#default_value' => isset($info['label']) ? $info['label'] : '',
642
    );
643
    $form['name'] = array(
644
      '#type' => 'textfield',
645
      '#size' => 40,
646
      '#default_value' => $name,
647
      '#element_validate' => array('rules_ui_element_machine_name_validate'),
648
    );
649

    
650
    $usage[0] = !isset($info['parameter']) || $info['parameter'] ? 1 : 0;
651
    $usage[1] = $provided ? 1 : 0;
652

    
653
    $form['usage'] = array(
654
      '#type' => 'select',
655
      '#default_value' => implode('', $usage),
656
      '#options' => array(
657
        '10' => t('Parameter'),
658
        '11' => t('Parameter + Provided'),
659
        '01' => t('Provided'),
660
      ),
661
    );
662
    if ($this->element instanceof RulesConditionContainer) {
663
      $form['usage']['#disabled'] = TRUE;
664
    }
665

    
666
    // Just set the weight #default_value for the returned form.
667
    $form['weight'] = array(
668
      '#type' => 'weight',
669
    );
670
    return $form;
671
  }
672

    
673
  /**
674
   * Returns the name of class for the given data type.
675
   *
676
   * @param $data_type
677
   *   The name of the data type
678
   * @param $parameter_info
679
   *   (optional) An array of info about the to be configured parameter. If
680
   *   given, this array is complemented with data type defaults also.
681
   */
682
  public function getDataTypeClass($data_type, &$parameter_info = array()) {
683
    $cache = rules_get_cache();
684
    $data_info = $cache['data_info'];
685
    // Add in data-type defaults.
686
    if (empty($parameter_info['ui class'])) {
687
      $parameter_info['ui class'] = (is_string($data_type) && isset($data_info[$data_type]['ui class'])) ? $data_info[$data_type]['ui class'] : 'RulesDataUI';
688
    }
689
    if (is_subclass_of($parameter_info['ui class'], 'RulesDataInputOptionsListInterface')) {
690
      $parameter_info['options list'] = array($parameter_info['ui class'], 'optionsList');
691
    }
692
    return $parameter_info['ui class'];
693
  }
694

    
695
  /**
696
   * Implements RulesPluginUIInterface.
697
   *
698
   * Shows a preview of the configuration settings.
699
   */
700
  public function buildContent() {
701
    $config_name = $this->element->root()->name;
702
    $content['label'] = array(
703
      '#type' => 'link',
704
      '#title' => $this->element->label(),
705
      '#href' => $this->element->isRoot() ? RulesPluginUI::path($config_name) : RulesPluginUI::path($config_name, 'edit', $this->element),
706
      '#prefix' => '<div class="rules-element-label">',
707
      '#suffix' => '</div>',
708
    );
709
    // Put the elements below in a "description" div.
710
    $content['description'] = array(
711
      '#prefix' => '<div class="description">',
712
    );
713
    $content['description']['parameter'] = array(
714
      '#caption' => t('Parameter'),
715
      '#theme' => 'rules_content_group',
716
    );
717
    foreach ($this->element->pluginParameterInfo() as $name => $parameter) {
718
      $element = array();
719
      if (!empty($this->element->settings[$name . ':select'])) {
720
        $element['content'] = array(
721
          '#markup' => '[' . $this->element->settings[$name . ':select'] . ']',
722
        );
723
      }
724
      elseif (isset($this->element->settings[$name]) && (!isset($parameter['default value']) || $parameter['default value'] != $this->element->settings[$name])) {
725
        $class = $this->getDataTypeClass($parameter['type'], $parameter);
726
        $method = empty($parameter['options list']) ? 'render' : 'renderOptionsLabel';
727
        // We cannot use method_exists() here as it would trigger a PHP bug.
728
        // @see https://www.drupal.org/node/1258284
729
        $element = call_user_func(array($class, $method), $this->element->settings[$name], $name, $parameter, $this->element);
730
      }
731
      // Only add parameters that are really configured / not default.
732
      if ($element) {
733
        $content['description']['parameter'][$name] = array(
734
          '#theme' => 'rules_parameter_configuration',
735
          '#info' => $parameter,
736
        ) + $element;
737
      }
738
    }
739
    foreach ($this->element->providesVariables() as $name => $var_info) {
740
      $content['description']['provides'][$name] = array(
741
        '#theme' => 'rules_variable_view',
742
        '#info' => $var_info,
743
        '#name' => $name,
744
      );
745
    }
746
    if (!empty($content['description']['provides'])) {
747
      $content['description']['provides'] += array(
748
        '#caption' => t('Provides variables'),
749
        '#theme' => 'rules_content_group',
750
      );
751
    }
752
    // Add integrity exception messages if there are any for this element.
753
    try {
754
      $this->element->integrityCheck();
755
      // A configuration is still marked as dirty, but already works again.
756
      if (!empty($this->element->dirty)) {
757
        rules_config_update_dirty_flag($this->element);
758
        $variables = array('%label' => $this->element->label(), '%name' => $this->element->name, '@plugin' => $this->element->plugin());
759
        drupal_set_message(t('The @plugin %label (%name) was marked dirty, but passes the integrity check now and is active again.', $variables));
760
        rules_clear_cache();
761
      }
762
    }
763
    catch (RulesIntegrityException $e) {
764
      $content['description']['integrity'] = array(
765
        '#theme' => 'rules_content_group',
766
        '#caption' => t('Error'),
767
        '#attributes' => array('class' => array('rules-content-group-integrity-error')),
768
        'error' => array(
769
          '#markup' => filter_xss($e->getMessage()),
770
        ),
771
      );
772
      // Also make sure the rule is marked as dirty.
773
      if (empty($this->element->dirty)) {
774
        rules_config_update_dirty_flag($this->element);
775
        rules_clear_cache();
776
      }
777
    }
778

    
779
    $content['#suffix'] = '</div>';
780
    $content['#type'] = 'container';
781
    $content['#attributes']['class'][] = 'rules-element-content';
782
    return $content;
783
  }
784

    
785
  /**
786
   * Implements RulesPluginUIInterface.
787
   */
788
  public function operations() {
789
    $name = $this->element->root()->name;
790
    $render = array(
791
      '#theme' => 'links__rules',
792
    );
793
    $render['#attributes']['class'][] = 'rules-operations';
794
    $render['#attributes']['class'][] = 'action-links';
795
    $render['#links']['edit'] = array(
796
      'title' => t('edit'),
797
      'href' => RulesPluginUI::path($name, 'edit', $this->element),
798
    );
799
    $render['#links']['delete'] = array(
800
      'title' => t('delete'),
801
      'href' => RulesPluginUI::path($name, 'delete', $this->element),
802
    );
803
    return $render;
804
  }
805

    
806
  /**
807
   * Implements RulesPluginUIInterface.
808
   */
809
  public function help() {}
810

    
811
  /**
812
   * Deprecated by the controllers overviewTable() method.
813
   */
814
  public static function overviewTable($conditions = array(), $options = array()) {
815
    return rules_ui()->overviewTable($conditions, $options);
816
  }
817

    
818
  /**
819
   * Generates an operation path.
820
   *
821
   * Generates a path using the given operation for the element with the given
822
   * id of the configuration with the given name.
823
   */
824
  public static function path($name, $op = NULL, RulesPlugin $element = NULL, $parameter = FALSE) {
825
    $element_id = isset($element) ? $element->elementId() : FALSE;
826
    if (isset(self::$basePath)) {
827
      $base_path = self::$basePath;
828
    }
829
    // Default to the paths used by 'rules_admin', so modules can easily re-use
830
    // its UI.
831
    else {
832
      $base_path = isset($element) && $element instanceof RulesTriggerableInterface ? 'admin/config/workflow/rules/reaction' : 'admin/config/workflow/rules/components';
833
    }
834

    
835
    // Only append the '/manage' path if it is not already present.
836
    if (substr($base_path, -strlen('/manage')) != '/manage') {
837
      $base_path .= '/manage';
838
    }
839

    
840
    return implode('/', array_filter(array($base_path, $name, $op, $element_id, $parameter)));
841
  }
842

    
843
  /**
844
   * Determines the default redirect target for an edited/deleted element.
845
   *
846
   * This is a parent element which is either a rule or the configuration root.
847
   */
848
  public static function defaultRedirect(RulesPlugin $element) {
849
    while (!$element->isRoot()) {
850
      if ($element instanceof Rule) {
851
        return self::path($element->root()->name, 'edit', $element);
852
      }
853
      $element = $element->parentElement();
854
    }
855
    return self::path($element->name);
856
  }
857

    
858
  /**
859
   * @see RulesUICategory::getOptions()
860
   */
861
  public static function getOptions($item_type, $items = NULL) {
862
    return RulesUICategory::getOptions($item_type, $items = NULL);
863
  }
864

    
865
  public static function formDefaults(&$form, &$form_state) {
866
    form_load_include($form_state, 'inc', 'rules', 'ui/ui.forms');
867
    // Add our own css.
868
    $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css';
869
    // Workaround for problems with jquery css in seven theme and the core
870
    // autocomplete.
871
    if ($GLOBALS['theme'] == 'seven') {
872
      $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.seven.css';
873
    }
874

    
875
    // Specify the wrapper div used by #ajax.
876
    $form['#prefix'] = '<div id="rules-form-wrapper">';
877
    $form['#suffix'] = '</div>';
878

    
879
    // Preserve the base path in the form state. The after build handler will
880
    // set self::$basePath again for cached forms.
881
    if (isset(self::$basePath)) {
882
      $form_state['_rules_base_path'] = RulesPluginUI::$basePath;
883
      $form['#after_build'][] = 'rules_form_after_build_restore_base_path';
884
    }
885
  }
886

    
887
  public static function getTags() {
888
    $result = db_select('rules_tags')
889
      ->distinct()
890
      ->fields('rules_tags', array('tag'))
891
      ->groupBy('tag')
892
      ->execute()
893
      ->fetchCol('tag');
894
    return drupal_map_assoc($result);
895
  }
896

    
897
}
898

    
899
/**
900
 * UI for abstract plugins (conditions & actions).
901
 */
902
class RulesAbstractPluginUI extends RulesPluginUI {
903

    
904
  /**
905
   * Overrides RulesPluginUI::form().
906
   *
907
   * Overridden to invoke the abstract plugins form alter callback and to add
908
   * the negation checkbox for conditions.
909
   */
910
  public function form(&$form, &$form_state, $options = array()) {
911
    parent::form($form, $form_state, $options);
912

    
913
    if ($this->element instanceof RulesCondition) {
914
      $form['negate'] = array(
915
        '#title' => t('Negate'),
916
        '#type' => 'checkbox',
917
        '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
918
        '#default_value' => $this->element->isNegated(),
919
        '#weight' => 5,
920
      );
921
    }
922
    $this->element->call('form_alter', array(&$form, &$form_state, $options));
923
  }
924

    
925
  public function form_extract_values($form, &$form_state) {
926
    parent::form_extract_values($form, $form_state);
927
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
928
    if ($this->element instanceof RulesCondition && isset($form_values['negate'])) {
929
      $this->element->negate($form_values['negate']);
930
    }
931
  }
932

    
933
  public function form_validate($form, &$form_state) {
934
    parent::form_validate($form, $form_state);
935
    // Validate the edited element and throw validation errors if it fails.
936
    try {
937
      $this->element->integrityCheck();
938
    }
939
    catch (RulesIntegrityException $e) {
940
      form_set_error(implode('][', $e->keys), $e->getMessage());
941
    }
942
  }
943

    
944
}
945

    
946
/**
947
 * UI for Rules Container.
948
 */
949
class RulesContainerPluginUI extends RulesPluginUI {
950

    
951
  /**
952
   * Generates a table for editing the contained elements.
953
   */
954
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
955
    parent::form($form, $form_state, $options);
956
    $form['elements'] = array(
957
      // Hide during creation or for embedded elements.
958
      '#access' => empty($options['init']) && $this->element->isRoot(),
959
      '#tree' => TRUE,
960
      '#theme' => 'rules_elements',
961
      '#empty' => t('None'),
962
      '#caption' => t('Elements'),
963
    );
964
    $form['elements']['#attributes']['class'][] = 'rules-container-plugin';
965

    
966
    // Recurse over all element children or use the provided iterator.
967
    $iterator = isset($iterator) ? $iterator : $this->element->elements();
968
    $root_depth = $this->element->depth();
969
    foreach ($iterator as $key => $child) {
970
      $id = $child->elementId();
971

    
972
      // Do not render rules as container element when displayed in a rule set.
973
      $is_container = $child instanceof RulesContainerPlugin && !($child instanceof Rule);
974
      $form['elements'][$id] = array(
975
        '#depth' => $child->depth() - $root_depth - 1,
976
        '#container' => $is_container,
977
      );
978
      $form['elements'][$id]['label'] = $child->buildContent();
979
      $form['elements'][$id]['weight'] = array(
980
        '#type' => 'weight',
981
        '#default_value' => $child->weight,
982
        '#delta' => 50,
983
      );
984
      $form['elements'][$id]['parent_id'] = array(
985
        '#type' => 'hidden',
986
        // If another iterator is passed in, the child parent may not equal
987
        // the current element. Thus ask the child for its parent.
988
        '#default_value' => $child->parentElement()->elementId(),
989
      );
990
      $form['elements'][$id]['element_id'] = array(
991
        '#type' => 'hidden',
992
        '#default_value' => $id,
993
      );
994
      $form['elements'][$id]['operations'] = $child->operations();
995
    }
996

    
997
    // Alter the submit button label.
998
    if (!empty($options['button']) && !empty($options['init'])) {
999
      $form['submit']['#value'] = t('Continue');
1000
    }
1001
    elseif (!empty($options['button']) && $this->element->isRoot()) {
1002
      $form['submit']['#value'] = t('Save changes');
1003
    }
1004
  }
1005

    
1006
  /**
1007
   * Applies the values of the form to the given rule configuration.
1008
   */
1009
  public function form_extract_values($form, &$form_state) {
1010
    parent::form_extract_values($form, $form_state);
1011
    $values = RulesPluginUI::getFormStateValues($form, $form_state);
1012
    // Now apply the new hierarchy.
1013
    if (isset($values['elements'])) {
1014
      foreach ($values['elements'] as $id => $data) {
1015
        $child = $this->element->elementMap()->lookup($id);
1016
        $child->weight = $data['weight'];
1017
        $parent = $this->element->elementMap()->lookup($data['parent_id']);
1018
        $child->setParent($parent ? $parent : $this->element);
1019
      }
1020
      $this->element->sortChildren(TRUE);
1021
    }
1022
  }
1023

    
1024
  public function operations() {
1025
    $ops = parent::operations();
1026
    $add_ops = self::addOperations();
1027
    $ops['#links'] += $add_ops['#links'];
1028
    return $ops;
1029
  }
1030

    
1031
  /**
1032
   * Gets the Add-* operations for the given element.
1033
   */
1034
  public function addOperations() {
1035
    $name = $this->element->root()->name;
1036
    $render = array(
1037
      '#theme' => 'links__rules',
1038
    );
1039
    $render['#attributes']['class'][] = 'rules-operations-add';
1040
    $render['#attributes']['class'][] = 'action-links';
1041
    foreach (rules_fetch_data('plugin_info') as $plugin => $info) {
1042
      if (!empty($info['embeddable']) && $this->element instanceof $info['embeddable']) {
1043
        $render['#links']['add_' . $plugin] = array(
1044
          'title' => t('Add !name', array('!name' => $plugin)),
1045
          'href' => RulesPluginUI::path($name, 'add', $this->element, $plugin),
1046
        );
1047
      }
1048
    }
1049
    return $render;
1050
  }
1051

    
1052
  public function buildContent() {
1053
    $content = parent::buildContent();
1054
    // Don't link the title for embedded container plugins, except for rules.
1055
    if (!$this->element->isRoot() && !($this->element instanceof Rule)) {
1056
      $content['label']['#markup'] = check_plain($content['label']['#title']);
1057
      unset($content['label']['#title']);
1058
    }
1059
    elseif ($this->element->isRoot()) {
1060
      $content['description']['settings'] = array(
1061
        '#theme' => 'rules_content_group',
1062
        '#weight' => -4,
1063
        'machine_name' => array(
1064
          '#markup' => t('Machine name') . ': ' . $this->element->name,
1065
        ),
1066
        'weight' => array(
1067
          '#access' => $this->element instanceof RulesTriggerableInterface,
1068
          '#markup' => t('Weight') . ': ' . $this->element->weight,
1069
        ),
1070
      );
1071
      if (!empty($this->element->tags)) {
1072
        $content['description']['tags'] = array(
1073
          '#theme' => 'rules_content_group',
1074
          '#caption' => t('Tags'),
1075
          'tags' => array(
1076
            '#markup' => check_plain(drupal_implode_tags($this->element->tags)),
1077
          ),
1078
        );
1079
      }
1080
      if ($vars = $this->element->componentVariables()) {
1081
        $content['description']['variables'] = array(
1082
          '#caption' => t('Parameter'),
1083
          '#theme' => 'rules_content_group',
1084
        );
1085
        foreach ($vars as $name => $info) {
1086
          if (!isset($info['parameter']) || $info['parameter']) {
1087
            $content['description']['variables'][$name] = array(
1088
              '#theme' => 'rules_variable_view',
1089
              '#info' => $info,
1090
              '#name' => $name,
1091
            );
1092
          }
1093
        }
1094
      }
1095
    }
1096
    return $content;
1097
  }
1098

    
1099
}
1100

    
1101
/**
1102
 * UI for Rules condition container.
1103
 */
1104
class RulesConditionContainerUI extends RulesContainerPluginUI {
1105

    
1106
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
1107
    parent::form($form, $form_state, $options, $iterator);
1108
    // Add the add-* operation links.
1109
    $form['elements']['#add'] = self::addOperations();
1110
    $form['elements']['#attributes']['class'][] = 'rules-condition-container';
1111
    $form['elements']['#caption'] = t('Conditions');
1112

    
1113
    // By default skip.
1114
    if (!empty($options['init']) && !$this->element->isRoot()) {
1115
      $config = $this->element->root();
1116
      $form['init_help'] = array(
1117
        '#type' => 'container',
1118
        '#id' => 'rules-plugin-add-help',
1119
        'content' => array(
1120
          '#markup' => t('You are about to add a new @plugin to the @config-plugin %label. Use indentation to make conditions a part of this logic group. See <a href="@url">the online documentation</a> for more information on condition sets.',
1121
            array('@plugin' => $this->element->plugin(),
1122
                  '@config-plugin' => $config->plugin(),
1123
                  '%label' => $config->label(),
1124
                  '@url' => rules_external_help('condition-components'))),
1125
        ),
1126
      );
1127
    }
1128
    $form['negate'] = array(
1129
      '#title' => t('Negate'),
1130
      '#type' => 'checkbox',
1131
      '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
1132
      '#default_value' => $this->element->isNegated(),
1133
      '#weight' => 5,
1134
    );
1135
  }
1136

    
1137
  public function form_extract_values($form, &$form_state) {
1138
    parent::form_extract_values($form, $form_state);
1139
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
1140
    if (isset($form_values['negate'])) {
1141
      $this->element->negate($form_values['negate']);
1142
    }
1143
  }
1144

    
1145
}
1146

    
1147
/**
1148
 * UI for Rules action container.
1149
 */
1150
class RulesActionContainerUI extends RulesContainerPluginUI {
1151

    
1152
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
1153
    parent::form($form, $form_state, $options, $iterator);
1154
    // Add the add-* operation links.
1155
    $form['elements']['#add'] = self::addOperations();
1156
    $form['elements']['#attributes']['class'][] = 'rules-action-container';
1157
    $form['elements']['#caption'] = t('Actions');
1158
  }
1159

    
1160
}
1161

    
1162
/**
1163
 * Class holding category related methods.
1164
 */
1165
class RulesUICategory {
1166

    
1167
  /**
1168
   * Gets info about all available categories, or about a specific category.
1169
   *
1170
   * @return array
1171
   */
1172
  public static function getInfo($category = NULL) {
1173
    $data = rules_fetch_data('category_info');
1174
    if (isset($category)) {
1175
      return $data[$category];
1176
    }
1177
    return $data;
1178
  }
1179

    
1180
  /**
1181
   * Returns a group label, e.g. as usable for opt-groups in a select list.
1182
   *
1183
   * @param array $item_info
1184
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
1185
   * @param bool $in_category
1186
   *   (optional) Whether group labels for grouping inside a category should be
1187
   *   return. Defaults to FALSE.
1188
   *
1189
   * @return string|bool
1190
   *   The group label to use, or FALSE if none can be found.
1191
   */
1192
  public static function getItemGroup($item_info, $in_category = FALSE) {
1193
    if (isset($item_info['category']) && !$in_category) {
1194
      return self::getCategory($item_info, 'label');
1195
    }
1196
    elseif (!empty($item_info['group'])) {
1197
      return $item_info['group'];
1198
    }
1199
    return FALSE;
1200
  }
1201

    
1202
  /**
1203
   * Gets the category for the given item info array.
1204
   *
1205
   * @param array $item_info
1206
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
1207
   * @param string|null $key
1208
   *   (optional) The key of the category info to return, e.g. 'label'. If none
1209
   *   is given the whole info array is returned.
1210
   *
1211
   * @return array|mixed|false
1212
   *   Either the whole category info array or the value of the given key. If
1213
   *   no category can be found, FALSE is returned.
1214
   */
1215
  public static function getCategory($item_info, $key = NULL) {
1216
    if (isset($item_info['category'])) {
1217
      $info = self::getInfo($item_info['category']);
1218
      return isset($key) ? $info[$key] : $info;
1219
    }
1220
    return FALSE;
1221
  }
1222

    
1223
  /**
1224
   * Returns an array of options to use with a select.
1225
   *
1226
   * Returns an array of options to use with a selectfor the items specified
1227
   * in the given hook.
1228
   *
1229
   * @param $item_type
1230
   *   The item type to get options for. One of 'data', 'event', 'condition' and
1231
   *   'action'.
1232
   * @param $items
1233
   *   (optional) An array of items to restrict the options to.
1234
   *
1235
   * @return array
1236
   *   An array of options.
1237
   */
1238
  public static function getOptions($item_type, $items = NULL) {
1239
    $sorted_data = array();
1240
    $ungrouped = array();
1241
    $data = $items ? $items : rules_fetch_data($item_type . '_info');
1242
    foreach ($data as $name => $info) {
1243
      // Verify the current user has access to use it.
1244
      if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) {
1245
        continue;
1246
      }
1247
      if ($group = RulesUICategory::getItemGroup($info)) {
1248
        $sorted_data[drupal_ucfirst($group)][$name] = drupal_ucfirst($info['label']);
1249
      }
1250
      else {
1251
        $ungrouped[$name] = drupal_ucfirst($info['label']);
1252
      }
1253
    }
1254
    asort($ungrouped);
1255
    foreach ($sorted_data as $key => $choices) {
1256
      asort($choices);
1257
      $sorted_data[$key] = $choices;
1258
    }
1259

    
1260
    // Sort the grouped data by category weights, defaulting to weight 0 for
1261
    // groups without a respective category.
1262
    $sorted_groups = array();
1263
    foreach (array_keys($sorted_data) as $label) {
1264
      $sorted_groups[$label] = array('weight' => 0, 'label' => $label);
1265
    }
1266
    // Add in category weights.
1267
    foreach (RulesUICategory::getInfo() as $info) {
1268
      if (isset($sorted_groups[$info['label']])) {
1269
        $sorted_groups[$info['label']] = $info;
1270
      }
1271
    }
1272
    uasort($sorted_groups, '_rules_ui_sort_categories');
1273

    
1274
    // Now replace weights with group content.
1275
    foreach ($sorted_groups as $group => $weight) {
1276
      $sorted_groups[$group] = $sorted_data[$group];
1277
    }
1278
    return $ungrouped + $sorted_groups;
1279
  }
1280

    
1281
}
1282

    
1283
/**
1284
 * Helper for sorting categories.
1285
 */
1286
function _rules_ui_sort_categories($a, $b) {
1287
  // @see element_sort()
1288
  $a_weight = isset($a['weight']) ? $a['weight'] : 0;
1289
  $b_weight = isset($b['weight']) ? $b['weight'] : 0;
1290
  if ($a_weight == $b_weight) {
1291
    // @see element_sort_by_title()
1292
    $a_title = isset($a['label']) ? $a['label'] : '';
1293
    $b_title = isset($b['label']) ? $b['label'] : '';
1294
    return strnatcasecmp($a_title, $b_title);
1295
  }
1296
  return ($a_weight < $b_weight) ? -1 : 1;
1297
}