You are here

ui.core.inc in Rules 7.2

Contains core Rules UI functions.

File

ui/ui.core.inc
View source
<?php

/**
 * @file
 * Contains core Rules UI functions.
 */

/**
 * Plugin UI Interface.
 */
interface RulesPluginUIInterface {

  /**
   * Adds the whole configuration form of this rules configuration.
   *
   * For rule elements that are part of a configuration this method just adds
   * the elements configuration form.
   *
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   * @param array $options
   *   An optional array of options with the known keys:
   *    - 'show settings': Whether to include the 'settings' fieldset for
   *      editing configuration settings like the label or categories. Defaults
   *      to FALSE.
   *    - 'button': Whether a submit button should be added. Defaults to FALSE.
   *    - 'init': Whether the element is about to be configured the first time
   *      and the configuration is about to be initialized. Defaults to FALSE.
   *    - 'restrict plugins: May be used to restrict the list of rules plugins
   *      that may be added to this configuration. For that set an array of
   *      valid plugins. Note that conditions and actions are always valid, so
   *      just set an empty array for just allowing those.
   *    - 'restrict conditions': Optionally set an array of condition names to
   *      restrict the conditions that are available for adding.
   *    - 'restrict actions': Optionally set an array of action names to
   *      restrict the actions that are available to for adding.
   *    - 'restrict events': Optionally set an array of event names to restrict
   *      the events that are available for adding.
   *
   * @todo Implement the 'restrict *' options.
   */
  public function form(&$form, &$form_state, $options = array());

  /**
   * Validate the configuration form of this rule element.
   *
   * @param array $form
   *   The form array.
   * @param array $form_state
   *   The current form state.
   */
  public function form_validate($form, &$form_state);

  /**
   * Form submit handler for the element configuration form.
   *
   * Submit the configuration form of this rule element. This makes sure to
   * put the updated configuration in the form state. For saving changes
   * permanently, just call $config->save() afterwards.
   *
   * @param array $form
   *   The form array.
   * @param array $form_state
   *   The current form state.
   */
  public function form_submit($form, &$form_state);

  /**
   * Returns a structured array for rendering this element in overviews.
   */
  public function buildContent();

  /**
   * Returns the help text for editing this plugin.
   */
  public function help();

  /**
   * Returns ui operations for this element.
   */
  public function operations();

}

/**
 * Helper object for mapping elements to ids.
 */
class RulesElementMap {

  /**
   * @var RulesPlugin
   */
  protected $configuration;
  protected $index = array();
  protected $counter = 0;

  /**
   * Constructor.
   */
  public function __construct(RulesPlugin $config) {
    $this->configuration = $config
      ->root();
  }

  /**
   * Makes sure each element has an assigned id.
   */
  public function index() {
    foreach ($this
      ->getUnIndexedElements($this->configuration) as $element) {
      $id =& $element
        ->property('elementId');
      $id = ++$this->counter;
      $this->index[$id] = $element;
    }
  }
  protected function getUnIndexedElements($element, &$unindexed = array()) {

    // Remember unindexed elements.
    $id = $element
      ->property('elementId');
    if (!isset($id)) {
      $unindexed[] = $element;
    }
    else {

      // Make sure $this->counter refers to the highest id.
      if ($id > $this->counter) {
        $this->counter = $id;
      }
      $this->index[$id] = $element;
    }

    // Recurse down the tree.
    if ($element instanceof RulesContainerPlugin) {
      foreach ($element as $child) {
        $this
          ->getUnIndexedElements($child, $unindexed);
      }
    }
    return $unindexed;
  }

  /**
   * Looks up the element with the given id.
   */
  public function lookup($id) {
    if (!$this->index) {
      $this
        ->index();
    }
    return isset($this->index[$id]) ? $this->index[$id] : FALSE;
  }

}

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

  /**
   * @var RulesPlugin
   */
  protected $element;

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

  /**
   * Provide $this->element to make the code more meaningful.
   */
  public function __construct(FacesExtendable $object) {
    parent::__construct($object);
    $this->element = $object;
  }

  /**
   * Returns the state values for $form, possibly only a part of the whole form.
   *
   * In case the form is embedded somewhere, this function figures out the
   * location of its form values and returns them for further use.
   *
   * @param array $form
   *   A form array, or an array of form elements to get the value for.
   * @param array $form_state
   *   The form state as usual.
   */
  public static function &getFormStateValues($form, &$form_state) {
    $values = NULL;
    if (isset($form_state['values'])) {

      // Assume the top level if parents are not yet set.
      $form += array(
        '#parents' => array(),
      );
      $values =& $form_state['values'];
      foreach ($form['#parents'] as $parent) {
        $values =& $values[$parent];
      }
    }
    return $values;
  }

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

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

    // We use $form_state['element_settings'] to store the settings of both
    // parameter modes. That way one can switch between the parameter modes
    // without losing the settings of those.
    $form_state += array(
      'element_settings' => $this->element->settings,
    );
    $settings = $this->element->settings + $form_state['element_settings'];
    $form['parameter'] = array(
      '#tree' => TRUE,
    );
    foreach ($this->element
      ->pluginParameterInfo() as $name => $parameter) {
      if ($parameter['type'] == 'hidden') {
        continue;
      }
      $form['parameter'][$name] = array(
        '#type' => 'fieldset',
        '#title' => check_plain($parameter['label']),
        '#description' => filter_xss(isset($parameter['description']) ? $parameter['description'] : ''),
      );

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

    // Provide a form for editing the label and name of provided variables.
    $settings = $this->element->settings;
    foreach ($this->element
      ->pluginProvidesVariables() as $var_name => $var_info) {
      $form['provides'][$var_name] = array(
        '#type' => 'fieldset',
        '#title' => check_plain($var_info['label']),
        '#description' => filter_xss(isset($var_info['description']) ? $var_info['description'] : ''),
      );
      $form['provides'][$var_name]['label'] = array(
        '#type' => 'textfield',
        '#title' => t('Variable label'),
        '#default_value' => isset($settings[$var_name . ':label']) ? $settings[$var_name . ':label'] : $var_info['label'],
        '#required' => TRUE,
      );
      $form['provides'][$var_name]['var'] = array(
        '#type' => 'textfield',
        '#title' => t('Variable name'),
        '#default_value' => isset($settings[$var_name . ':var']) ? $settings[$var_name . ':var'] : $var_name,
        '#description' => t('The variable name must contain only lowercase letters, numbers, and underscores and must be unique in the current scope.'),
        '#element_validate' => array(
          'rules_ui_element_machine_name_validate',
        ),
        '#required' => TRUE,
      );
    }
    if (!empty($form['provides'])) {
      $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>';
      $form['provides'] += array(
        '#tree' => TRUE,
        '#prefix' => '<h4 class="rules-form-heading">' . t('Provided variables') . '</h4>' . $help,
      );
    }

    // Add settings form, if specified.
    if (!empty($options['show settings'])) {
      $this
        ->settingsForm($form, $form_state);
    }

    // Add submit button, if specified.
    if (!empty($options['button'])) {
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Save'),
        '#weight' => 10,
      );
    }
  }

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

    // Init the mode.
    if (!isset($mode)) {
      if (isset($settings[$name . ':select'])) {
        $mode = 'selector';
      }
      elseif (isset($settings[$name]) && $supports_input_mode) {
        $mode = 'input';
      }
      elseif (isset($info['restriction'])) {
        $mode = $info['restriction'];
      }
      else {

        // Allow the parameter to define the 'default mode' and fallback to the
        // data type default.
        $mode = !empty($info['default mode']) ? $info['default mode'] : call_user_func(array(
          $class,
          'getDefaultMode',
        ));
      }
    }

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

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

    // Add a link for switching the input mode when JS is enabled and a button
    // to switch it without JavaScript, in case switching is possible.
    if ($supports_input_mode && empty($info['restriction'])) {
      $value = $mode == 'selector' ? t('Switch to the direct input mode') : t('Switch to data selection');
      $form['switch_button'] = array(
        '#type' => 'submit',
        '#name' => 'param_' . $name,
        '#attributes' => array(
          'class' => array(
            'rules-switch-button',
          ),
        ),
        '#parameter' => $name,
        '#value' => $value,
        '#submit' => array(
          'rules_ui_parameter_replace_submit',
        ),
        '#ajax' => rules_ui_form_default_ajax('none'),
        // Do not validate!
        '#limit_validation_errors' => array(),
      );
    }
    return $form;
  }

  /**
   * Implements RulesPluginUIInterface.
   */
  public function form_validate($form, &$form_state) {
    $this
      ->form_extract_values($form, $form_state);
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
    if (isset($form_values['provides'])) {
      $vars = $this->element
        ->availableVariables();
      foreach ($form_values['provides'] as $name => $values) {
        if (isset($vars[$values['var']])) {
          form_error($form['provides'][$name]['var'], t('The variable name %name is already taken.', array(
            '%name' => $values['var'],
          )));
        }
      }
    }

    // Settings have been updated, so process them now.
    $this->element
      ->processSettings(TRUE);

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

  /**
   * Applies the values of the form to the element.
   */
  public function form_extract_values($form, &$form_state) {
    $this->element->settings = array();
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
    if (isset($form_values['parameter'])) {
      foreach ($form_values['parameter'] as $name => $values) {
        $this->element->settings += $values['settings'];
      }
    }
    if (isset($form_values['provides'])) {
      foreach ($form_values['provides'] as $name => $values) {
        $this->element->settings[$name . ':label'] = $values['label'];
        $this->element->settings[$name . ':var'] = $values['var'];
      }
    }
    if (!empty($form['settings'])) {
      $this
        ->settingsFormExtractValues($form, $form_state);
    }
  }

  /**
   * Implements RulesPluginUIInterface.
   */
  public function form_submit($form, &$form_state) {

    // Need to save the element first, before trying to set the component
    // permissions in settingsFormSubmit(), because hook_permission() needs
    // to be able to load the modified element from the DB in order to work
    // properly.
    // @see https://www.drupal.org/project/rules/issues/2340505
    $this->element
      ->save();
    if (!empty($form['settings'])) {
      $this
        ->settingsFormSubmit($form, $form_state);
    }
  }

  /**
   * Adds the configuration settings form (label, tags, description, ...).
   */
  public function settingsForm(&$form, &$form_state) {
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);

    // Add the settings in a separate fieldset below.
    $form['settings'] = array(
      '#type' => 'fieldset',
      '#title' => t('Settings'),
      '#collapsible' => TRUE,
      '#collapsed' => empty($form_values['settings']['vars']['more']),
      '#weight' => 5,
      '#tree' => TRUE,
    );
    $form['settings']['label'] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
      '#default_value' => $this->element
        ->label(),
      '#required' => TRUE,
      '#weight' => -5,
    );

    // @todo For Drupal 8 use "owner" for generating machine names and
    // module only for the modules providing default configurations.
    if (!empty($this->element->module) && !empty($this->element->name) && $this->element->module == 'rules' && strpos($this->element->name, 'rules_') === 0) {

      // Remove the Rules module prefix from the machine name.
      $machine_name = substr($this->element->name, strlen($this->element->module) + 1);
    }
    else {
      $machine_name = $this->element->name;
    }
    $form['settings']['name'] = array(
      '#type' => 'machine_name',
      '#default_value' => isset($machine_name) ? $machine_name : '',
      // The string 'rules_' is pre-pended to machine names, so the
      // maxlength must be less than the field length of 64 characters.
      '#maxlength' => 58,
      '#disabled' => entity_has_status('rules_config', $this->element, ENTITY_IN_CODE) && !(isset($form_state['op']) && $form_state['op'] == 'clone'),
      '#machine_name' => array(
        'exists' => 'rules_config_load',
        'source' => array(
          'settings',
          'label',
        ),
      ),
      '#required' => TRUE,
      '#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.'),
    );
    $form['settings']['tags'] = array(
      '#type' => 'textfield',
      '#title' => t('Tags'),
      '#default_value' => isset($this->element->tags) ? drupal_implode_tags($this->element->tags) : '',
      '#autocomplete_path' => 'admin/config/workflow/rules/autocomplete_tags',
      '#description' => t('Tags associated with this configuration, used for filtering in the admin interface. Separate multiple tags with commas.'),
    );

    // Show a form for editing variables for components.
    if (($plugin_info = $this->element
      ->pluginInfo()) && !empty($plugin_info['component'])) {
      if ($this->element
        ->hasStatus(ENTITY_IN_CODE)) {
        $description = t('The variables used by the component. They can not be edited for configurations that are provided in code.');
      }
      else {
        $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.', array(
          '@url' => rules_external_help('variables'),
        ));
      }
      $form['settings']['vars'] = array(
        '#prefix' => '<div id="rules-component-variables">',
        '#suffix' => '</div>',
        '#tree' => TRUE,
        '#element_validate' => array(
          'rules_ui_element_variable_form_validate',
        ),
        '#theme' => 'rules_ui_variable_form',
        '#title' => t('Variables'),
        '#description' => $description,
        // Variables can not be edited on configurations in code.
        '#disabled' => $this->element
          ->hasStatus(ENTITY_IN_CODE),
      );
      $weight = 0;
      $provides = $this->element
        ->providesVariables();
      foreach ($this->element
        ->componentVariables() as $name => $var_info) {
        $form['settings']['vars']['items'][$name] = array(
          'weight' => array(
            '#default_value' => $weight++,
          ),
        ) + RulesPluginUI::getVariableForm($name, $var_info, isset($provides[$name]));
      }

      // Add one empty row in case user wants to add an additional variable.
      $form['settings']['vars']['items'][] = array(
        'weight' => array(
          '#default_value' => $weight++,
        ),
      ) + RulesPluginUI::getVariableForm();

      // Submit button will cause a form rebuild using the currently-entered
      // values. If a variable has been added, a new empty row will also appear.
      $form['settings']['vars']['more'] = array(
        '#type' => 'submit',
        '#value' => t('Add more'),
        '#ajax' => rules_ui_form_default_ajax('none'),
        '#limit_validation_errors' => array(
          array(
            'vars',
          ),
        ),
        '#submit' => array(
          'rules_form_submit_rebuild',
        ),
      );
      if (!empty($this->element->id)) {

        // Display a setting to manage access.
        $form['settings']['access'] = array(
          '#weight' => 50,
        );
        $plugin_type = $this->element instanceof RulesActionInterface ? t('action') : t('condition');
        $form['settings']['access']['access_exposed'] = array(
          '#type' => 'checkbox',
          '#title' => t('Configure access for using this component with a permission.'),
          '#default_value' => !empty($this->element->access_exposed),
          '#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,
          )),
        );
        $form['settings']['access']['permissions'] = array(
          '#type' => 'container',
          '#states' => array(
            'visible' => array(
              ':input[name="settings[access][access_exposed]"]' => array(
                'checked' => TRUE,
              ),
            ),
          ),
        );
        $form['settings']['access']['permissions']['matrix'] = $this
          ->settingsFormPermissionMatrix();
      }
    }

    // @todo Attach field form thus description.
  }

  /**
   * Provides a matrix permission for the component based in the existing roles.
   *
   * @return array
   *   Form elements with the matrix of permissions for a component.
   */
  protected function settingsFormPermissionMatrix() {
    $form['#theme'] = 'user_admin_permissions';
    $status = array();
    $options = array();
    $role_names = user_roles();
    $role_permissions = user_role_permissions($role_names);
    $component_permission = rules_permissions_by_component(array(
      $this->element,
    ));
    $component_permission_name = key($component_permission);
    $form['permission'][$component_permission_name] = array(
      '#type' => 'item',
      '#markup' => $component_permission[$component_permission_name]['title'],
    );
    $options[$component_permission_name] = '';
    foreach ($role_names as $rid => $name) {
      if (isset($role_permissions[$rid][$component_permission_name])) {
        $status[$rid][] = $component_permission_name;
      }
    }

    // Build the checkboxes for each role.
    foreach ($role_names as $rid => $name) {
      $form['checkboxes'][$rid] = array(
        '#type' => 'checkboxes',
        '#options' => $options,
        '#default_value' => isset($status[$rid]) ? $status[$rid] : array(),
        '#attributes' => array(
          'class' => array(
            'rid-' . $rid,
          ),
        ),
      );
      $form['role_names'][$rid] = array(
        '#markup' => check_plain($name),
        '#tree' => TRUE,
      );
    }

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

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function settingsFormExtractValues($form, &$form_state) {
    $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state);
    $this->element->label = $form_values['label'];

    // If the name was changed we have to redirect to the URL that contains
    // the new name, instead of rebuilding on the old URL with the old name.
    if ($form['settings']['name']['#default_value'] != $form_values['name']) {
      $module = isset($this->element->module) ? $this->element->module : 'rules';
      $this->element->name = $module . '_' . $form_values['name'];
      $form_state['redirect'] = RulesPluginUI::path($this->element->name, 'edit', $this->element);
    }
    $this->element->tags = empty($form_values['tags']) ? array() : drupal_explode_tags($form_values['tags']);
    if (isset($form_values['vars']['items'])) {
      $vars =& $this->element
        ->componentVariables();
      $vars = array();
      if ($this->element instanceof RulesActionContainer) {
        $provides =& $this->element
          ->componentProvidesVariables();
        $provides = array();
      }
      usort($form_values['vars']['items'], 'rules_element_sort_helper');
      foreach ($form_values['vars']['items'] as $item) {
        if ($item['type'] && $item['name'] && $item['label']) {
          $vars[$item['name']] = array(
            'label' => $item['label'],
            'type' => $item['type'],
          );
          if (!$item['usage'][0]) {
            $vars[$item['name']]['parameter'] = FALSE;
          }
          if ($item['usage'][1] && isset($provides)) {
            $provides[] = $item['name'];
          }
        }
      }

      // Disable FAPI persistence for the variable form so renumbering works.
      $input =& $form_state['input'];
      foreach ($form['settings']['#parents'] as $parent) {
        $input =& $input[$parent];
      }
      unset($input['vars']);
    }
    $this->element->access_exposed = isset($form_values['access']['access_exposed']) ? $form_values['access']['access_exposed'] : FALSE;
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function settingsFormValidate($form, &$form_state) {
    $form_values = RulesPluginUI::getFormStateValues($form['settings'], $form_state);
    if ($form['settings']['name']['#default_value'] != $form_values['name'] && rules_config_load($this->element->name)) {
      form_error($form['settings']['name'], t('The machine-readable name %name is already taken.', array(
        '%name' => $form_values['name'],
      )));
    }
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function settingsFormSubmit($form, &$form_state) {
    if (isset($form_state['values']['settings']['access']) && !empty($this->element->access_exposed)) {

      // Save the permission matrix.
      foreach ($form_state['values']['settings']['access']['permissions']['matrix']['checkboxes'] as $rid => $value) {

        // Need to account for the case where the machine name has been changed,
        // because then the $value array variable will be keyed with the wrong
        // permission name. So here we recompute the permission name to use as
        // a key and extract the value from the $value array.
        $component_permission = rules_permissions_by_component(array(
          $this->element,
        ));
        $component_permission_name = key($component_permission);
        user_role_change_permissions($rid, array(
          $component_permission_name => current($value),
        ));
      }
    }
  }

  /**
   * Returns the form for configuring the info of a single variable.
   */
  public function getVariableForm($name = '', $info = array(), $provided = FALSE) {
    $form['type'] = array(
      '#type' => 'select',
      '#options' => array(
        0 => '--',
      ) + RulesPluginUI::getOptions('data'),
      '#default_value' => isset($info['type']) ? $info['type'] : 0,
    );
    $form['label'] = array(
      '#type' => 'textfield',
      '#size' => 40,
      '#default_value' => isset($info['label']) ? $info['label'] : '',
    );
    $form['name'] = array(
      '#type' => 'textfield',
      '#size' => 40,
      '#default_value' => $name,
      '#element_validate' => array(
        'rules_ui_element_machine_name_validate',
      ),
    );
    $usage[0] = !isset($info['parameter']) || $info['parameter'] ? 1 : 0;
    $usage[1] = $provided ? 1 : 0;
    $form['usage'] = array(
      '#type' => 'select',
      '#default_value' => implode('', $usage),
      '#options' => array(
        '10' => t('Parameter'),
        '11' => t('Parameter + Provided'),
        '01' => t('Provided'),
      ),
    );
    if ($this->element instanceof RulesConditionContainer) {
      $form['usage']['#disabled'] = TRUE;
    }

    // Just set the weight #default_value for the returned form.
    $form['weight'] = array(
      '#type' => 'weight',
    );
    return $form;
  }

  /**
   * Returns the name of class for the given data type.
   *
   * @param string $data_type
   *   The name of the data type
   * @param array $parameter_info
   *   (optional) An array of info about the to be configured parameter. If
   *   given, this array is complemented with data type defaults also.
   */
  public function getDataTypeClass($data_type, &$parameter_info = array()) {
    $cache = rules_get_cache();
    $data_info = $cache['data_info'];

    // Add in data-type defaults.
    if (empty($parameter_info['ui class'])) {
      $parameter_info['ui class'] = is_string($data_type) && isset($data_info[$data_type]['ui class']) ? $data_info[$data_type]['ui class'] : 'RulesDataUI';
    }
    if (is_subclass_of($parameter_info['ui class'], 'RulesDataInputOptionsListInterface')) {
      $parameter_info['options list'] = array(
        $parameter_info['ui class'],
        'optionsList',
      );
    }
    return $parameter_info['ui class'];
  }

  /**
   * Implements RulesPluginUIInterface.
   *
   * Shows a preview of the configuration settings.
   */
  public function buildContent() {
    $config_name = $this->element
      ->root()->name;
    $content['label'] = array(
      '#type' => 'link',
      '#title' => $this->element
        ->label(),
      '#href' => $this->element
        ->isRoot() ? RulesPluginUI::path($config_name) : RulesPluginUI::path($config_name, 'edit', $this->element),
      '#prefix' => '<div class="rules-element-label">',
      '#suffix' => '</div>',
    );

    // Put the elements below in a "description" div.
    $content['description'] = array(
      '#prefix' => '<div class="description">',
    );
    $content['description']['parameter'] = array(
      '#caption' => t('Parameter'),
      '#theme' => 'rules_content_group',
    );
    foreach ($this->element
      ->pluginParameterInfo() as $name => $parameter) {
      $element = array();
      if (!empty($this->element->settings[$name . ':select'])) {
        $element['content'] = array(
          '#markup' => '[' . $this->element->settings[$name . ':select'] . ']',
        );
      }
      elseif (isset($this->element->settings[$name])) {
        $class = $this
          ->getDataTypeClass($parameter['type'], $parameter);
        $method = empty($parameter['options list']) ? 'render' : 'renderOptionsLabel';

        // We cannot use method_exists() here as it would trigger a PHP bug.
        // @see https://www.drupal.org/node/1258284
        $element = call_user_func(array(
          $class,
          $method,
        ), $this->element->settings[$name], $name, $parameter, $this->element);
      }

      // Only add parameters that are really configured / not default.
      if ($element) {
        $content['description']['parameter'][$name] = array(
          '#theme' => 'rules_parameter_configuration',
          '#info' => $parameter,
        ) + $element;
      }
    }
    foreach ($this->element
      ->providesVariables() as $name => $var_info) {
      $content['description']['provides'][$name] = array(
        '#theme' => 'rules_variable_view',
        '#info' => $var_info,
        '#name' => $name,
      );
    }
    if (!empty($content['description']['provides'])) {
      $content['description']['provides'] += array(
        '#caption' => t('Provides variables'),
        '#theme' => 'rules_content_group',
      );
    }

    // Add integrity exception messages if there are any for this element.
    try {
      $this->element
        ->integrityCheck();

      // A configuration is still marked as dirty, but already works again.
      if (!empty($this->element->dirty)) {
        rules_config_update_dirty_flag($this->element);
        $variables = array(
          '%label' => $this->element
            ->label(),
          '%name' => $this->element->name,
          '@plugin' => $this->element
            ->plugin(),
        );
        drupal_set_message(t('The @plugin %label (%name) was marked dirty, but passes the integrity check now and is active again.', $variables));
        rules_clear_cache();
      }
    } catch (RulesIntegrityException $e) {
      $content['description']['integrity'] = array(
        '#theme' => 'rules_content_group',
        '#caption' => t('Error'),
        '#attributes' => array(
          'class' => array(
            'rules-content-group-integrity-error',
          ),
        ),
        'error' => array(
          '#markup' => filter_xss($e
            ->getMessage()),
        ),
      );

      // Also make sure the rule is marked as dirty.
      if (empty($this->element->dirty)) {
        rules_config_update_dirty_flag($this->element);
        rules_clear_cache();
      }
    }
    $content['#suffix'] = '</div>';
    $content['#type'] = 'container';
    $content['#attributes']['class'][] = 'rules-element-content';
    return $content;
  }

  /**
   * Implements RulesPluginUIInterface.
   */
  public function operations() {
    $name = $this->element
      ->root()->name;
    $render = array(
      '#theme' => 'links__rules',
    );
    $render['#attributes']['class'][] = 'rules-operations';
    $render['#attributes']['class'][] = 'action-links';
    $render['#links']['edit'] = array(
      'title' => t('edit'),
      'href' => RulesPluginUI::path($name, 'edit', $this->element),
    );
    $render['#links']['delete'] = array(
      'title' => t('delete'),
      'href' => RulesPluginUI::path($name, 'delete', $this->element),
    );
    return $render;
  }

  /**
   * Implements RulesPluginUIInterface.
   */
  public function help() {
  }

  /**
   * Deprecated by the controllers overviewTable() method.
   */
  public static function overviewTable($conditions = array(), $options = array()) {
    return rules_ui()
      ->overviewTable($conditions, $options);
  }

  /**
   * Generates an operation path.
   *
   * Generates a path using the given operation for the element with the given
   * id of the configuration with the given name.
   */
  public static function path($name, $op = NULL, RulesPlugin $element = NULL, $parameter = FALSE) {
    $element_id = isset($element) ? $element
      ->elementId() : FALSE;
    if (isset(self::$basePath)) {
      $base_path = self::$basePath;
    }
    else {
      $base_path = isset($element) && $element instanceof RulesTriggerableInterface ? 'admin/config/workflow/rules/reaction' : 'admin/config/workflow/rules/components';
    }

    // Only append the '/manage' path if it is not already present.
    if (substr($base_path, -strlen('/manage')) != '/manage') {
      $base_path .= '/manage';
    }
    return implode('/', array_filter(array(
      $base_path,
      $name,
      $op,
      $element_id,
      $parameter,
    )));
  }

  /**
   * Determines the default redirect target for an edited/deleted element.
   *
   * This is a parent element which is either a rule or the configuration root.
   */
  public static function defaultRedirect(RulesPlugin $element) {
    while (!$element
      ->isRoot()) {
      if ($element instanceof Rule) {
        return self::path($element
          ->root()->name, 'edit', $element);
      }
      $element = $element
        ->parentElement();
    }
    return self::path($element->name);
  }

  /**
   * @see RulesUICategory::getOptions()
   */
  public static function getOptions($item_type, $items = NULL) {
    return RulesUICategory::getOptions($item_type, $items = NULL);
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public static function formDefaults(&$form, &$form_state) {
    form_load_include($form_state, 'inc', 'rules', 'ui/ui.forms');

    // Add our own css.
    $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.css';

    // Workaround for problems with jquery css in seven theme and the core
    // autocomplete.
    if ($GLOBALS['theme'] == 'seven') {
      $form['#attached']['css'][] = drupal_get_path('module', 'rules') . '/ui/rules.ui.seven.css';
    }

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

    // Preserve the base path in the form state. The after build handler will
    // set self::$basePath again for cached forms.
    if (isset(self::$basePath)) {
      $form_state['_rules_base_path'] = RulesPluginUI::$basePath;
      $form['#after_build'][] = 'rules_form_after_build_restore_base_path';
    }
  }
  public static function getTags() {
    $result = db_select('rules_tags')
      ->distinct()
      ->fields('rules_tags', array(
      'tag',
    ))
      ->groupBy('tag')
      ->execute()
      ->fetchCol('tag');
    return drupal_map_assoc($result);
  }

}

/**
 * UI for abstract plugins (conditions & actions).
 */
class RulesAbstractPluginUI extends RulesPluginUI {

  /**
   * Overrides RulesPluginUI::form().
   *
   * Overridden to invoke the abstract plugins form alter callback and to add
   * the negation checkbox for conditions.
   */
  public function form(&$form, &$form_state, $options = array()) {
    parent::form($form, $form_state, $options);
    if ($this->element instanceof RulesCondition) {
      $form['negate'] = array(
        '#title' => t('Negate'),
        '#type' => 'checkbox',
        '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
        '#default_value' => $this->element
          ->isNegated(),
        '#weight' => 5,
      );
    }
    $this->element
      ->call('form_alter', array(
      &$form,
      &$form_state,
      $options,
    ));
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function form_extract_values($form, &$form_state) {
    parent::form_extract_values($form, $form_state);
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
    if ($this->element instanceof RulesCondition && isset($form_values['negate'])) {
      $this->element
        ->negate($form_values['negate']);
    }
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function form_validate($form, &$form_state) {
    parent::form_validate($form, $form_state);

    // Validate the edited element and throw validation errors if it fails.
    try {
      $this->element
        ->integrityCheck();
    } catch (RulesIntegrityException $e) {
      form_set_error(implode('][', $e->keys), $e
        ->getMessage());
    }
  }

}

/**
 * UI for Rules Container.
 */
class RulesContainerPluginUI extends RulesPluginUI {

  /**
   * Generates a table for editing the contained elements.
   */
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
    parent::form($form, $form_state, $options);
    $form['elements'] = array(
      // Hide during creation or for embedded elements.
      '#access' => empty($options['init']) && $this->element
        ->isRoot(),
      '#tree' => TRUE,
      '#theme' => 'rules_elements',
      '#empty' => t('None'),
      '#caption' => t('Elements'),
    );
    $form['elements']['#attributes']['class'][] = 'rules-container-plugin';

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

      // Do not render rules as container element when displayed in a rule set.
      $is_container = $child instanceof RulesContainerPlugin && !$child instanceof Rule;
      $form['elements'][$id] = array(
        '#depth' => $child
          ->depth() - $root_depth - 1,
        '#container' => $is_container,
      );
      $form['elements'][$id]['label'] = $child
        ->buildContent();
      $form['elements'][$id]['weight'] = array(
        '#type' => 'weight',
        '#default_value' => $child->weight,
        '#delta' => 50,
      );
      $form['elements'][$id]['parent_id'] = array(
        '#type' => 'hidden',
        // If another iterator is passed in, the child parent may not equal
        // the current element. Thus ask the child for its parent.
        '#default_value' => $child
          ->parentElement()
          ->elementId(),
      );
      $form['elements'][$id]['element_id'] = array(
        '#type' => 'hidden',
        '#default_value' => $id,
      );
      $form['elements'][$id]['operations'] = $child
        ->operations();
    }

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

  /**
   * Applies the values of the form to the given rule configuration.
   */
  public function form_extract_values($form, &$form_state) {
    parent::form_extract_values($form, $form_state);
    $values = RulesPluginUI::getFormStateValues($form, $form_state);

    // Now apply the new hierarchy.
    if (isset($values['elements'])) {
      foreach ($values['elements'] as $id => $data) {
        $child = $this->element
          ->elementMap()
          ->lookup($id);
        $child->weight = $data['weight'];
        $parent = $this->element
          ->elementMap()
          ->lookup($data['parent_id']);
        $child
          ->setParent($parent ? $parent : $this->element);
      }
      $this->element
        ->sortChildren(TRUE);
    }
  }
  public function operations() {
    $ops = parent::operations();
    $add_ops = self::addOperations();
    $ops['#links'] += $add_ops['#links'];
    return $ops;
  }

  /**
   * Gets the Add-* operations for the given element.
   */
  public function addOperations() {
    $name = $this->element
      ->root()->name;
    $render = array(
      '#theme' => 'links__rules',
    );
    $render['#attributes']['class'][] = 'rules-operations-add';
    $render['#attributes']['class'][] = 'action-links';
    foreach (rules_fetch_data('plugin_info') as $plugin => $info) {
      if (!empty($info['embeddable']) && $this->element instanceof $info['embeddable']) {
        $render['#links']['add_' . $plugin] = array(
          'title' => t('Add !name', array(
            '!name' => $plugin,
          )),
          'href' => RulesPluginUI::path($name, 'add', $this->element, $plugin),
        );
      }
    }
    return $render;
  }
  public function buildContent() {
    $content = parent::buildContent();

    // Don't link the title for embedded container plugins, except for rules.
    if (!$this->element
      ->isRoot() && !$this->element instanceof Rule) {

      // $content['label']['#type'] is currently set to 'link', but in this
      // case we don't want a link, we just want 'markup' text.
      $content['label']['#type'] = 'markup';
      $content['label']['#markup'] = check_plain($content['label']['#title']);
      unset($content['label']['#title']);
    }
    elseif ($this->element
      ->isRoot()) {
      $content['description']['settings'] = array(
        '#theme' => 'rules_content_group',
        '#weight' => -4,
        'machine_name' => array(
          '#markup' => t('Machine name') . ': ' . $this->element->name,
        ),
        'weight' => array(
          '#access' => $this->element instanceof RulesTriggerableInterface,
          '#markup' => t('Weight') . ': ' . $this->element->weight,
        ),
      );
      if (!empty($this->element->tags)) {
        $content['description']['tags'] = array(
          '#theme' => 'rules_content_group',
          '#caption' => t('Tags'),
          'tags' => array(
            '#markup' => implode(', ', array_map(function ($entry) {
              return l($entry, '/admin/config/workflow/rules', array(
                'query' => array(
                  'event' => '0',
                  'tag' => $entry,
                ),
              ));
            }, $this->element->tags)),
          ),
        );
      }
      if ($vars = $this->element
        ->componentVariables()) {
        $content['description']['variables'] = array(
          '#caption' => t('Parameter'),
          '#theme' => 'rules_content_group',
        );
        foreach ($vars as $name => $info) {
          if (!isset($info['parameter']) || $info['parameter']) {
            $content['description']['variables'][$name] = array(
              '#theme' => 'rules_variable_view',
              '#info' => $info,
              '#name' => $name,
            );
          }
        }
      }
    }
    return $content;
  }

}

/**
 * UI for Rules condition container.
 */
class RulesConditionContainerUI extends RulesContainerPluginUI {

  /**
   * Implements RulesPluginUIInterface::form().
   */
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
    parent::form($form, $form_state, $options, $iterator);

    // Add the add-* operation links.
    $form['elements']['#add'] = self::addOperations();
    $form['elements']['#attributes']['class'][] = 'rules-condition-container';
    $form['elements']['#caption'] = t('Conditions');

    // By default skip.
    if (!empty($options['init']) && !$this->element
      ->isRoot()) {
      $config = $this->element
        ->root();
      $form['init_help'] = array(
        '#type' => 'container',
        '#id' => 'rules-plugin-add-help',
        'content' => array(
          '#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.', array(
            '@plugin' => $this->element
              ->plugin(),
            '@config-plugin' => $config
              ->plugin(),
            '%label' => $config
              ->label(),
            '@url' => rules_external_help('condition-components'),
          )),
        ),
      );
    }
    $form['negate'] = array(
      '#title' => t('Negate'),
      '#type' => 'checkbox',
      '#description' => t('If checked, the condition result is negated such that it returns TRUE if it evaluates to FALSE.'),
      '#default_value' => $this->element
        ->isNegated(),
      '#weight' => 5,
    );
  }

  /**
   * @param array $form
   *   The form array where to add the form.
   * @param array $form_state
   *   The current form state.
   */
  public function form_extract_values($form, &$form_state) {
    parent::form_extract_values($form, $form_state);
    $form_values = RulesPluginUI::getFormStateValues($form, $form_state);
    if (isset($form_values['negate'])) {
      $this->element
        ->negate($form_values['negate']);
    }
  }

}

/**
 * UI for Rules action container.
 */
class RulesActionContainerUI extends RulesContainerPluginUI {

  /**
   * Implements RulesPluginUIInterface::form().
   */
  public function form(&$form, &$form_state, $options = array(), $iterator = NULL) {
    parent::form($form, $form_state, $options, $iterator);

    // Add the add-* operation links.
    $form['elements']['#add'] = self::addOperations();
    $form['elements']['#attributes']['class'][] = 'rules-action-container';
    $form['elements']['#caption'] = t('Actions');
  }

}

/**
 * Class holding category related methods.
 */
class RulesUICategory {

  /**
   * Gets info about all available categories, or about a specific category.
   *
   * @return array
   */
  public static function getInfo($category = NULL) {
    $data = rules_fetch_data('category_info');
    if (isset($category)) {
      return $data[$category];
    }
    return $data;
  }

  /**
   * Returns a group label, e.g. as usable for opt-groups in a select list.
   *
   * @param array $item_info
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
   * @param bool $in_category
   *   (optional) Whether group labels for grouping inside a category should be
   *   return. Defaults to FALSE.
   *
   * @return string|bool
   *   The group label to use, or FALSE if none can be found.
   */
  public static function getItemGroup($item_info, $in_category = FALSE) {
    if (isset($item_info['category']) && !$in_category) {
      return self::getCategory($item_info, 'label');
    }
    elseif (!empty($item_info['group'])) {
      return $item_info['group'];
    }
    return FALSE;
  }

  /**
   * Gets the category for the given item info array.
   *
   * @param array $item_info
   *   The info-array of an item, e.g. an entry of hook_rules_action_info().
   * @param string|null $key
   *   (optional) The key of the category info to return, e.g. 'label'. If none
   *   is given the whole info array is returned.
   *
   * @return array|mixed|false
   *   Either the whole category info array or the value of the given key. If
   *   no category can be found, FALSE is returned.
   */
  public static function getCategory($item_info, $key = NULL) {
    if (isset($item_info['category'])) {
      $info = self::getInfo($item_info['category']);
      return isset($key) ? $info[$key] : $info;
    }
    return FALSE;
  }

  /**
   * Returns an array of options to use with a select.
   *
   * Returns an array of options to use with a selectfor the items specified
   * in the given hook.
   *
   * @param string $item_type
   *   The item type to get options for. One of 'data', 'event', 'condition' and
   *   'action'.
   * @param array|null $items
   *   (optional) An array of items to restrict the options to.
   *
   * @return array
   *   An array of options.
   */
  public static function getOptions($item_type, $items = NULL) {
    $sorted_data = array();
    $ungrouped = array();
    $data = $items ? $items : rules_fetch_data($item_type . '_info');
    foreach ($data as $name => $info) {

      // Verify the current user has access to use it.
      if (!user_access('bypass rules access') && !empty($info['access callback']) && !call_user_func($info['access callback'], $item_type, $name)) {
        continue;
      }
      if ($group = RulesUICategory::getItemGroup($info)) {
        $sorted_data[drupal_ucfirst($group)][$name] = drupal_ucfirst($info['label']);
      }
      else {
        $ungrouped[$name] = drupal_ucfirst($info['label']);
      }
    }
    asort($ungrouped);
    foreach ($sorted_data as $key => $choices) {
      asort($choices);
      $sorted_data[$key] = $choices;
    }

    // Sort the grouped data by category weights, defaulting to weight 0 for
    // groups without a respective category.
    $sorted_groups = array();
    foreach (array_keys($sorted_data) as $label) {
      $sorted_groups[$label] = array(
        'weight' => 0,
        'label' => $label,
      );
    }

    // Add in category weights.
    foreach (RulesUICategory::getInfo() as $info) {
      if (isset($sorted_groups[$info['label']])) {
        $sorted_groups[$info['label']] = $info;
      }
    }
    uasort($sorted_groups, '_rules_ui_sort_categories');

    // Now replace weights with group content.
    foreach ($sorted_groups as $group => $weight) {
      $sorted_groups[$group] = $sorted_data[$group];
    }
    return $ungrouped + $sorted_groups;
  }

}

/**
 * Helper for sorting categories.
 */
function _rules_ui_sort_categories($a, $b) {

  // @see element_sort()
  $a_weight = isset($a['weight']) ? $a['weight'] : 0;
  $b_weight = isset($b['weight']) ? $b['weight'] : 0;
  if ($a_weight == $b_weight) {

    // @see element_sort_by_title()
    $a_title = isset($a['label']) ? $a['label'] : '';
    $b_title = isset($b['label']) ? $b['label'] : '';
    return strnatcasecmp($a_title, $b_title);
  }
  return $a_weight < $b_weight ? -1 : 1;
}

Functions

Namesort descending Description
_rules_ui_sort_categories Helper for sorting categories.

Classes

Namesort descending Description
RulesAbstractPluginUI UI for abstract plugins (conditions & actions).
RulesActionContainerUI UI for Rules action container.
RulesConditionContainerUI UI for Rules condition container.
RulesContainerPluginUI UI for Rules Container.
RulesElementMap Helper object for mapping elements to ids.
RulesPluginUI Faces UI extender for all kind of Rules plugins.
RulesUICategory Class holding category related methods.

Interfaces

Namesort descending Description
RulesPluginUIInterface Plugin UI Interface.