You are here

vbo_search_and_replace.module in Views Bulk Operations Search & Replace 6

Same filename and directory in other branches
  1. 7 vbo_search_and_replace.module

vbo_search_and_replace.module

Defines an search and replace action for Views Bulk Operations

File

vbo_search_and_replace.module
View source
<?php

/**
 * @file vbo_search_and_replace.module
 *
 * Defines an search and replace action for Views Bulk Operations
 */
define('VBO_SNR_NO_FIELD_EXLUSIONS', '_none_');

/***********************************************************************
 *
 * ACTION FUNCTIONS
 *
 ***********************************************************************/

/**
 * Implementation of hook_action_info().
 * Called by VBO on its own hook_action_info().
 */
function vbo_search_and_replace_action_info() {
  if (!module_exists('content')) {
    return array();
  }
  return array(
    'vbo_search_and_replace_action' => array(
      'type' => 'node',
      'description' => t('Search and Replace'),
      'configurable' => TRUE,
      // 'behavior' => array('changes_node_property'),
      // 'form properties' => array('#field_info'),
      'rules_ignore' => TRUE,
    ),
  );
}

/**
 * Action form function
 */
function vbo_search_and_replace_action_form($context) {

  //drupal_add_css(drupal_get_path('module', 'vbo_search_and_replace') . '/style.css');
  $form = array();

  // This action form uses static-time settings. If they were not set, pull the defaults now.
  if (!isset($context['settings'])) {
    $context['settings'] = vbo_search_and_replace_action_views_bulk_operations_form_default_settings();
  }

  // Add a little styling to display the checkboxes inline if settings are set
  if ($context['settings']['selection_widget'] == 'checkboxes' && $context['settings']['checkbox_columns']) {
    $style = '<style>
.views-bulk-operations-form .form-checkboxes.fields-to-search .form-item {
  width: 200px;
  display: inline-block;
}</style>';
    drupal_set_html_head($style);
  }
  $options = vbo_search_and_replace_get_available_fields($context, $context['settings']['selection_widget'] == 'select');
  $form['fields_to_search'] = array(
    '#title' => t('Field(s) to Search'),
    '#type' => $context['settings']['selection_widget'],
    '#multiple' => TRUE,
    '#size' => 20,
    '#description' => t('Choose which field(s) to perform a search and replace on'),
    '#options' => $options,
    '#required' => TRUE,
    '#attributes' => array(
      'class' => 'fields-to-search',
    ),
    '#default_value' => array(),
  );
  $form['all_fields'] = array(
    '#type' => 'value',
    '#default_value' => array_slice(vbo_search_and_replace_get_available_fields($context), 1),
  );
  $form['search'] = array(
    '#title' => t('Search'),
    '#type' => 'textarea',
    '#description' => t('The string to search for, that will be replaced'),
    '#required' => TRUE,
  );
  $form['replace'] = array(
    '#title' => t('Replace'),
    '#type' => 'textarea',
    '#description' => t('The replacement string. The search string will be replaced with this.'),
  );
  $form['advanced'] = array(
    '#title' => t('Advanced Options'),
    '#type' => 'fieldset',
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#attributes' => array(
      'class' => 'search-and-replace-advanced-options',
    ),
  );
  $form['advanced']['details'] = array(
    '#value' => t('Enhanced searches can be performed by specifying a prefix and a suffix for the search string. Only results that match the prefix, the search string and the suffix will be affected, however, only the search string will be replaced. <br/>
    <em><strong>Example 1:</strong><br/>
    <ul>
    <li> *Search String: "brown"</li>
    <li> *Replacement String: "yellow"</li>
    </ul>
    Using the above parameters, all the instances of "brown" will be changed to "yellow"<br/>
    <strong>Example 1:</strong><br/>
    <ul>
    <li> *Prefix: "the quick "</li>
    <li> *Search String: "brown"</li>
    <li> *Suffix: " fox"</li>
    <li> *Replacement String: "yellow"</li>
    </ul>
    Using the above parameters, "brown" will only be replaced with "yellow" if it is wrapped in the prefix and suffix. So, "the quick brown fox" will become to "the quick yellow fox"</em>'),
    '#prefix' => '<div class="description">',
    '#suffix' => '</div>',
  );
  $form['advanced']['search_prefix'] = array(
    '#title' => t('Search Prefix'),
    '#type' => 'textfield',
    '#description' => t('The string that search string must be immediately preceeded by in order for a match to be made'),
  );
  $form['advanced']['search_suffix'] = array(
    '#title' => t('Search Suffix'),
    '#type' => 'textfield',
    '#description' => t('The string that the search string must be immediately followed by in order for a match to be made'),
  );
  $form['advanced']['case_sensitive'] = array(
    '#title' => t('Case Sensitive'),
    '#type' => 'checkbox',
    '#default_value' => 0,
    '#description' => t('Whether or not the search should be case sensitive'),
  );
  $form['advanced']['exact_match'] = array(
    '#title' => t('Exact Match'),
    '#type' => 'checkbox',
    '#default_value' => 0,
    '#description' => t('Check this if the entire field must match the search parameters including the prefix and suffix. <i>example: With this option selected, for the search string "The quick brown fox" to obtain a match, the entire contents of the field must be "The quick brown fox". A field with the value "The quick brown fox jumps over the lazy dog" will not be considered a match</i>'),
  );
  return $form;
}

/**
 * Search and replace action submit function.
 */
function vbo_search_and_replace_action_submit($form, &$form_state) {

  // Get the selected fields to perform search and replace on
  $fields = $form_state['values']['fields_to_search'];

  // Field and property data
  $all_fields = content_fields();
  $properties = vbo_search_and_replace_node_properties();

  // If it's set to do all fields we process accordingly
  if (isset($fields['_all_']) && $fields['_all_']) {

    // rebuild the fields array
    $fields = array();
    foreach ($form_state['values']['all_fields'] as $key => $value) {

      // If it's a property, we pull the label from $properties
      if (array_key_exists($key, $properties)) {
        $fields[$key] = $properties[$key];
      }
      else {

        // Otherwise it's a field so we pull it from the field info
        $fields[$key] = $all_fields[$key]['widget']['label'];
      }
    }
  }
  else {

    // Otherwise, 'all' isn't set so we disable the ones that are
    foreach ($fields as $key => $value) {
      if ($value) {

        // If it's a property, we pull the label from $properties
        if (array_key_exists($key, $properties)) {
          $fields[$key] = $properties[$key];
        }
        else {

          // Otherwise it's a field so we pull it from the field info
          $fields[$key] = $all_fields[$key]['widget']['label'];
        }
      }
      else {

        // Just unset if it's not selected
        unset($fields[$key]);
      }
    }
  }
  $settings = array(
    'search_prefix' => $form_state['values']['search_prefix'],
    'search_suffix' => $form_state['values']['search_suffix'],
    'case_sensitive' => $form_state['values']['case_sensitive'],
    'exact_match' => $form_state['values']['exact_match'],
  );
  return array(
    'fields' => $fields,
    'search_and_replace_settings' => $settings,
    'search' => $form_state['values']['search'],
    'replace' => $form_state['values']['replace'],
  );
}
function vbo_search_and_replace_action(&$node, $context) {

  // Flag for if a search and replace actually changes the node
  $node_changed = FALSE;

  // Token replacements for drupal_set_message
  $replacements = array(
    '@node_type' => $node->type,
    '@node_title' => $node->title,
  );

  // List of non cck fields
  $properties = vbo_search_and_replace_node_properties();

  // Get our settings
  $settings = $context['search_and_replace_settings'];
  foreach ($context['fields'] as $field_name => $field_label) {
    $replacements['@field_label'] = $field_label;
    if (property_exists($node, $field_name)) {

      // If it's a non-cck
      $replaced = '';
      if (array_key_exists($field_name, $properties)) {
        $replaced = _vbo_search_and_replace_search_and_replace($context['search'], $context['replace'], $node->{$field_name}, $settings);
        if ($node->{$field_name} != $replaced) {
          $replacements['@old'] = $node->{$field_name};
          $replacements['@new'] = $replaced;
          $node->{$field_name} = $replaced;
          $node_changed = TRUE;
          drupal_set_message(t('Field @field_label in @node_type: @node_title changed from: "@old" to "@new"', $replacements));
        }
        else {
          drupal_set_message(t('Field @field_label in @node_type: @node_title skipped', $replacements));
        }
      }
      else {
        foreach ($node->{$field_name} as $delta => $item) {
          $replaced = _vbo_search_and_replace_search_and_replace($context['search'], $context['replace'], $node->{$field_name}[$delta]['value'], $settings);
          if ($node->{$field_name}[$delta]['value'] != $replaced) {
            $replacements['@old'] = $node->{$field_name}[$delta]['value'];
            $replacements['@new'] = $replaced;
            $node->{$field_name}[$delta]['value'] = $replaced;
            $node_changed = TRUE;
            drupal_set_message(t('Field @field_label in @node_type: @node_title changed from: "@old" to "@new"', $replacements));
          }
          else {
            drupal_set_message(t('Field @field_label in @node_type: @node_title skipped', $replacements));
          }
        }
      }
    }
  }

  // Save the node
  if ($node_changed) {
    node_save($node);
  }
}

/*****************************************************************************
*
* SETTINGS FORM ON VBO STYLE PLUG-IN SETTINGS PAGE
*
*****************************************************************************/

/**
 * Implementation of hook_views_bulk_operations_form()
 * where hook is the name of the action callback function
 */
function vbo_search_and_replace_action_views_bulk_operations_form($settings) {
  $fields = array(
    VBO_SNR_NO_FIELD_EXLUSIONS => t('- No Exclusions -'),
  ) + vbo_search_and_replace_node_properties();
  $form = array();
  foreach (content_fields() as $field) {

    // We only want to find and replace on text fields
    if ($field['widget']['type'] == 'text_textarea' || $field['widget']['type'] == 'text_textfield') {

      // Push the fields into an array as an opt group
      $fields[$field['type_name']][$field['field_name']] = $field['widget']['label'] . ' (' . $field['field_name'] . ')';
    }
  }

  //Sort the fields within the type
  foreach ($fields as $type => $field) {
    if (count($fields[$type]) > 1) {
      sort($fields[$type]);
    }
  }
  if (empty($settings['exclude_fields'])) {
    $settings['exclude_fields'] = array(
      VBO_SNR_NO_FIELD_EXLUSIONS,
    );
  }
  if (empty($settings['selection_widget'])) {
    $settings['widget'] = 'checkboxes';
  }
  if (empty($settings['checkbox_columns'])) {
    $settings['checkbox_columns'] = 1;
  }
  $form['selection_widget'] = array(
    '#type' => 'select',
    '#title' => t('Widget for selecting fields'),
    '#options' => array(
      'select' => 'Select List',
      'checkboxes' => 'Checkboxes',
    ),
    '#description' => t('Select the widget that will be presented to the user to select which fields to perform the search and replace on'),
    '#default_value' => $settings['selection_widget'],
  );
  $form['checkbox_columns'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display checkboxes inline'),
    '#description' => t('If you have checkboxes selected for the widget, this option will add a little bit of styling to the page to display the checkboxes inline as columns.'),
    '#default_value' => $settings['checkbox_columns'],
  );
  $form['exclude_fields'] = array(
    '#type' => 'select',
    '#title' => t('Exclude fields'),
    '#options' => $fields,
    '#multiple' => TRUE,
    '#size' => 20,
    '#description' => t('Select which field(s) to exclude from search and replace. NOTE: Only textfields and textareas that belong to node types that are selected in the view will be available for search and replace, this list is simply a way to further exclude fields from being available for search and replace.'),
    '#default_value' => $settings['exclude_fields'],
  );
  return $form;
}
function vbo_search_and_replace_action_views_bulk_operations_form_validate($form, $form_state) {
  if (empty($form_state['values']['exclude_fields'])) {
    form_set_error($form_state['values']['_error_element_base'] . 'exclude_fields', t('You must select at least one item in the list. Select "- No Exlusions -" if you wish to make all fields available for search and replace.'));
  }
}

/*****************************************************************************
*
* HELPER FUNCTIONS
*
*****************************************************************************/

/**
 * Helper Function to get default values if options weren't configured yet
 */
function vbo_search_and_replace_action_views_bulk_operations_form_default_settings() {
  $settings['exclude_fields'] = array(
    VBO_SNR_NO_FIELD_EXLUSIONS,
  );
  $settings['selection_widget'] = 'checkboxes';
  $settings['checkbox_columns'] = 1;
  return $settings;
}

/**
 * Returns a list of node properties that can be searched and replaced on
 */
function vbo_search_and_replace_node_properties() {
  return array(
    'title' => 'Title',
    'body' => 'Body',
    'teaser' => 'Teaser',
  );
}

/**
 * Helper function that returns the list of options for the fields to search
 */
function vbo_search_and_replace_get_available_fields($context, $nested = FALSE) {

  // Initialize an array to store the options
  $options = array(
    VBO_ACTION_FIELDS_ALL => "*All Fields",
  ) + vbo_search_and_replace_node_properties();

  // If this is VBO using the action...
  if (isset($context['selection']) && isset($context['view'])) {

    // Build an array of the nids of all the selected rows
    $nids = array_map('_views_bulk_operations_get_oid', $context['selection'], array_fill(0, count($context['selection']), $context['view']->base_field));

    // Get the distinct node types for the selected rows
    $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid IN (%s)", implode(',', $nids));
  }
  else {

    // It's not VBO So we just take all the node types
    $result = db_query("SELECT type from {node_type}");
  }

  // Array to store all fields in
  $fields = array();

  // load the fields for the selected node types
  while ($type = db_result($result)) {
    $type_info = content_types($type);
    $fields += $type_info['fields'];
  }
  foreach ($fields as $field) {

    // Fields must be text fields
    if ($field['widget']['type'] != 'text_textarea' && $field['widget']['type'] != 'text_textfield') {
      continue;
    }

    // Permissions must be enabled to edit this field
    if (module_exists('content_permissions') && in_array('edit ' . $field['field_name'], module_invoke('content_permissions', 'perm')) && !user_access('edit ' . $field['field_name'])) {
      continue;
    }

    // The field should not be excluded. "- No Exlusions -" Trumps any excluded fields
    if (!empty($context['settings']['exclude_fields']) && in_array($field['field_name'], $context['settings']['exclude_fields']) && !in_array(VBO_SNR_NO_FIELD_EXLUSIONS, $context['settings']['exclude_fields'])) {
      continue;
    }
    if ($nested) {

      // Group the fields in an opt group
      $options[$field['type_name']][$field['field_name']] = $field['widget']['label'];
    }
    else {

      // normal options list
      $options[$field['field_name']] = $field['widget']['label'];
    }
  }

  // Sort the fields alphabetically
  if ($nested) {
    foreach ($options as $type => $fields) {
      if (is_array($options[$type])) {
        asort($options[$type]);
      }
    }
  }
  else {
    asort($options);
  }
  return $options;
}

/**
 * Performs a search and replace on a value and returns the result.
 *
 * Search is case insensitive by default but can be changed in $settings
 *
 * @param $search
 *    The value to search for
 *
 * @param $replace
 *    The value to use as a replacement
 *
 * @param $value
 *    The Value to perform search and replace on
 *
 * @param $settings
 *    An array of settings for the search and replace:
 *      - search_prefix: Add a prefix to the search.
 *      - search_suffix: Add a suffix to the search
 *      - exact_match: Set to TRUE to match entire $subject instead of just a part
 *      - case_sensitive: Set to TRUE to make the search case sensitive
 */
function _vbo_search_and_replace_search_and_replace($search, $replace, $subject, $settings = array()) {

  // Set up settings that aren't set
  $settings['search_prefix'] = isset($settings['search_prefix']) ? $settings['search_prefix'] : '';
  $settings['search_suffix'] = isset($settings['search_suffix']) ? $settings['search_suffix'] : '';
  $settings['case_sensitive'] = isset($settings['case_sensitive']) ? $settings['case_sensitive'] : '';
  $settings['exact_match'] = isset($settings['exact_match']) ? $settings['exact_match'] : '';

  // Our search and replace strings WITH prefix and suffix
  $search = $settings['search_prefix'] . $search . $settings['search_suffix'];
  $replace = $settings['search_prefix'] . $replace . $settings['search_suffix'];

  // If it's an exact match, perform search and replace on the entire value
  if ($settings['exact_match']) {
    if ($subject === $search) {
      $subject = $replace;
    }
  }
  else {
    if ($settings['case_sensitive']) {
      if (strpos($subject, $search) !== FALSE) {
        $subject = str_replace($search, $replace, $subject);
      }
    }
    else {
      if (strpos(strtolower($subject), strtolower($search)) !== FALSE) {
        $subject = str_ireplace($search, $replace, $subject);
      }
    }
  }

  // Return the subject
  return $subject;
}

Functions

Namesort descending Description
vbo_search_and_replace_action
vbo_search_and_replace_action_form Action form function
vbo_search_and_replace_action_info Implementation of hook_action_info(). Called by VBO on its own hook_action_info().
vbo_search_and_replace_action_submit Search and replace action submit function.
vbo_search_and_replace_action_views_bulk_operations_form Implementation of hook_views_bulk_operations_form() where hook is the name of the action callback function
vbo_search_and_replace_action_views_bulk_operations_form_default_settings Helper Function to get default values if options weren't configured yet
vbo_search_and_replace_action_views_bulk_operations_form_validate
vbo_search_and_replace_get_available_fields Helper function that returns the list of options for the fields to search
vbo_search_and_replace_node_properties Returns a list of node properties that can be searched and replaced on
_vbo_search_and_replace_search_and_replace Performs a search and replace on a value and returns the result.

Constants