Projet

Général

Profil

Paste
Télécharger (46,7 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / rules / ui / ui.core.inc @ 76e2e7c3

1
<?php
2

    
3
/**
4
 * @file Contains core ui functions.
5
 */
6

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

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

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

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

    
63
  /**
64
   * Returns a structured array for rendering this element in overviews.
65
   */
66
  public function buildContent();
67

    
68
  /**
69
   * Returns the help text for editing this plugin.
70
   */
71
  public function help();
72

    
73
  /**
74
   * Returns ui operations for this element.
75
   */
76
  public function operations();
77

    
78
}
79

    
80
/**
81
 * Helper object for mapping elements to ids.
82
 */
83
class RulesElementMap {
84

    
85
  /**
86
   * @var RulesPlugin
87
   */
88
  protected $configuration;
89
  protected $index = array();
90
  protected $counter = 0;
91

    
92
  public function __construct(RulesPlugin $config) {
93
    $this->configuration = $config->root();
94
  }
95

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

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

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

    
140
/**
141
 * Faces UI extender for all kind of Rules plugins. Provides various useful
142
 * methods for any rules UI.
143
 */
144
class RulesPluginUI extends FacesExtender implements RulesPluginUIInterface {
145

    
146
  /**
147
   * @var RulesPlugin
148
   */
149
  protected $element;
150

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

    
163
  /**
164
   * Provide $this->element to make the code more meaningful.
165
   */
166
  public function __construct(FacesExtendable $object) {
167
    parent::__construct($object);
168
    $this->element = $object;
169
  }
170

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

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

    
206
    // Add the help to the top of the form.
207
    $help = $this->element->help();
208
    $form['help'] = is_array($help) ? $help : array('#markup' => $help);
209

    
210
    // We use $form_state['element_settings'] to store the settings of both
211
    // parameter modes. That way one can switch between the parameter modes
212
    // without losing the settings of those.
213
    $form_state += array('element_settings' => $this->element->settings);
214
    $settings = $this->element->settings + $form_state['element_settings'];
215

    
216
    $form['parameter'] = array(
217
      '#tree' => TRUE,
218
    );
219

    
220
    foreach ($this->element->pluginParameterInfo() as $name => $parameter) {
221
      if ($parameter['type'] == 'hidden') {
222
        continue;
223
      }
224

    
225
      $form['parameter'][$name] = array(
226
        '#type' => 'fieldset',
227
        '#title' => check_plain($parameter['label']),
228
        '#description' => filter_xss(isset($parameter['description']) ? $parameter['description'] : ''),
229
      );
230

    
231
      // Init the parameter input mode.
232
      $form_state['parameter_mode'][$name] = !isset($form_state['parameter_mode'][$name]) ? NULL : $form_state['parameter_mode'][$name];
233
      $form['parameter'][$name] += $this->getParameterForm($name, $parameter, $settings, $form_state['parameter_mode'][$name]);
234
    }
235

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

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

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

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

    
306
    // For translatable parameters, pre-populate an internal translation source
307
    // key so data type forms or input evaluators (i18n) may produce suiting
308
    // help.
309
    if (drupal_multilingual() && !empty($info['translatable'])) {
310
      $parameter = $this->element->pluginParameterInfo();
311
      $info['custom translation language'] = !empty($parameter['language']);
312
    }
313

    
314
    // Add the parameter form.
315
    if ($mode == 'input' && $supports_input_mode) {
316
      $form['settings'] = call_user_func(array($class, 'inputForm'), $name, $info, $settings, $this->element);
317
    }
318
    else {
319
      $form['settings'] = call_user_func(array($class, 'selectionForm'), $name, $info, $settings, $this->element);
320
    }
321

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

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

    
342
  /**
343
   * Implements RulesPluginUIInterface.
344
   */
345
  public function form_validate($form, &$form_state) {
346
    $this->form_extract_values($form, $form_state);
347
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
348

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

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

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

    
392
  /**
393
   * Implements RulesPluginUIInterface.
394
   */
395
  public function form_submit($form, &$form_state) {
396
    if (!empty($form['settings'])) {
397
      $this->settingsFormSubmit($form, $form_state);
398
    }
399
    $this->element->save();
400
  }
401

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

    
454
    // Show a form for editing variables for components.
455
    if (($plugin_info = $this->element->pluginInfo()) && !empty($plugin_info['component'])) {
456
      if ($this->element->hasStatus(ENTITY_IN_CODE)) {
457
        $description = t('The variables used by the component. They can not be edited for configurations that are provided in code.');
458
      }
459
      else {
460
        $description = t('Variables are normally input <em>parameters</em> for the component – data that should be available for the component to act on. Additionaly, 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.',
461
          array('@url' => rules_external_help('variables'))
462
        );
463
      }
464
      $form['settings']['vars'] = array(
465
        '#prefix' => '<div id="rules-component-variables">',
466
        '#suffix' => '</div>',
467
        '#tree' => TRUE,
468
        '#element_validate' => array('rules_ui_element_variable_form_validate'),
469
        '#theme' => 'rules_ui_variable_form',
470
        '#title' => t('Variables'),
471
        '#description' => $description,
472
        // Variables can not be edited on configurations in code.
473
        '#disabled' => $this->element->hasStatus(ENTITY_IN_CODE),
474
      );
475

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

    
519
    // TODO: Attach field form thus description.
520
  }
521

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

    
533
    $role_names = user_roles();
534
    $role_permissions = user_role_permissions($role_names);
535
    $component_permission = rules_permissions_by_component(array($this->element));
536
    $component_permission_name = key($component_permission);
537

    
538
    $form['permission'][$component_permission_name] = array(
539
      '#type' => 'item',
540
      '#markup' => $component_permission[$component_permission_name]['title'],
541
    );
542
    $options[$component_permission_name] = '';
543
    foreach ($role_names as $rid => $name) {
544
      if (isset($role_permissions[$rid][$component_permission_name])) {
545
        $status[$rid][] = $component_permission_name;
546
      }
547
    }
548

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

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

    
563
    return $form;
564
  }
565

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

    
578
    if (isset($form_values['vars']['items'])) {
579
      $vars = &$this->element->componentVariables();
580
      $vars = array();
581
      if ($this->element instanceof RulesActionContainer) {
582
        $provides = &$this->element->componentProvidesVariables();
583
        $provides = array();
584
      }
585

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

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

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

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

    
645
    $usage[0] = !isset($info['parameter']) || $info['parameter'] ? 1 : 0;
646
    $usage[1] = $provided ? 1 : 0;
647

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

    
661
    // Just set the weight #default_value for the returned form.
662
    $form['weight'] = array(
663
      '#type' => 'weight',
664
    );
665
    return $form;
666
  }
667

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

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

    
773
    $content['#suffix'] = '</div>';
774
    $content['#type'] = 'container';
775
    $content['#attributes']['class'][] = 'rules-element-content';
776
    return $content;
777
  }
778

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

    
800
  /**
801
   * Implements RulesPluginUIInterface.
802
   */
803
  public function help() {}
804

    
805

    
806
  /**
807
   * Deprecated by the controllers overviewTable() method.
808
   */
809
  public static function overviewTable($conditions = array(), $options = array()) {
810
    return rules_ui()->overviewTable($conditions, $options);
811
  }
812

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

    
830
  /**
831
   * Determines the default redirect target for an edited/deleted element. This
832
   * is a parent element which is either a rule or the configuration root.
833
   */
834
  public static function defaultRedirect(RulesPlugin $element) {
835
    while (!$element->isRoot()) {
836
      if ($element instanceof Rule) {
837
        return self::path($element->root()->name, 'edit', $element);
838
      }
839
      $element = $element->parentElement();
840
    }
841
    return self::path($element->name);
842
  }
843

    
844
  /**
845
   * @see RulesUICategory::getOptions()
846
   */
847
  public static function getOptions($item_type, $items = NULL) {
848
    return RulesUICategory::getOptions($item_type, $items = NULL);
849
  }
850

    
851
  public static function formDefaults(&$form, &$form_state) {
852
    form_load_include($form_state, 'inc', 'rules', 'ui/ui.forms');
853
    // Add our own css.
854
    $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css';
855
    // Workaround for problems with jquery css in seven theme and the core
856
    // autocomplete.
857
    if ($GLOBALS['theme'] == 'seven') {
858
      $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.seven.css';
859
    }
860

    
861
    // Specify the wrapper div used by #ajax.
862
    $form['#prefix'] = '<div id="rules-form-wrapper">';
863
    $form['#suffix'] = '</div>';
864

    
865
    // Preserve the base path in the form state. The after build handler will
866
    // set self::$basePath again for cached forms.
867
    if (isset(self::$basePath)) {
868
      $form_state['_rules_base_path'] = RulesPluginUI::$basePath;
869
      $form['#after_build'][] = 'rules_form_after_build_restore_base_path';
870
    }
871
  }
872

    
873
  public static function getTags() {
874
    $result = db_select('rules_tags')
875
      ->distinct()
876
      ->fields('rules_tags', array('tag'))
877
      ->groupBy('tag')
878
      ->execute()
879
      ->fetchCol('tag');
880
    return drupal_map_assoc($result);
881
  }
882
}
883

    
884
/**
885
 * UI for abstract plugins (conditions & actions).
886
 */
887
class RulesAbstractPluginUI extends RulesPluginUI {
888

    
889
  /**
890
   * Overridden to invoke the abstract plugins form alter callback and to add
891
   * the negation checkbox for conditions.
892
   */
893
  public function form(&$form, &$form_state, $options = array()) {
894
    parent::form($form, $form_state, $options);
895

    
896
    if ($this->element instanceof RulesCondition) {
897
      $form['negate'] = array(
898
        '#title' => t('Negate'),
899
        '#type' => 'checkbox',
900
        '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
901
        '#default_value' => $this->element->isNegated(),
902
        '#weight' => 5,
903
      );
904
    }
905
    $this->element->call('form_alter', array(&$form, &$form_state, $options));
906
  }
907

    
908
  public function form_extract_values($form, &$form_state) {
909
    parent::form_extract_values($form, $form_state);
910
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
911
    if ($this->element instanceof RulesCondition && isset($form_values['negate'])) {
912
      $this->element->negate($form_values['negate']);
913
    }
914
  }
915

    
916
  public function form_validate($form, &$form_state) {
917
    parent::form_validate($form, $form_state);
918
    // Validate the edited element and throw validation errors if it fails.
919
    try {
920
      $this->element->integrityCheck();
921
    }
922
    catch (RulesIntegrityException $e) {
923
      form_set_error(implode('][', $e->keys), $e->getMessage());
924
    }
925
  }
926
}
927

    
928
/**
929
 * UI for Rules Container.
930
 */
931
class RulesContainerPluginUI extends RulesPluginUI {
932

    
933
  /**
934
   * Generates a table for editing the contained elements.
935
   */
936
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
937
    parent::form($form, $form_state, $options);
938
    $form['elements'] = array(
939
      // Hide during creation or for embedded elements.
940
      '#access' => empty($options['init']) && $this->element->isRoot(),
941
      '#tree' => TRUE,
942
      '#theme' => 'rules_elements',
943
      '#empty' => t('None'),
944
      '#caption' => t('Elements')
945
    );
946
    $form['elements']['#attributes']['class'][] = 'rules-container-plugin';
947

    
948
    // Recurse over all element childrens or use the provided iterator.
949
    $iterator = isset($iterator) ? $iterator : $this->element->elements();
950
    $root_depth = $this->element->depth();
951
    foreach ($iterator as $key => $child) {
952
      $id = $child->elementId();
953

    
954
      // Do not render rules as container element when displayed in a rule set.
955
      $is_container = $child instanceof RulesContainerPlugin && !($child instanceof Rule);
956
      $form['elements'][$id] = array(
957
        '#depth' => $child->depth() - $root_depth - 1,
958
        '#container' => $is_container,
959
      );
960
      $form['elements'][$id]['label'] = $child->buildContent();
961
      $form['elements'][$id]['weight'] = array(
962
        '#type' => 'weight',
963
        '#default_value' => $child->weight,
964
        '#delta' => 50,
965
      );
966
      $form['elements'][$id]['parent_id'] = array(
967
        '#type' => 'hidden',
968
        // If another iterator is passed in, the childs parent may not equal
969
        // the current element. Thus ask the child for its parent.
970
        '#default_value' => $child->parentElement()->elementId(),
971
      );
972
      $form['elements'][$id]['element_id'] = array(
973
        '#type' => 'hidden',
974
        '#default_value' => $id,
975
      );
976
      $form['elements'][$id]['operations'] = $child->operations();
977
    }
978

    
979
    // Alter the submit button label.
980
    if (!empty($options['button']) && !empty($options['init'])) {
981
      $form['submit']['#value'] = t('Continue');
982
    }
983
    elseif (!empty($options['button']) && $this->element->isRoot()) {
984
      $form['submit']['#value'] = t('Save changes');
985
    }
986
  }
987

    
988
  /**
989
   * Applies the values of the form to the given rule configuration.
990
   */
991
  public function form_extract_values($form, &$form_state) {
992
    parent::form_extract_values($form, $form_state);
993
    $values = RulesPluginUI::getFormStateValues($form, $form_state);
994
    // Now apply the new hierarchy.
995
    if (isset($values['elements'])) {
996
      foreach ($values['elements'] as $id => $data) {
997
        $child = $this->element->elementMap()->lookup($id);
998
        $child->weight = $data['weight'];
999
        $parent = $this->element->elementMap()->lookup($data['parent_id']);
1000
        $child->setParent($parent ? $parent : $this->element);
1001
      }
1002
      $this->element->sortChildren(TRUE);
1003
    }
1004
  }
1005

    
1006
  public function operations() {
1007
    $ops = parent::operations();
1008
    $add_ops = self::addOperations();
1009
    $ops['#links'] += $add_ops['#links'];
1010
    return $ops;
1011
  }
1012

    
1013
  /**
1014
   * Gets the Add-* operations for the given element.
1015
   */
1016
  public function addOperations() {
1017
    $name = $this->element->root()->name;
1018
    $render = array(
1019
      '#theme' => 'links__rules',
1020
    );
1021
    $render['#attributes']['class'][] = 'rules-operations-add';
1022
    $render['#attributes']['class'][] = 'action-links';
1023
    foreach (rules_fetch_data('plugin_info') as $plugin => $info) {
1024
      if (!empty($info['embeddable']) && $this->element instanceof $info['embeddable']) {
1025
        $render['#links']['add_' . $plugin] = array(
1026
          'title' => t('Add !name', array('!name' => $plugin)),
1027
          'href' => RulesPluginUI::path($name, 'add', $this->element, $plugin),
1028
        );
1029
      }
1030
    }
1031
    return $render;
1032
  }
1033

    
1034

    
1035
  public function buildContent() {
1036
    $content = parent::buildContent();
1037
    // Don't link the title for embedded container plugins, except for rules.
1038
    if (!$this->element->isRoot() && !($this->element instanceof Rule)) {
1039
      $content['label']['#type'] = 'markup';
1040
      $content['label']['#markup'] = check_plain($content['label']['#title']);
1041
      unset($content['label']['#title']);
1042
    }
1043
    elseif ($this->element->isRoot()) {
1044
      $content['description']['settings'] = array(
1045
        '#theme' => 'rules_content_group',
1046
        '#weight' => -4,
1047
        'machine_name' => array(
1048
          '#markup' => t('Machine name') . ': ' . $this->element->name,
1049
        ),
1050
        'weight' => array(
1051
          '#access' => $this->element instanceof RulesTriggerableInterface,
1052
          '#markup' => t('Weight') . ': ' . $this->element->weight,
1053
        ),
1054
      );
1055
      if (!empty($this->element->tags)) {
1056
        $content['description']['tags'] = array(
1057
          '#theme' => 'rules_content_group',
1058
          '#caption' => t('Tags'),
1059
          'tags' => array(
1060
            '#markup' => check_plain(drupal_implode_tags($this->element->tags)),
1061
          ),
1062
        );
1063
      }
1064
      if ($vars = $this->element->componentVariables()) {
1065
        $content['description']['variables'] = array(
1066
          '#caption' => t('Parameter'),
1067
          '#theme' => 'rules_content_group',
1068
        );
1069
        foreach ($vars as $name => $info) {
1070
          if (!isset($info['parameter']) || $info['parameter']) {
1071
            $content['description']['variables'][$name] = array(
1072
              '#theme' => 'rules_variable_view',
1073
              '#info' => $info,
1074
              '#name' => $name,
1075
            );
1076
          }
1077
        }
1078
      }
1079
    }
1080
    return $content;
1081
  }
1082
}
1083

    
1084
/**
1085
 * UI for Rules condition container.
1086
 */
1087
class RulesConditionContainerUI extends RulesContainerPluginUI {
1088

    
1089
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
1090
    parent::form($form, $form_state, $options, $iterator);
1091
    // Add the add-* operation links.
1092
    $form['elements']['#add'] = self::addOperations();
1093
    $form['elements']['#attributes']['class'][] = 'rules-condition-container';
1094
    $form['elements']['#caption'] = t('Conditions');
1095

    
1096
    // By default skip
1097
    if (!empty($options['init']) && !$this->element->isRoot()) {
1098
      $config = $this->element->root();
1099
      $form['init_help'] = array(
1100
        '#type' => 'container',
1101
        '#id' => 'rules-plugin-add-help',
1102
        'content' => array(
1103
          '#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.',
1104
            array('@plugin' => $this->element->plugin(),
1105
                  '@config-plugin' => $config->plugin(),
1106
                  '%label' => $config->label(),
1107
                  '@url' => rules_external_help('condition-components'))),
1108
        ),
1109
      );
1110
    }
1111
    $form['negate'] = array(
1112
      '#title' => t('Negate'),
1113
      '#type' => 'checkbox',
1114
      '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
1115
      '#default_value' => $this->element->isNegated(),
1116
      '#weight' => 5,
1117
    );
1118
  }
1119

    
1120
  public function form_extract_values($form, &$form_state) {
1121
    parent::form_extract_values($form, $form_state);
1122
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
1123
    if (isset($form_values['negate'])) {
1124
      $this->element->negate($form_values['negate']);
1125
    }
1126
  }
1127
}
1128

    
1129
/**
1130
 * UI for Rules action container.
1131
 */
1132
class RulesActionContainerUI extends RulesContainerPluginUI {
1133

    
1134
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
1135
    parent::form($form,  $form_state, $options, $iterator);
1136
    // Add the add-* operation links.
1137
    $form['elements']['#add'] = self::addOperations();
1138
    $form['elements']['#attributes']['class'][] = 'rules-action-container';
1139
    $form['elements']['#caption'] = t('Actions');
1140
  }
1141
}
1142

    
1143
/**
1144
 * Class holding category related methods.
1145
 */
1146
class RulesUICategory {
1147

    
1148
  /**
1149
   * Gets info about all available categories, or about a specific category.
1150
   *
1151
   * @return array
1152
   */
1153
  public static function getInfo($category = NULL) {
1154
    $data = rules_fetch_data('category_info');
1155
    if (isset($category)) {
1156
      return $data[$category];
1157
    }
1158
    return $data;
1159
  }
1160

    
1161
  /**
1162
   * Returns a group label, e.g. as usable for opt-groups in a select list.
1163
   *
1164
   * @param array $item_info
1165
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
1166
   * @param bool $in_category
1167
   *   (optional) Whether group labels for grouping inside a category should be
1168
   *   return. Defaults to FALSE.
1169
   *
1170
   * @return string|boolean
1171
   *   The group label to use, or FALSE if none can be found.
1172
   */
1173
  public static function getItemGroup($item_info, $in_category = FALSE) {
1174
    if (isset($item_info['category']) && !$in_category) {
1175
      return self::getCategory($item_info, 'label');
1176
    }
1177
    else if (!empty($item_info['group'])) {
1178
      return $item_info['group'];
1179
    }
1180
    return FALSE;
1181
  }
1182

    
1183
  /**
1184
   * Gets the category for the given item info array.
1185
   *
1186
   * @param array $item_info
1187
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
1188
   * @param string|null $key
1189
   *   (optional) The key of the category info to return, e.g. 'label'. If none
1190
   *   is given the whole info array is returned.
1191
   *
1192
   * @return array|mixed|false
1193
   *   Either the whole category info array or the value of the given key. If
1194
   *   no category can be found, FALSE is returned.
1195
   */
1196
  public static function getCategory($item_info, $key = NULL) {
1197
    if (isset($item_info['category'])) {
1198
      $info = self::getInfo($item_info['category']);
1199
      return isset($key) ? $info[$key] : $info;
1200
    }
1201
    return FALSE;
1202
  }
1203

    
1204
  /**
1205
   * Returns an array of options to use with a select for the items specified
1206
   * in the given hook.
1207
   *
1208
   * @param $item_type
1209
   *   The item type to get options for. One of 'data', 'event', 'condition' and
1210
   *   'action'.
1211
   * @param $items
1212
   *   (optional) An array of items to restrict the options to.
1213
   *
1214
   * @return
1215
   *   An array of options.
1216
   */
1217
  public static function getOptions($item_type, $items = NULL) {
1218
    $sorted_data = array();
1219
    $ungrouped = array();
1220
    $data = $items ? $items : rules_fetch_data($item_type . '_info');
1221
    foreach ($data as $name => $info) {
1222
      // Verfiy the current user has access to use it.
1223
      if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) {
1224
        continue;
1225
      }
1226
      if ($group = RulesUICategory::getItemGroup($info)) {
1227
        $sorted_data[drupal_ucfirst($group)][$name] = drupal_ucfirst($info['label']);
1228
      }
1229
      else {
1230
        $ungrouped[$name] = drupal_ucfirst($info['label']);
1231
      }
1232
    }
1233
    asort($ungrouped);
1234
    foreach ($sorted_data as $key => $choices) {
1235
      asort($choices);
1236
      $sorted_data[$key] = $choices;
1237
    }
1238

    
1239
    // Sort the grouped data by category weights, defaulting to weight 0 for
1240
    // groups without a respective category.
1241
    $sorted_groups = array();
1242
    foreach (array_keys($sorted_data) as $label) {
1243
      $sorted_groups[$label] = array('weight' => 0, 'label' => $label);
1244
    }
1245
    // Add in category weights.
1246
    foreach (RulesUICategory::getInfo() as $info) {
1247
      if (isset($sorted_groups[$info['label']])) {
1248
        $sorted_groups[$info['label']] = $info;
1249
      }
1250
    }
1251
    uasort($sorted_groups, '_rules_ui_sort_categories');
1252

    
1253
    // Now replace weights with group content.
1254
    foreach ($sorted_groups as $group => $weight) {
1255
      $sorted_groups[$group] = $sorted_data[$group];
1256
    }
1257
    return $ungrouped + $sorted_groups;
1258
  }
1259
}
1260

    
1261
/**
1262
 * Helper for sorting categories.
1263
 */
1264
function _rules_ui_sort_categories($a, $b) {
1265
  // @see element_sort()
1266
  $a_weight = isset($a['weight']) ? $a['weight'] : 0;
1267
  $b_weight = isset($b['weight']) ? $b['weight'] : 0;
1268
  if ($a_weight == $b_weight) {
1269
    // @see element_sort_by_title()
1270
    $a_title = isset($a['label']) ? $a['label'] : '';
1271
    $b_title = isset($b['label']) ? $b['label'] : '';
1272
    return strnatcasecmp($a_title, $b_title);
1273
  }
1274
  return ($a_weight < $b_weight) ? -1 : 1;
1275
}