<?php
// $Id: views_bulk_operations.module,v 1.29.2.9.2.128 2010/02/04 00:43:02 kratib Exp $

/**
 * @file 
 * Allows operations to be performed on items selected in a view.
 */

// Define the steps in the multistep form that executes operations.
define('VIEWS_BULK_OPS_STEP_VIEW', 1);
define('VIEWS_BULK_OPS_STEP_CONFIG', 2);
define('VIEWS_BULK_OPS_STEP_CONFIRM', 3);
define('VIEWS_BULK_OPS_STEP_SINGLE', 4);

// Types of bulk execution.
define('VBO_EXECUTION_DIRECT',    1);
define('VBO_EXECUTION_BATCH',     2);
define('VBO_EXECUTION_QUEUE',     3);

// Types of aggregate actions.
define('VBO_AGGREGATE_FORCED',    1);
define('VBO_AGGREGATE_FORBIDDEN', 0);
define('VBO_AGGREGATE_OPTIONAL',  2);

// Access operations.
define('VBO_ACCESS_OP_VIEW',      0x01);
define('VBO_ACCESS_OP_UPDATE',    0x02);
define('VBO_ACCESS_OP_CREATE',    0x04);
define('VBO_ACCESS_OP_DELETE',    0x08);

/**
 * Implementation of hook_views_api().
 */
function views_bulk_operations_views_api() {
  return array(
    'api' => 2.0,
  );
}

/**
 * Implementation of hook_elements().
 */
function views_bulk_operations_elements() {
  $type['views_node_selector'] = array(
    '#input' => TRUE,
    '#view' => NULL,
    '#process' => array('views_node_selector_process'),
  );
  return $type;
}

/**
 * Process the views_node_selector element defined earlier.
 * 
 * @see views_bulk_operations_elements()
 */
function views_node_selector_process($element, $edit) {
  $element['#tree'] = TRUE;
  if (!isset($element['#value'])) {
    $element['#value'] = array('selection' => array(), 'select_all' => FALSE);
  }

  $view = $element['#view'];
  $options = array();
  foreach ($view->result as $object) {
    $options[$object->{$view->base_field}] = '';
  }
  $element['selection']['#options'] = $options;
  $element['selection']['#value'] = $element['#value']['selection'];
  $element['selection']['#attributes'] = array();
  $element['selection'] = expand_checkboxes($element['selection']);
  
  $element['select_all'] = array('#type' => 'hidden', '#default_value' => $element['#value']['select_all']);
  return $element; 
}

/**
 * Implementation of hook_theme().
 */
function views_bulk_operations_theme() {
  $themes = array(
    'views_node_selector' => array(
      'arguments' => array('element' => NULL),
    ),
    'views_bulk_operations_confirmation' => array(
      'arguments' => array('objects' => NULL, 'view' => NULL),
    ),
  );
  $files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations'), '(.*).action.inc$');
  if ($files) foreach ($files as $file) {
    $action_theme_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($file->filename, '.inc')).'_theme';
    if (function_exists($action_theme_fn)) {
      $themes += call_user_func($action_theme_fn);
    }
  }
  return $themes;
}

/**
 * Provide the ability to select items in a view using checkboxes.
 */
function theme_views_node_selector($element) {
  require_once(drupal_get_path('module', 'views').'/theme/theme.inc');
  
  drupal_add_js('misc/tableselect.js');
  drupal_add_js(drupal_get_path('module', 'views_bulk_operations').'/views_bulk_operations.js');
  drupal_add_css(drupal_get_path('module', 'views_bulk_operations') . '/views_bulk_operations.css', 'module');

  // Don't add the vbo setting twice.
  $javascript = drupal_add_js();
  $found = FALSE;
  if (!empty($javascript['setting'])) foreach ($javascript['setting'] as $setting) {
    if (is_array($setting) && isset($setting['vbo']) && isset($setting['vbo']['url'])) {
      $found = TRUE;
      break;
    }
  }
  if (!$found) {
    drupal_add_js(array('vbo' => array('url' => url($_GET['q']))), 'setting');
  }

  $output = '';
  $view = $element['#view'];
  $sets = $element['#sets'];
  $vars = array(
    'view' => $view,
  );
  // Give each group its own headers row.
  foreach ($sets as $title => $records) {
    $headers = array();

    // template_preprocess_views_view_table() expects the raw data in 'rows'.
    $vars['rows'] = $records;
      
    // Render the view as table. Function from views/theme/theme.inc
    template_preprocess_views_view_table($vars);
    
    // Add checkboxes to the header and the rows.
    $hide_select_all = @$view->display['default']->display_options['style_options']['hide_select_all'];
    if (!$hide_select_all) {
      $headers[] = theme('table_select_header_cell');
    }
    else {
      $headers[] = array('class' => 'no_select_all');
    }
    foreach ($vars['header'] as $field => $label) {
      $headers[] = array('data' => $label, 'class' => "views-field views-field-{$vars['fields'][$field]}");
    }
    $rows = array();
    foreach ($records as $num => $object) {
      $row = array('class' => 'rowclick', 'data' => array());
      $row['data'][] =  theme('checkbox', $element['selection'][$object->{$view->base_field}]);
      foreach ($vars['rows'][$num] as $field => $content) {
        $row['data'][] = array('data' => $content, 'class' => "views-field views-field-{$vars['fields'][$field]}");
      }
      $rows[] = $row;
    }
    // Add the first row as option to select all records across all pages.
    if ($view->pager['use_pager'] && $view->total_rows > $view->pager['items_per_page']) {
      $group = count($sets) > 1 ? t('set') : t('page');
      $row = array(
        array(
          'data' => '<span id="vbo-this-page">'. t('All <strong>!objects</strong> rows in this !group are selected.', array('!objects' => count($records), '!group' => $group))
            .'&nbsp;<input type="button" id="vbo-select-all-pages" value="'. t('Select all !objects rows in this view.', array('!objects' => $view->total_rows)) .'" /></span>'
            .'<span id="vbo-all-pages" style="display: none">'. t('All <strong>!objects</strong> rows in this view are selected.', array('!objects' => $view->total_rows))
            .'&nbsp;<input type="button" id="vbo-select-this-page" value="'. t('Select only !objects rows in this !group.', array('!objects' => count($records), '!group' => $group)) .'" /></span>',
          'class' => 'view-field view-field-select-all',
          'colspan' => count($headers) + 1,
        ),
      );
      array_unshift($rows, $row);
    }

    $output .= theme('table', $headers, $rows, array('class' => $vars['class']), $title);
    $output .= theme('hidden', $element['select_all']);
  }
  return theme('form_element', $element, $output);
}

/**
 * Implementation of hook_init().
 */
function views_bulk_operations_init() {
  // Reset selection if we're not in the view anymore.
  if (!isset($_SESSION['vbo_values'][$_GET['q']])) {
    unset($_SESSION['vbo_values']);
  }

  // Automatically include the action files.
  $files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations'), '(.*).action.inc$');
  if ($files) foreach ($files as $file) {
    require_once($file->filename);
  }
}

/**
 * Define multistep form for selecting and executing an operation.
 */
function views_bulk_operations_form($form_state, $plugin) {
  // Force browser to reload the page if Back is hit.
  if (preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
    drupal_set_header("Cache-Control: no-cache"); // works for IE6+
  }
  else {
    drupal_set_header("Cache-Control: no-store"); // works for Firefox and other browsers
  }
  
  // If there's a session variable on this view, pre-load the old values.
  if (isset($_SESSION['vbo_values'][$_GET['q']])) {
    $default_objects = array('selection' => array_filter($_SESSION['vbo_values'][$_GET['q']]['objects']['selection']), 'select_all' => $_SESSION['vbo_values'][$_GET['q']]['objects']['select_all']);
    $default_operation = $_SESSION['vbo_values'][$_GET['q']]['operation'];
  }
  else {
    $default_objects = array('selection' => NULL, 'select_all' => FALSE);
    $default_operation = NULL;
  }

  if (!isset($form_state['storage']['step'])) {
    if (count($plugin->get_selected_operations()) == 1 && $plugin->options['merge_single_action']) {
      $step = VIEWS_BULK_OPS_STEP_SINGLE;
    }
    else {
      $step = VIEWS_BULK_OPS_STEP_VIEW;
    }
    $form['exposed_input'] = array(
      '#type' => 'value',
      '#value' => $plugin->view->get_exposed_input(),
    );
    $form['arguments'] = array(
      '#type' => 'value',
      '#value' => $plugin->view->args,
    );

    // If empty view, render the empty text.
    if (!$plugin->view->result) {
      $form['empty'] = array('#value' => $plugin->view->display_handler->render_empty());
      return $form;
    }
  } 
  else {
    $plugin->strip_view();
    switch ($form_state['storage']['step']) {
      case VIEWS_BULK_OPS_STEP_VIEW:
        $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
        if ($operation['configurable']) {
          $step = VIEWS_BULK_OPS_STEP_CONFIG;
        }
        else {
          $step = VIEWS_BULK_OPS_STEP_CONFIRM;
        }
        break;
      case VIEWS_BULK_OPS_STEP_SINGLE:
      case VIEWS_BULK_OPS_STEP_CONFIG:
        $step = VIEWS_BULK_OPS_STEP_CONFIRM;
        break;
      }
  }
  $form['step'] = array(
    '#type' => 'value',
    '#value' => $step
  );
  $form['plugin'] = array(
    '#type' => 'value',
    '#value' => $plugin
  );
  
  switch ($step) {
    case VIEWS_BULK_OPS_STEP_VIEW:
      $form['select'] = array(
        '#type' => 'fieldset',
        '#title' => t('Bulk operations'),
        '#prefix' => '<div id="views-bulk-operations-select">',
        '#suffix' => '</div>',             
      );
      $form['objects'] = array(
        '#type' => 'views_node_selector',
        '#view' => $plugin->view,
        '#sets' => $plugin->sets,
        '#value' => $default_objects,
        '#prefix' => '<div class="views-node-selector">',
        '#suffix' => '</div>',
      );
      if ($plugin->options['display_type'] == 0) {
        // Create dropdown and submit button.
        $form['select']['operation'] = array(
          '#type' => 'select',
          '#options' => array(0 => t('- Choose an operation -')) + $plugin->get_selected_operations(),
          '#default_value' => $default_operation,
          '#prefix' => '<div id="views-bulk-operations-dropdown">',
          '#suffix' => '</div>',             
        );
        $form['select']['submit'] = array(
          '#type' => 'submit',
          '#value' => t('Execute'),
          '#prefix' => '<div id="views-bulk-operations-submit">',
          '#suffix' => '</div>',             
        );
      }
      else {
        // Create buttons for actions.
        foreach ($plugin->get_selected_operations() as $md5 => $description) {
          $form['select'][$md5] = array(
            '#type' => 'submit',
            '#value' => $description,
            '#hash' => $md5,
          );
        }
      }
      break;
  
    case VIEWS_BULK_OPS_STEP_SINGLE:
      $ops = array_keys($plugin->get_selected_operations());
      $operation = $plugin->get_operation_info($ops[0]);
      $form['operation'] = array('#type' => 'value', '#value' => $ops[0]);
      if ($operation['configurable']) {
        $dummy_selection = array();
        foreach ($plugin->view->result as $result) {
          $dummy_selection[$result->{$plugin->view->base_field}] = $result;
        }
        $form += _views_bulk_operations_action_form($operation, $plugin->view, $dummy_selection, $plugin->get_operation_settings($operation));
      }
      $form['objects'] = array(
        '#type' => 'views_node_selector',
        '#view' => $plugin->view,
        '#sets' => $plugin->sets,
        '#value' => $default_objects,
        '#prefix' => '<div class="views-node-selector">',
        '#suffix' => '</div>',
      );
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => $operation['label'],
        '#prefix' => '<div id="views-bulk-operations-submit">',
        '#suffix' => '</div>',             
      );
      break;
    
    case VIEWS_BULK_OPS_STEP_CONFIG:
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      $form['operation'] = array('#type' => 'value', '#value' => $operation);
      $form += _views_bulk_operations_action_form(
        $operation, 
        $plugin->view, 
        array_filter($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['selection']), 
        $plugin->get_operation_settings($operation)
      );
      $form['execute'] = array(
        '#type' => 'submit',
        '#value' => t('Next'),
        '#weight' => 98,
      );
      $query = drupal_query_string_encode($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['exposed_input']);
      $form['cancel'] = array(
        '#type' => 'markup',
        '#value' => t('<a href="@view">Cancel</a>', array('@view' => url($_GET['q'], array('query' => $query)))),
        '#weight' => 99,
      );
      drupal_set_title(t('Set parameters for \'%action\'', array('%action' => $operation['label'])));
      break;
    
    case VIEWS_BULK_OPS_STEP_CONFIRM:
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      $query = drupal_query_string_encode($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['exposed_input']);
      $objects = $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['selection'];
      $form = confirm_form($form,
        t('Are you sure you want to perform \'%action\' on selected rows?', array('%action' => $operation['label'])),
        array('path' => $_GET['q'], 'query' => $query),
        theme('views_bulk_operations_confirmation', $objects, $plugin->view)
      ); 
      break;
  }
 
  // Use views_bulk_operations_form_submit() for form submit, regardless of form_id.
  $form['#submit'][] = 'views_bulk_operations_form_submit';
  $form['#validate'][] = 'views_bulk_operations_form_validate';
  $form['#cache'] = TRUE;
  return $form;
}

/**
 * Implementation of hook_form_alter().
 *
 * Usability improvements to standard "Send e-mail" action.
 */
function views_bulk_operations_form_alter(&$form, $form_state, $form_id) {
  if (strpos($form_id, 'views_bulk_operations_form') === 0) {
    if (isset($form['operation']) && ($form['operation']['#value']['callback'] == 'system_send_email_action' || $form['operation']['#value'] == 'system_send_email_action')) {
      // Hide recipient field, as we have already chosen it from user list.
      $form['recipient']['#default_value'] = '%author';
      $form['recipient']['#type'] = 'hidden';
    }
  }
}

/**
 * Validate the selected operation.
 * 
 * @see views_bulk_operations_form()
 */
function views_bulk_operations_form_validate($form, &$form_state) {
  switch ($form_state['values']['step']) {
    case VIEWS_BULK_OPS_STEP_VIEW:
      $_SESSION['vbo_values'][$_GET['q']] = $form_state['values'];

      if (!array_filter($form_state['values']['objects']['selection'])) { // If all 0, no row selected
        form_set_error('objects', t('No row selected. Please select one or more rows.'));
      }
      if (!empty($form_state['clicked_button']['#hash'])) {
        $form_state['values']['operation'] = $form_state['clicked_button']['#hash'];
      }
      if (!$form_state['values']['operation']) { // No action selected
        form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
      }
      break;

    case VIEWS_BULK_OPS_STEP_SINGLE:
      $_SESSION['vbo_values'][$_GET['q']] = $form_state['values'];

      if (!array_filter($form_state['values']['objects']['selection'])) { // If all 0, no row selected
        form_set_error('objects', t('No row selected. Please select one or more rows.'));
      }
      $plugin = $form_state['values']['plugin'];
      $operation = $plugin->get_operation_info($form_state['values']['operation']);
      if ($operation['configurable']) {
        _views_bulk_operations_action_validate($operation, $form, $form_state);
      }
      break;

    case VIEWS_BULK_OPS_STEP_CONFIG:
      $plugin = $form_state['values']['plugin'];
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      _views_bulk_operations_action_validate($operation, $form, $form_state);
      break;
  }
}

/**
 * Helper function to adjust the selected set of nodes based on different situations.
 */ 
function _views_bulk_operations_adjust_selection(&$selection, $select_all, $exposed_input, $arguments, $plugin) {
  if ($select_all) {
    // Adjust selection to select all nodes across pages.
    $view = views_get_view($plugin->view->vid ? $plugin->view->vid : $plugin->view->name);
    $view->set_items_per_page(0);
    $view->set_exposed_input($exposed_input);
    $view->set_arguments($arguments);

    // HACK for date_api_filter_handler: set $_GET with the exposed_input.
    $_GET += $exposed_input;

    $view->execute($plugin->view->current_display);
    $results = array();
    foreach ($view->result as $result) {
      $results[$result->{$view->base_field}] = $result;
    }
    $selection = $results;
  }
  else {
    // Adjust selection to filter out previous selections.
    $results = array();
    foreach ($plugin->view->result as $result) {
      if ($selection[$result->{$plugin->view->base_field}]) {
        $results[$result->{$plugin->view->base_field}] = $result;
      }
    }
    $selection = $results;
  }
    
  // Adjust sticky selection accordingly.
  $_SESSION['vbo_values'][$_GET['q']]['objects'] = array('selection' => $selection, 'select_all' => $select_all);
}

/**
 * Submit handler for the selected operation.
 * 
 * @see views_bulk_operations_form()
 */
function views_bulk_operations_form_submit($form, &$form_state) {
  $plugin = $form_state['values']['plugin'];
  
  switch ($form_state['values']['step']) {
    case VIEWS_BULK_OPS_STEP_VIEW:
      $form_state['storage']['step'] = $form_state['values']['step'];
      $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW] = $form_state['values'];
      _views_bulk_operations_adjust_selection(
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['selection'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['select_all'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['exposed_input'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['arguments'], 
        $plugin
      );
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      if (!$operation['configurable'] && $plugin->options['skip_confirmation']) {
        break; // Go directly to execution
      }
      return;

    case VIEWS_BULK_OPS_STEP_SINGLE:
      $form_state['storage']['step'] = $form_state['values']['step'];
      $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW] = $form_state['values'];
      $form_state['storage'][VIEWS_BULK_OPS_STEP_CONFIG] = $form_state['values']; // we're not taking any chances
      _views_bulk_operations_adjust_selection(
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['selection'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['select_all'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['exposed_input'], 
        $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['arguments'], 
        $plugin
      );
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      if ($operation['configurable']) {
        $form_state['storage']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
      }
      if ($plugin->options['skip_confirmation']) {
        break; // Go directly to execution
      }
      return;

    case VIEWS_BULK_OPS_STEP_CONFIG:
      $form_state['storage']['step'] = $form_state['values']['step'];
      $form_state['storage'][VIEWS_BULK_OPS_STEP_CONFIG] = $form_state['values'];
      $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
      $form_state['storage']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
      if ($plugin->options['skip_confirmation']) {
        break; // Go directly to execution
      }
      return; 

    case VIEWS_BULK_OPS_STEP_CONFIRM:
      break;
  }

  // Clean up unneeded SESSION variables.
  unset($_SESSION['vbo_values'][$_GET['q']]);

  // Execute the VBO.
  $operation = $plugin->get_operation_info($form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['operation']);
  $operation_arguments = array();
  if ($operation['configurable']) {
    $form_state['values'] += $form_state['storage'][VIEWS_BULK_OPS_STEP_CONFIG];
    $operation_arguments = $form_state['storage']['operation_arguments'];
  }
  _views_bulk_operations_execute(
    $plugin->view, 
    $form_state['storage'][VIEWS_BULK_OPS_STEP_VIEW]['objects']['selection'], 
    $operation, 
    $operation_arguments,
    array('execution_type' => $plugin->options['execution_type'], 'display_result' => $plugin->options['display_result'])
  );

  // Clean up the form.
  unset($form_state['storage']);
  $form_state['redirect'] = isset($form_state['values']['exposed_input']['destination']) ? $form_state['values']['exposed_input']['destination'] : $_GET['q'];
}

/**
 * Helper function to execute the chosen action upon selected objects.
 */
function _views_bulk_operations_execute($view, $objects, $operation, $operation_arguments, $options) {
  // Add action arguments.
  $params = array();
  if ($operation['configurable'] && is_array($operation_arguments)) {
    $params += $operation_arguments;
  }
  // Add static callback arguments. Note that in the case of actions, static arguments
  // are picked up from the database in actions_do().
  if (isset($operation['callback arguments'])) {
    $params += $operation['callback arguments'];
  }
  // Add this view as parameter.
  $params['view'] = $view;

  if (version_compare(VERSION, '6.10', '<')) {
    // Hack to force actions_do() to process any number of invocations. 
    // Check http://drupal.org/node/290282 to understand more.
    // This was fixed as of D6.10: http://cvs.drupal.org/viewvc.py/drupal/drupal/includes/actions.inc?view=log&pathrev=DRUPAL-6-10
    variable_set('actions_max_stack', 10000000);
  }
  if ($operation['aggregate'] != VBO_AGGREGATE_FORCED && $options['execution_type'] == VBO_EXECUTION_BATCH) {
    $operations = array();
    foreach ($objects as $oid => $row) {
      $operations[] = array('_views_bulk_operations_batch_process', array($oid, $row));
    }

    // Save the options in the session because Batch API doesn't give a way to 
    // send a parameter to the finished callback.
    $_SESSION['vbo_options']['display_result'] = $options['display_result'];
    $_SESSION['vbo_options']['operation'] = $operation;
    $_SESSION['vbo_options']['params'] = serialize($params);

    $batch = array(
      'operations' => $operations,
      'finished' => '_views_bulk_operations_batch_finished',
      'title' => t('Performing %action on selected rows...', array('%action' => $operation['label'])),
    );  
    batch_set($batch);
  }
  else if ($operation['aggregate'] != VBO_AGGREGATE_FORCED && module_exists('job_queue') && $options['execution_type'] == VBO_EXECUTION_QUEUE) {
    global $user;
    unset($params['view']);
    foreach ($objects as $oid => $row) {
      job_queue_add('_views_bulk_operations_queue_process', 
        t('Perform %action on @type %oid.', array(
          '%action' => $operation['label'],
          '@type' => t($operation['type']),
          '%oid' => $oid
        )), 
        array($oid, $row, $operation, $params, $user->uid, $options['display_result'])
      );
    }
    if ($options['display_result']) {
      drupal_set_message(t('Enqueued %action on @types %oid. Check the <a href="@queue">queued jobs page</a>.', array(
        '%action' => $operation['label'], 
        '@types' => format_plural(count($objects), t($operation['type']), t($operation['type'] .'s')),
        '%oid' => implode(', ', array_keys($objects)), 
        '@queue' => url('admin/reports/job_queue')
      )));
    }
  }
  else /*if ($options['execution_type'] == VBO_EXECUTION_DIRECT)*/ {
    @set_time_limit(0);
    
    $context['results']['rows'] = 0;
    $context['results']['time'] = microtime(TRUE);

    _views_bulk_operations_direct_process($operation, $objects, $params, $context);
    _views_bulk_operations_direct_finished(TRUE, $context['results'], array(), $options['display_result']);
  }
}

/**
 * Helper function to handle Job queue operations.
 */
function _views_bulk_operations_queue_process($oid, $row, $operation, $params, $uid, $display_result) {
  views_include('view'); // Force include of view.inc before we unserialize the parameters to make sure view object can be restored.
  module_load_include('inc', 'node', 'node.admin');

  $object_info = _views_bulk_operations_object_info_for_type($operation['type']);
  if (!$object_info) return;
  $object = call_user_func($object_info['load'], $oid);
  $account = user_load(array('uid' => $uid));
  if (!_views_bulk_operations_object_permission($operation, $object, $account)) {
    watchdog('views bulk operations', 'Skipped %action on @type %title due to insufficient permissions.', array(
      '%action' => $operation['label'], 
      '@type' => t($operation['type']),
      '%title' => $object->{$object_info['title']},
    ), WATCHDOG_ALERT);
    return;
  }

  _views_bulk_operations_action_do($operation, $oid, $object, $row, $params, $account);

  if ($display_result) {
    watchdog('views bulk operations', 'Performed %action on @type %title.', array(
      '%action' => $operation['label'], 
      '@type' => t($operation['type']),
      '%title' => $object->{$object_info['title']},
    ), WATCHDOG_INFO);
  }
}

/**
 * Helper function to handle Batch API operations.
 */
function _views_bulk_operations_batch_process($oid, $row, &$context) {
  views_include('view'); // Force include of view.inc before we unserialize the parameters to make sure view object can be restored.
  module_load_include('inc', 'node', 'node.admin');

  $operation = $_SESSION['vbo_options']['operation'];
  $params = unserialize($_SESSION['vbo_options']['params']);

  if (!isset($context['results']['time'])) {
    $context['results']['time'] = microtime(TRUE);
  }

  $object_info = _views_bulk_operations_object_info_for_type($operation['type']);
  if (!$object_info) return;
  $object = call_user_func($object_info['load'], $oid);
  if (!_views_bulk_operations_object_permission($operation, $object)) {
    $context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
      '%action' => $operation['label'], 
      '@type' => t($operation['type']),
      '%title' => $object->{$object_info['title']},
    ));
    return;
  }

  _views_bulk_operations_action_do($operation, $oid, $object, $row, $params);

  $context['results']['log'][] = t('Performed %action on @type %title.', array(
    '%action' => $operation['label'], 
    '@type' => t($operation['type']),
    '%title' => $object->{$object_info['title']},
  ));
  if (isset($context['results']['rows'])) {
    $context['results']['rows'] += 1;
  }
  else {
    $context['results']['rows'] = 1;
  }
}

/**
 * Helper function to cleanup Batch API operations.
 */
function _views_bulk_operations_batch_finished($success, $results, $operations, $display_result = NULL) {
  if ($success) {
    if ($results['rows'] > 0) {
      $message = t('!results rows processed in about !time ms:', array('!results' => $results['rows'], '!time' => round((microtime(TRUE) - $results['time']) * 1000)));
    }
    else {
      $message = t('No rows were processed:');
    }
    $message .= "\n". theme('item_list', $results['log']);
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = t('An error occurred while processing @operation with arguments: @arguments', 
      array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
  }
  if (version_compare(VERSION, '6.10', '<')) {
    // See http://cvs.drupal.org/viewvc.py/drupal/drupal/includes/actions.inc?view=log&pathrev=DRUPAL-6-10
    variable_set('actions_max_stack', 35);
  }
  if ($display_result || @$_SESSION['vbo_options']['display_result']) {
    _views_bulk_operations_log($message);
  }
  unset($_SESSION['vbo_options']); // unset the options which were used for just one invocation
}

/**
 * Helper function for direct execution operations.
 */
function _views_bulk_operations_direct_process($operation, $objects, $params, &$context) {
  $object_info = _views_bulk_operations_object_info_for_type($operation['type']);
  if (!$object_info) return;

  if ($operation['aggregate'] != VBO_AGGREGATE_FORBIDDEN) {
    if (isset($object_info['access'])) {
      foreach ($objects as $oid => $row) {
        $object = call_user_func($object_info['load'], $oid);
        if (!_views_bulk_operations_object_permission($operation, $object)) {
          unset($objects[$oid]);
          $context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
            '%action' => $operation['label'], 
            '@type' => t($operation['type']),
            '%title' => $object->{$object_info['title']},
          ));
        }
      }
    }

    if (!empty($objects)) {
      _views_bulk_operations_action_aggregate_do($operation, $objects, $params);

      $context['results']['log'][] = t('Performed aggregate %action on @types %oids.', array(
        '%action' => $operation['label'], 
        '@types' => format_plural(count($objects), t($operation['type']), t($operation['type']  .'s')),
        '%oids' => implode(',', array_keys($objects)),
      ));
      $context['results']['rows'] += count($objects);
    }
  }
  else foreach ($objects as $oid => $row) {
    $object = call_user_func($object_info['load'], $oid);
    if (!_views_bulk_operations_object_permission($operation, $object)) {
      $context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
        '%action' => $operation['label'], 
        '@type' => t($operation['type']),
        '%title' => $object->{$object_info['title']},
      ));
      continue;
    }
   
    _views_bulk_operations_action_do($operation, $oid, $object, $row, $params);
    
    $context['results']['log'][] = t('Performed %action on @type %title.', array(
      '%action' => $operation['label'], 
      '@type' => t($operation['type']),
      '%title' => $object->{$object_info['title']},
    ));
    $context['results']['rows'] += 1;
  }
}

/**
 * Helper function to cleanup direct execution operations.
 */
function _views_bulk_operations_direct_finished($success, $results, $operations, $display_result) {
  _views_bulk_operations_batch_finished($success, $results, $operations, $display_result);
}

/**
 * Helper function to execute one operation.
 */
function _views_bulk_operations_action_do($operation, $oid, $object, $row, $params, $account = NULL) {
  _views_bulk_operations_action_permission($operation, $account);

  $params[$operation['type']] = $object; // Add the object to the context for token support
  $params['row'] = $row; // Expose the original view row to the action

  if ($operation['source'] == 'action') {
    actions_do($operation['callback'], $object, $params);
    if ($operation['type'] == 'node' && ($operation['access op'] & VBO_ACCESS_OP_UPDATE)) { // Save nodes explicitly if needed
      $node_options = variable_get('node_options_'. $object->type, array('status', 'promote'));
      $object->revision = in_array('revision', $node_options);
      node_save($object);
    }
  }
  else { // source == 'operation'
    $args = array_merge(array(array($oid)), $params);
    call_user_func_array($operation['callback'], $args);
  }
}

/**
 * Helper function to execute an aggregate operation.
 */
function _views_bulk_operations_action_aggregate_do($operation, $objects, $params) {
  _views_bulk_operations_action_permission($operation);

  $params['rows'] = $objects;
  $args = array_merge(array(array_keys($objects)), $params);
  call_user_func_array($operation['callback'], $args);
}

/**
 * Helper function to verify access permission to execute operation.
 */
function _views_bulk_operations_action_permission($operation, $account = NULL) {
  if (module_exists('actions_permissions')) {
    $perm = actions_permissions_get_perm($operation['label'], $operation['callback']);
    if (!user_access($perm, $account)) {
      global $user;
      watchdog('actions permissions', 'An attempt by user %user to !perm was blocked due to insufficient permissions.', 
        array('!perm' => $perm, '%user' => isset($account) ? $account->name : $user->name), WATCHDOG_ALERT);
      drupal_access_denied(); 
    }
  }

  // Check against additional permissions.
  if (!empty($operation['permissions'])) foreach ($operation['permissions'] as $perm) {
    if (!user_access($perm, $account)) {
      global $user;
      watchdog('actions permissions', 'An attempt by user %user to !perm was blocked due to insufficient permissions.', 
        array('!perm' => $perm, '%user' => isset($account) ? $account->name : $user->name), WATCHDOG_ALERT);
      drupal_access_denied(); 
    }
  }
}

/**
 * Helper function to verify access permission to operate on object.
 */
function _views_bulk_operations_object_permission($operation, $object, $account = NULL) {
  // Check against object access permissions.
  $object_info = _views_bulk_operations_object_info_for_type($operation['type']);
  if (!isset($object_info['access'])) return TRUE;

  $access_ops = array(
    VBO_ACCESS_OP_VIEW => 'view',
    VBO_ACCESS_OP_UPDATE => 'update',
    VBO_ACCESS_OP_CREATE => 'create',
    VBO_ACCESS_OP_DELETE => 'delete',
  );
  foreach ($access_ops as $bit => $op) {
    if ($operation['access op'] & $bit) {
      if (!call_user_func($object_info['access'], $op, $object, $account)) {
        return FALSE;
      }
    }
  }

  return TRUE;
}

/**
 * Helper function to let the configurable action provide its configuration form.
 */
function _views_bulk_operations_action_form($action, $view, $selection, $settings) {
  $action_form = $action['callback'].'_form';
  return call_user_func($action_form, array('view' => $view, 'selection' => $selection, 'settings' => $settings));
}

/**
 * Helper function to let the configurable action validate the form if it provides a validator.
 */
function _views_bulk_operations_action_validate($action, $form, $form_values) {
  $action_validate = $action['callback'].'_validate';
  if (function_exists($action_validate)) {
    call_user_func($action_validate, $form, $form_values);
  }
}

/**
 * Helper function to let the configurable action process the configuration form.
 */
function _views_bulk_operations_action_submit($action, $form, $form_values) {
  $action_submit = $action['callback'].'_submit';
  return call_user_func($action_submit, $form, $form_values);
}

/**
 * Theme function to show the confirmation page before executing the action.
 */
function theme_views_bulk_operations_confirmation($objects, $view) {
  $object_info = _views_bulk_operations_object_info_for_view($view);
  $items = array();
  foreach ($objects as $oid => $row) {
    if ($view->pager_original['use_pager'] && count($items) >= $view->pager_original['items_per_page']) {
      $items[] = t('...and <strong>!count</strong> more.', array('!count' => count($objects) - count($items)));
      break;
    }
    if ($object = call_user_func($object_info['load'], $oid)) {
      $items[] = check_plain((string)$object->{$object_info['title']});
    }
  }
  $output = theme('item_list', $items, t('You selected the following <strong>!count</strong> rows:', array('!count' => count($objects))));
  return $output;
}

/**
 * Implementation of hook_forms().
 * 
 * Force each instance of function to use the same callback.
 */
function views_bulk_operations_forms() {
  // Get the form ID.
  $args = func_get_args();
  $form_id = $args[0];
  // Ensure we map a callback for our form and not something else.
  $forms = array();
  if (strpos($form_id, 'views_bulk_operations_form') === 0) {
    // Let the forms API know where to get the form data corresponding
    // to this form id.
    $forms[$form_id] = array('callback' => 'views_bulk_operations_form');
  }
  return $forms;
}

/**
 * Implementation of hook_views_bulk_operations_object_info()
 *
 * Hook used by VBO to be able to handle different objects as does Views 2.
 *
 * The array returned for each object type contains:
 *  'type' => the object type name, should be the same as 'type' field in actions. 
 *  'base_table' => the Views 2 table name corresponding to that object type, should be the same as the $view->base_table attribute.
 *  'load' => a function($id) that returns the corresponding object.
 *  'title' => an attribute on the object that returns a human-friendly identifier of the object.
 *  'access' (optional) => a function($op, $node, $account = NULL) that behaves like node_access().
 *
 */
function views_bulk_operations_views_bulk_operations_object_info() {
  return array(
    'node' => array(
      'type' => 'node',
      'base_table' => 'node',
      'load' => '_views_bulk_operations_node_load',
      'title' => 'title',
      'access' => 'node_access',
    ),
    'user' => array(
      'type' => 'user',
      'base_table' => 'users',
      'load' => 'user_load',
      'title' => 'name',
    ),
    'comment' => array(
      'type' => 'comment',
      'base_table' => 'comments',
      'load' => '_comment_load',
      'title' => 'subject',
    ),
    'term' => array(
      'type' => 'term',
      'base_table' => 'term_data',
      'load' => 'taxonomy_get_term',
      'title' => 'name',
    ),
  );
}

/**
 * Load function for objects of type 'node'.
 */
function _views_bulk_operations_node_load($nid) {
  return node_load($nid, NULL, TRUE);
}

/**
 * Helper function to return all object info.
 */
function _views_bulk_operations_get_object_info($reset = FALSE) {
  static $object_info = array();
  if ($reset || empty($object_info)) {
    $object_info = module_invoke_all('views_bulk_operations_object_info');
  }
  return $object_info;
}

/**
 * Helper function to return object info for a given view.
 */
function _views_bulk_operations_object_info_for_view($view) {
  foreach (_views_bulk_operations_get_object_info() as $object_info) {
    if ($object_info['base_table'] == $view->base_table) {
      return $object_info;
    }
  }
  watchdog('views bulk operations', 'Could not find object info for view table @table.', array('@table' => $view->base_table), WATCHDOG_ERROR);
  return NULL;
}

/**
 * Helper function to return object info for a given type ('node', 'user', etc.)
 */
function _views_bulk_operations_object_info_for_type($type) {
  $object_info = _views_bulk_operations_get_object_info();
  if (!isset($object_info[$type])) {
    watchdog('views bulk operations', 'Could not find object info for type @type.', array('@type' => $type), WATCHDOG_ERROR);
    return NULL;
  }
  return $object_info[$type];
}

/**
 * Implementation of hook_action_info().
 */
function views_bulk_operations_action_info() {
  $actions = array();
  $files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations'), '(.*).action.inc$');
  if ($files) foreach ($files as $file) {
    require_once($file->filename);
    $action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($file->filename, '.inc')).'_info';
    $action_info = call_user_func($action_info_fn);
    if (is_array($action_info)) $actions += $action_info;
  }

  // Add VBO's own programmatic action.
  $actions['views_bulk_operations_action'] = array(
    'description' => t('Execute a VBO programmatically'),
    'type' => 'system',
    'configurable' => TRUE,
    'behavior' => array('changes node property'),
  );

  return $actions;
}

/**
 * Form function for views_bulk_operations_action action.
 */
function views_bulk_operations_action_form($context) {
  // Some views choke on being rebuilt at this moment because of validation errors in the action form.
  // So we save the error state, reset it, build the views, then reinstate the errors.
  // Also unset the error messages because they'll be displayed again after the loop.
  $errors = form_get_errors();
  if (!empty($errors)) foreach ($errors as $message) {
    unset($_SESSION['messages']['error'][array_search($message, $_SESSION['messages']['error'])]);
  }
  form_set_error(NULL, '', TRUE);

  // Look for all views with VBO styles, and for each find the operations they use.
  $views[0] = t('- Choose a view -');
  $operations[0] = t('- Choose an operation -');
  foreach (views_get_all_views() as $name => $view) {
    foreach (array_keys($view->display) as $display) {
      $display_options = &$view->display[$display]->display_options;
      if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') {
        $vid = empty($view->vid) ? $view->name : $view->vid;
        $view->build($display);
        $views[$vid] = $view->name . (!empty($view->description) ? ': ' . $view->description : '');
        foreach (array_filter($display_options['style_options']['selected_operations']) as $operation) {
          $views_operations[$vid][$operation] = $view->style_plugin->options['all_operations'][$operation]['label'];
          $operations[$operation] = $view->style_plugin->options['all_operations'][$operation]['label'];
        }
      }
    }
  }
  
  if (!empty($errors)) foreach ($errors as $name => $message) {
    form_set_error($name, $message);
  }

  drupal_add_js(array('vbo' => array('action' => array('views_operations' => $views_operations))), 'setting');
  drupal_add_js(drupal_get_path('module', 'views_bulk_operations') .'/views_bulk_operations.action.js');

  $form['view_vid'] = array(
    '#type' => 'select',
    '#title' => t('View'),
    '#description' => t('Select the VBO to be executed.'),
    '#options' => $views,
    '#default_value' => @$context['view_vid'],
    '#attributes' => array('onchange' => 'Drupal.vbo.action.updateOperations(this.options[this.selectedIndex].value);'),
  );
  $form['operation_callback'] = array(
    '#type' => 'select',
    '#title' => t('Operation'),
    '#description' => t('Select the operation to be executed.'),
    '#options' => $operations,
    '#default_value' => @$context['operation_callback'],
  );
  $form['operation_arguments'] = array(
    '#type' => 'textarea',
    '#title' => t('Operation arguments'),
    '#description' => t('Enter PHP script that will assemble the operation arguments (in the case of configurable actions).
                         These arguments should be of the form: <code>return array(\'argument1\' => \'value1\', ...);</code> 
                         and they should correspond to the values returned by the action\'s form submit function.
                         The variables <code>&$object</code> and <code>$context</code> are available to this script.
                        '),
    '#default_value' => @$context['operation_arguments'],
  );
  $form['view_exposed_input'] = array(
    '#type' => 'textarea',
    '#title' => t('View exposed input'),
    '#description' => t('Enter PHP script that will assemble the view exposed input (if the view accepts exposed input).
                         These input should be of the form: <code>return array(\'input1\' => \'value1\', ...);</code> 
                         and they should correspond to the query values used on the view URL when exposed filters are applied.
                         The variables <code>&$object</code> and <code>$context</code> are available to this script.
                        '),
    '#default_value' => @$context['view_exposed_input'],
  );
  $form['view_arguments'] = array(
    '#type' => 'textarea',
    '#title' => t('View arguments'),
    '#description' => t('Enter PHP script that will assemble the view arguments (if the view accepts arguments).
                         These input should be of the form: <code>return array(\'value1\', ...);</code> 
                         and they should correspond to the arguments defined in the view.
                         The variables <code>&$object</code> and <code>$context</code> are available to this script.
                        '),
    '#default_value' => @$context['view_arguments'],
  );
  return $form;
}

/**
 * Form validate function for views_bulk_operations_action action.
 */
function views_bulk_operations_action_validate($form, $form_state) {
  if (empty($form_state['values']['view_vid'])) {
    form_set_error('view_vid', t('You must choose a view to be executed.'));
  }
  if (empty($form_state['values']['operation_callback'])) {
    form_set_error('operation_callback', t('You must choose an operation to be executed.'));
  }
}

/**
 * Form submit function for views_bulk_operations_action action.
 */
function views_bulk_operations_action_submit($form, $form_state) {
  return array(
    'view_vid' => $form_state['values']['view_vid'],
    'operation_callback' => $form_state['values']['operation_callback'],
    'operation_arguments' => $form_state['values']['operation_arguments'],
    'view_exposed_input' => $form_state['values']['view_exposed_input'],
    'view_arguments' => $form_state['values']['view_arguments'],
  );
}

/**
 * Execution function for views_bulk_operations_action action.
 */
function views_bulk_operations_action(&$object, $context) {
  $operation_arguments = array();
  if (!empty($context['operation_arguments'])) {
    $operation_arguments = eval($context['operation_arguments']);
  }
  $view_exposed_input = array();
  if (!empty($context['view_exposed_input'])) {
    $view_exposed_input = eval($context['view_exposed_input']);
  }
  $view_arguments = array();
  if (!empty($context['view_arguments'])) {
    $view_arguments = eval($context['view_arguments']);
  }
  views_bulk_operations_execute($context['view_vid'], $context['operation_callback'], $operation_arguments, $view_exposed_input, $view_arguments);
}

/**
 * API function to programmatically invoke a VBO.
 */
function views_bulk_operations_execute($vid, $operation_callback, $operation_arguments = array(), $view_exposed_input = array(), $view_arguments = array()) {
  $view = views_get_view($vid);
  if (!is_object($view)) {
    _views_bulk_operations_report_error('Could not find view %vid.', array('%vid' => $vid));
    return;
  }

  // Find the view display that has the VBO style.
  $found = FALSE;
  foreach (array_keys($view->display) as $display) {
    $display_options = &$view->display[$display]->display_options;
    if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') {
      $view->set_display($display);
      $found = TRUE;
      break;
    }
  }
  if (!$found) {
    _views_bulk_operations_report_error('Could not find a VBO display in view %vid.', array('%vid' => $vid));
    return;
  }

  // Execute the view.
  $view->set_exposed_input($view_exposed_input);
  $view->set_arguments($view_arguments);
  $view->set_items_per_page(0);
  $view->execute();

  // Find the selected operation.
  $plugin = $view->style_plugin;
  $operations = $plugin->get_selected_operations();
  if (!isset($operations[$operation_callback])) {
    _views_bulk_operations_report_error('Could not find operation %operation in view %vid.', array('%operation' => $operation_callback, '%vid' => $vid));
    return;
  }
  $operation = $plugin->get_operation_info($operation_callback);
  foreach ($view->result as $result) {
    $objects[$result->{$view->base_field}] = $result->{$view->base_field};
  }

  // Execute the operation on the view results.
  $execution_type = $plugin->options['execution_type'];
  if ($execution_type == VBO_EXECUTION_BATCH) {
    $execution_type = VBO_EXECUTION_DIRECT; // we don't yet support Batch API here
  }
  $display_result = $plugin->options['display_result'];
  _views_bulk_operations_execute(
    $view, 
    $objects, 
    $operation, 
    $operation_arguments,
    array('execution_type' => $execution_type, 'display_result' => $display_result)
  );
}

/**
 * Helper function to report an error.
 */
function _views_bulk_operations_report_error($msg, $arg) {
  watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
  if (function_exists('drush_set_error')) {
    drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
  }
}

/**
 * Helper function to log an information.
 */
function _views_bulk_operations_log($msg) {
  if (function_exists('drush_log')) {
    drush_log(strip_tags($msg), 'ok');
  }
  else {
    drupal_set_message($msg);
  }
}

/**
 * API function to add actions to a VBO.
 */
function views_bulk_operations_add_actions($vid, $actions) {
  $view = views_get_view($vid);
  if (!is_object($view)) {
    _views_bulk_operations_report_error('Could not find view %vid.', array('%vid' => $vid));
    return;
  }

  // Find the view display that has the VBO style.
  $found = FALSE;
  foreach (array_keys($view->display) as $display) {
    $display_options = &$view->display[$display]->display_options;
    if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') {
      $found = TRUE;
      break;
    }
  }
  if (!$found) {
    _views_bulk_operations_report_error('Could not find a VBO display in view %vid.', array('%vid' => $vid));
    return;
  }

  // Iterate on the desired actions.
  $selected_operations = $display_options['style_options']['selected_operations'];
  $ignored = array();
  if (!empty($actions)) foreach ($actions as $action) {
    $modified = FALSE;
    if (is_numeric($action)) { // aid
      $action_object = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action));
      if (is_object($action_object)) {
        $parameters = unserialize($action_object->parameters);
        $key = $action_object->callback . (empty($parameters) ? '' : ':'. md5($action_object->parameters));
        if (isset($selected_operations[$key])) { // available for this view
          $display_options['style_options']['selected_operations'][$key] = $key;
          $modified = TRUE;
        }
      }
    }
    else { // callback or title
      if (isset($selected_operations[$action])) { // callback and available for this view
        $display_options['style_options']['selected_operations'][$action] = $action;
        $modified = TRUE;
      }
      else { // try the title
        $action_object = db_fetch_object(db_query("SELECT * FROM {actions} WHERE UPPER(description) = '%s'", strtoupper(db_escape_string($action))));
        if (is_object($action_object)) {
          $parameters = unserialize($action_object->parameters);
          $key = $action_object->callback . (empty($parameters) ? '' : ':'. md5($action_object->parameters));
          if (isset($selected_operations[$key])) { // available for this view
            $display_options['style_options']['selected_operations'][$key] = $key;
            $modified = TRUE;
          }
        }
      }
    }
    if (!$modified) {
      $ignored[] = $action;
    }
  }

  // Save the view if anything was changed.
  if (count($actions) > count($ignored)) {
    $view->save();
    views_object_cache_clear('view', $vid);

    $msg = t('View %vid was successfully modified.', array('%vid' => $vid));
    if (!empty($ignored)) {
      $msg .= ' ' . t('The following actions were ignored: %ignored.', array('%ignored' => implode(', ', $ignored)));
    }
  }
  else {
    $msg = t('View %vid was not modified, because all actions were ignored.', array('%vid' => $vid));
  }
  _views_bulk_operations_log($msg);
}

