Skip to content
views_bulk_operations.module 41 KiB
Newer Older
Karim Ratib's avatar
Karim Ratib committed
<?php

Karim Ratib's avatar
Karim Ratib committed
/**
Karim Ratib's avatar
Karim Ratib committed
 * Allows operations to be performed on items selected in a view.
 */

// 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);
Karim Ratib's avatar
Karim Ratib committed

/**
 * Implementation of hook_cron_queue_info().
 */
function views_bulk_operations_cron_queue_info() {
  return array(
    'views_bulk_operations' => array(
      'worker callback' => '_views_bulk_operations_queue_process',
      'time' => 30,
    ),
  );
}

Karim Ratib's avatar
Karim Ratib committed
/**
 * Implements hook_views_api().
Karim Ratib's avatar
Karim Ratib committed
 */
function views_bulk_operations_views_api() {
  return array(
    'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
Karim Ratib's avatar
Karim Ratib committed
  );
Karim Ratib's avatar
Karim Ratib committed
/**
 * Implements hook_theme().
Karim Ratib's avatar
Karim Ratib committed
 */
Karim Ratib's avatar
Karim Ratib committed
function views_bulk_operations_theme() {
    'views_bulk_operations_select_all' => array(
      'variables' => array('view' => NULL),
    ),
    'views_bulk_operations_confirmation' => array(
      'variables' => array('rows' => NULL, 'vbo' => NULL),
Karim Ratib's avatar
Karim Ratib committed
  );
  $files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '/\.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);
    }
  }
Karim Ratib's avatar
Karim Ratib committed
}
Karim Ratib's avatar
Karim Ratib committed
/**
 * Implements hook_init().
Karim Ratib's avatar
Karim Ratib committed
 */
function views_bulk_operations_init() {
  // Reset selection if we're not in the view anymore.
  if (isset($_SESSION) && !isset($_SESSION['vbo_values'][$_GET['q']])) {
    unset($_SESSION['vbo_values']);
Karim Ratib's avatar
Karim Ratib committed

  // Automatically include the action files.
  $files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '/\.action\.inc$/');
      require_once($file->uri);
  }
}

/**
 * Gets the VBO field if it exists on the passed-in view.
 *
 * @return
 *  The field object if found. Otherwise, FALSE.
 */
function _views_bulk_operations_get_field($view) {
  foreach ($view->field as $field_name => $field) {
    if ($field instanceof views_bulk_operations_handler_field_operations) {
      // Add in the view object for convenience.
      $field->view = $view;
      return $field;
Karim Ratib's avatar
Karim Ratib committed
    }
Karim Ratib's avatar
Karim Ratib committed
  }
Karim Ratib's avatar
Karim Ratib committed
}

/**
 * Implements hook_views_form_substitutions().
 */
function views_bulk_operations_views_form_substitutions() {
  // Views check_plains the column label, so VBO needs to do the same
  // in order for the replace operation to succeed.
  $select_all_placeholder = check_plain('<!--views-bulk-operations-select-all-->');
  $select_all = array(
    '#type' => 'checkbox',
    '#default_value' => FALSE,
    '#attributes' => array('class' => array('vbo-select-all')),
  );
  return array(
    $select_all_placeholder => drupal_render($select_all),
  );
}

function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
  if (strpos($form_id, 'views_form_') === 0) {
    $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  }
  // Not a VBO-enabled views form.
  if (empty($vbo)) {
    return;
  // Add VBO's custom callbacks.
  $form['#validate'][] = 'views_bulk_operations_form_validate';
  $form['#submit'][] = 'views_bulk_operations_form_submit';

  // Quickfix for VBO & exposed filters using ajax. See http://drupal.org/node/1191928.
  $query = drupal_get_query_parameters($_GET, array('q'));
  $form['#action'] = url($vbo->view->get_url(), array('query' => $query));

  // Add basic VBO functionality.
  if ($form_state['step'] == 'views_form_views_form') {
    $form = views_bulk_operations_form($form, $form_state, $vbo);
  }
/**
 * Returns the 'select all' div that gets inserted below the table header row
 * or above the view results (depending on the style plugin), providing a choice
 * between selecting items on the current page, and on all pages.
 *
 * The actual insertion is done by JS, matching the degradation behavior
 * of Drupal core (no JS - no select all).
 */
function theme_views_bulk_operations_select_all($variables) {
  $view = $variables['view'];
  $form = array();

  if ($view->style_plugin instanceof views_plugin_style_table) {
    // Table displays only get the "select all" markup if they are paginated.
    if (count($view->result) == $view->total_rows) {
      return '';
    }

    $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
    $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
    $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
    $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));

    $form['select_all_pages'] = array(
      '#type' => 'button',
      '#attributes' => array('class' => array('vbo-select-all-pages')),
      '#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
      '#prefix' => '<span class="vbo-this-page">' . $this_page . ' &nbsp;',
      '#suffix' => '</span>',
    );
    $form['select_this_page'] = array(
      '#type' => 'button',
      '#attributes' => array('class' => array('vbo-select-this-page')),
      '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
      '#prefix' => '<span class="vbo-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
      '#suffix' => '</span>',
    );
  }
  else {
    $form['select_all'] = array(
      '#type' => 'fieldset',
      '#attributes' => array('class' => array('vbo-fieldset-select-all')),
    );
    $form['select_all']['this_page'] = array(
      '#type' => 'checkbox',
      '#title' => t('Select all items on this page'),
      '#default_value' => '',
      '#attributes' => array('class' => array('vbo-select-this-page')),
    );
    // If the view is paginated, offer an option to select rows on all pages.
    if (count($view->result) != $view->total_rows) {
      $form['select_all']['or'] = array(
        '#type' => 'markup',
        '#markup' => '<em>OR</em>',
      );
      $form['select_all']['all_pages'] = array(
        '#type' => 'checkbox',
        '#title' => t('Select all items on all pages'),
        '#default_value' => '',
        '#attributes' => array('class' => array('vbo-select-all-pages')),
      );
    }
  }

  $output = '<div class="vbo-select-all-markup">';
  $output .= drupal_render($form);
  $output .= '</div>';
Karim Ratib's avatar
Karim Ratib committed
/**
 * Extend the views_form multistep form with elements for executing an operation.
Karim Ratib's avatar
Karim Ratib committed
 */
function views_bulk_operations_form($form, &$form_state, $vbo) {
  $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
  $form['#prefix'] = '<div class="vbo-views-form">';
  $form['#suffix'] = '</div>';
  // Force browser to reload the page if Back is hit.
  if (preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
    drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+
    drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers

  // If there's a session variable on this view, pre-load the old operation value.
  if (isset($_SESSION['vbo_values'][$_GET['q']])) {
    $default_operation = $_SESSION['vbo_values'][$_GET['q']]['operation'];
    $select_all =  $_SESSION['vbo_values'][$_GET['q']]['rows']['select_all'];
    $default_operation = NULL;
  // Set by JS to indicate that all rows on all pages are selected.
  $form['select_all'] = array(
    '#type' => 'hidden',
    '#attributes' => array('class' => 'select-all-rows'),
  if (count($vbo->get_selected_operations()) == 1 && $vbo->options['vbo']['merge_single_action']) {
      '#type' => 'value',
    $ops = array_keys($vbo->get_selected_operations());
    $operation = $vbo->get_operation_info($ops[0]);
    $form['operation'] = array('#type' => 'value', '#value' => $ops[0]);
    if ($operation['configurable']) {
      $dummy_selection = array();
      foreach ($vbo->view->result as $result) {
        $dummy_selection[$result->{$vbo->view->base_field}] = $result;
Karim Ratib's avatar
Karim Ratib committed
      }
      $form += _views_bulk_operations_action_form($form, $form_state, $operation, $vbo->view, $dummy_selection, $vbo->get_operation_settings($operation));
    }
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $operation['label'],
    );
Karim Ratib's avatar
Karim Ratib committed
  }
  else {
    $form['single_operation'] = array(
      '#type' => 'value',
      '#value' => FALSE,
    );

    unset($form['actions']['submit']);
    $form['select'] = array(
      '#type' => 'fieldset',
      '#title' => t('Operations'),
      '#collapsible' => TRUE,
      '#attributes' => array('class' => array('container-inline')),
    );
    if ($vbo->options['vbo']['display_type'] == 0) {
      // Create dropdown and submit button.
      $form['select']['operation'] = array(
        '#type' => 'select',
        '#options' => array(0 => t('- Choose an operation -')) + $vbo->get_selected_operations(),
        '#default_value' => $default_operation,
Karim Ratib's avatar
Karim Ratib committed
      );
      $form['select']['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Execute'),
Karim Ratib's avatar
Karim Ratib committed
      );
    }
    else {
      // Create buttons for actions.
      foreach ($vbo->get_selected_operations() as $md5 => $description) {
        $form['select'][$md5] = array(
Karim Ratib's avatar
Karim Ratib committed
          '#type' => 'submit',
Karim Ratib's avatar
Karim Ratib committed
        );
      }
Karim Ratib's avatar
Karim Ratib committed
  }
  // Adds the "select all" functionality if the view has results.
  // If the view is using a table style plugin, the markup gets moved to
  // a table row below the header.
  // If we are using radio buttons, we don't use select all at all.
  if (!empty($vbo->view->result) && !$vbo->options['vbo']['force_single']) {
    $form['select_all_markup'] = array(
      '#type' => 'markup',
      '#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view)),
    );
  }
Karim Ratib's avatar
Karim Ratib committed
  return $form;
}

 * Multistep form callback for the "configure" step.
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
  $vbo = _views_bulk_operations_get_field($view);
  $operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
  $query = drupal_get_query_parameters($_GET, array('q'));
  $form['operation'] = array('#type' => 'value', '#value' => $operation);
  $form['actions'] = array(
    '#type' => 'container',
    '#attributes' => array('class' => array('form-actions')),
    '#weight' => 100,
  );
  $form['actions']['submit'] = array(
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
  );
  drupal_set_title(t('Set parameters for %action', array('%action' => $operation['label'])), PASS_THROUGH);

  $form += _views_bulk_operations_action_form(
    $form,
    $form_state,
    $operation,
    $vbo->view,
    $form_state['views_bulk_operations']['views_form_views_form']['selection'],
    $vbo->get_operation_settings($operation)
  );
Karim Ratib's avatar
Karim Ratib committed
/**
 * Multistep form callback for the "confirm" step.
Karim Ratib's avatar
Karim Ratib committed
 */
function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) {
  $vbo = _views_bulk_operations_get_field($view);
  $operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
  $rows = $form_state['views_bulk_operations']['views_form_views_form']['selection'];
  $query = drupal_get_query_parameters($_GET, array('q'));
  $form = confirm_form($form,
    t('Are you sure you want to perform %action on selected rows?', array('%action' => $operation['label'])),
    array('path' => $view->get_url(), 'query' => $query),
    theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo))
  );

  return $form;
}

/**
 * Goes through the submitted values, and returns
 * an array of selected rows, in the form of
 * $entity_id => $row_index.
 */
function _views_bulk_operations_get_selection($vbo, $form_state) {
    // If using "force single", the selection needs to be converted to an array.
    if (is_array($form_state['values'][$field_name])) {
      $selected_rows = array_filter($form_state['values'][$field_name]);
    }
    else {
      $selected_rows = array($form_state['values'][$field_name]);
    }
    // At this point, $selected_rows is an array of $row_number => $entity_id.
    // We need the $entity_id to be the key, so the array gets flipped.
    $selection = array_flip($selected_rows);
  }

  return $selection;
function views_bulk_operations_form_validate($form, &$form_state) {
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  // This is not a VBO-enabled views form.
  if (!$vbo) {
    return;
  }

  switch ($form_state['step']) {
    case 'views_form_views_form':
      if ($form_state['values']['single_operation']) {
        $operation = $vbo->get_operation_info($form_state['values']['operation']);
        if ($operation['configurable']) {
          _views_bulk_operations_action_validate($operation, $form, $form_state);
        }
Karim Ratib's avatar
Karim Ratib committed
      }
      else {
        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.'));
        }
Karim Ratib's avatar
Karim Ratib committed
      }

      $field_name = $vbo->options['id'];
      $selection = _views_bulk_operations_get_selection($vbo, $form_state);
      if (!$selection) {
        form_set_error($field_name, t('Please select at least one item.'));
      }

      // if using force single, make sure only one value was submitted.
      if ($vbo->options['vbo']['force_single'] && count($selection) > 1) {
        form_set_error($field_name, t('You may only select one item.'));
      }

      $_SESSION['vbo_values'][$_GET['q']] = array(
        'operation' => $form_state['values']['operation'],
        'rows' => array(
          'selection' => $selection,
          'select_all' => $form_state['values']['select_all'],
        ),
      );
    case 'views_bulk_operations_config_form':
      $operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
      _views_bulk_operations_action_validate($operation, $form, $form_state);
Karim Ratib's avatar
Karim Ratib committed
      break;
Karim Ratib's avatar
Karim Ratib committed
}

 * Helper function to adjust the selected set of rows.
 */
function _views_bulk_operations_adjust_selection(&$selection, $select_all, $vbo) {
    // Adjust selection to select all rows across pages.
    $view = views_get_view($vbo->view->name);
    $view->set_exposed_input($vbo->view->get_exposed_input());
    $view->set_arguments($vbo->view->args);
    $view->set_display($vbo->view->current_display);
    $view->display_handler->set_option('pager', array('type' => 'none'));
    $view->build();
    // Unset every field except the VBO one (which holds the entity id).
    // That way the performance hit becomes much smaller, because there is no
    // chance of views_handler_field_field::post_execute() firing entity_load().
    foreach ($view->field as $field_name => $field) {
      if ($field_name != $vbo->options['id']) {
        unset($view->field[$field_name]);
      }
    }
    $view->execute($vbo->view->current_display);
    foreach ($view->result as $row_index => $result) {
      $results[$result->{$vbo->field_alias}] = $row_index;
  // Adjust sticky selection accordingly.
  $_SESSION['vbo_values'][$_GET['q']]['rows'] = array('selection' => $selection, 'select_all' => $select_all);
Karim Ratib's avatar
Karim Ratib committed
/**
 * Submit handler for all steps of the VBO multistep form.
Karim Ratib's avatar
Karim Ratib committed
 */
function views_bulk_operations_form_submit($form, &$form_state) {
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  // This is not a VBO-enabled views form.
  if (!$vbo) {
    return;
  }
  switch ($form_state['step']) {
    case 'views_form_views_form':
      $form_state['views_bulk_operations']['views_form_views_form'] = $form_state['values'];
      $form_state['views_bulk_operations']['views_form_views_form']['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
      _views_bulk_operations_adjust_selection(
        $form_state['views_bulk_operations']['views_form_views_form']['selection'],
        $form_state['views_bulk_operations']['views_form_views_form']['select_all'],
        $vbo

      $operation = $vbo->get_operation_info($form_state['values']['operation']);
      if ($form_state['values']['single_operation']) {
        if ($operation['configurable']) {
          $form_state['views_bulk_operations']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
        }
        if (!empty($operation['options']['skip_confirmation'])) {
          break; // Go directly to execution
        }
        $form_state['step'] = 'views_bulk_operations_confirm_form';
        if (!$operation['configurable'] && !empty($operation['options']['skip_confirmation'])) {
          break; // Go directly to execution
        }
        $form_state['step'] = $operation['configurable'] ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
Karim Ratib's avatar
Karim Ratib committed
      }
      $form_state['single_operation'] = $form_state['values']['single_operation'];
      $form_state['rebuild'] = TRUE;
Karim Ratib's avatar
Karim Ratib committed
      return;
    case 'views_bulk_operations_config_form':
      $form_state['step'] = 'views_bulk_operations_confirm_form';
      $form_state['views_bulk_operations']['views_bulk_operations_config_form'] = $form_state['values'];
      $operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
      $form_state['views_bulk_operations']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
      if (!empty($operation['options']['skip_confirmation'])) {
Karim Ratib's avatar
Karim Ratib committed
        break; // Go directly to execution
      }
      $form_state['rebuild'] = TRUE;
    case 'views_bulk_operations_confirm_form':
Karim Ratib's avatar
Karim Ratib committed
      break;
  // Clean up unneeded SESSION variables.
  unset($_SESSION['vbo_values'][$_GET['q']]);
  $operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
  $operation_arguments = array();
  if ($operation['configurable']) {
    if ($form_state['single_operation']) {
      $form_state['values'] += $form_state['views_bulk_operations']['views_form_views_form'];
    }
    else {
      $form_state['values'] += $form_state['views_bulk_operations']['views_bulk_operations_config_form'];
    }
    $operation_arguments = $form_state['views_bulk_operations']['operation_arguments'];
  }
  _views_bulk_operations_execute(
    $form_state['views_bulk_operations']['views_form_views_form']['selection'],
    $operation,
    array('execution_type' => $vbo->options['vbo']['execution_type'], 'display_result' => $vbo->options['vbo']['display_result'])
  unset($form_state['step']);
  unset($form_state['views_bulk_operations']);

  $query = drupal_get_query_parameters($_GET, array('q'));
  $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
 * Execute the chosen action upon selected entities.
 *
 * There are three different ways of executing the action:
 * - Executing the action directly.
 *     Simple, fast, fragile. Tells PHP to ignore the execution time limit,
 *     loads all selected entities, runs the action. Of course, it's still
 *     limited by available memory, so it's very easy to try and load too many
 *     entities, which kills the script. This is the only execution type
 *     available for aggregate actions, because they need all selected entities,
 *     and Batch API / Drupal Queue are all about segmenting the entity loading
 *     and action executing in order to get around memory limits and timeouts.
 * - Using Batch API
 *     The most commonly used method. Spreads the execution across several
 *     background page requests, while showing a progress bar to the user.
 *     Each time _views_bulk_operations_batch_process() runs, it tries to load
 *     and execute a safe number of entities ($entity_load_capacity, defaults
 *     to 10). If the total selected number of entities is less than that, the
 *     code falls back to the direct execution method, since the execution can
 *     fit into a single page request, and there's no gain in using Batch API.
 * - Using the Drupal Queue.
 *     Adds each entity separately to the queue, to be processed by a worker
 *     function, which usually happens on cron.
function _views_bulk_operations_execute($vbo, $rows, $operation, $operation_arguments, $options) {
  // Add action arguments, callback arguments, other settings needed by helper
  // functions for specific execution types.
  $params = array();
  if ($operation['configurable'] && is_array($operation_arguments)) {
    $params += $operation_arguments;
Karim Ratib's avatar
Karim Ratib committed
  }
  if (isset($operation['callback arguments'])) {
    $params += $operation['callback arguments'];
Karim Ratib's avatar
Karim Ratib committed
  }
  $params['revision'] = $vbo->revision;
  $params['entity_load_capacity'] = $vbo->options['vbo']['entity_load_capacity'];
  // If the selected rows need to be passed to the action, add them in.
  if ($operation['pass rows']) {
    foreach ($rows as $entity_id => $row_index) {
      $rows[$entity_id] = $vbo->view->result[$row_index];
    }
  }

  if (!$operation['aggregate'] && !empty($operation['options']['use_queue'])) {
    foreach ($rows as $entity_id => $row) {
      $job = array(
        'description' => t('Perform %action on @type !entity_id.', array(
          '@type' => $entity_type,
          '!entity_id' => $entity_id
        'arguments' => array($entity_id, $row, $operation, $params, $user->uid, $options['display_result']),
      $queue = DrupalQueue::get('views_bulk_operations');
      $queue->createItem($job);
Karim Ratib's avatar
Karim Ratib committed
    }
    if ($options['display_result']) {
      drupal_set_message(t('Enqueued the selected operation (%action).', array(
Karim Ratib's avatar
Karim Ratib committed
  }
  elseif (!$operation['aggregate'] && $params['entity_load_capacity'] < count($rows)) {
    // 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' => array(
        array('_views_bulk_operations_batch_process', array($rows)),
      ),
      'finished' => '_views_bulk_operations_execute_finished',
      'progress_message' => '',
      'title' => t('Performing %action on selected rows...', array('%action' => $operation['label'])),
    );
    batch_set($batch);
  }
    $context['results']['rows'] = 0;
    $context['results']['time'] = microtime(TRUE);

    _views_bulk_operations_direct_process($operation, $rows, $params, $context);
    _views_bulk_operations_execute_finished(TRUE, $context['results'], array(), 0, $options + array('operation' => $operation, 'params' => $params));
Karim Ratib's avatar
Karim Ratib committed
}

 * Worker function to handle actions coming from the Drupal Queue.
function _views_bulk_operations_queue_process($data) {
  list($entity_id, $row, $operation, $params, $uid, $display_result) = $data['arguments'];
  $entities = _views_bulk_operations_entity_load($entity_type, array($entity_id), $params['revision']);
  $entity = reset($entities);
  // No entity found. It might have been deleted in the meantime. Abort.
  if (!$entity) {
    return;
  }
  if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
    watchdog('views bulk operations', 'Skipped %action on @type %title due to insufficient permissions.', array(
      '@type' => $entity_type,
      '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
    ), WATCHDOG_ALERT);
    return;
  }

  // Pass the selected row to the action if needed.
  if ($operation['pass rows']) {
    $params['rows'] = array($entity_id => $row);
  }
  _views_bulk_operations_action_do($operation, $entity_id, $entity, $params);

  if ($display_result) {
    watchdog('views bulk operations', 'Performed %action on @type %title.', array(
      '@type' => $entity_type,
      '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
Karim Ratib's avatar
Karim Ratib committed
/**
 * Helper function to handle Batch API operations.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_batch_process($rows, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($rows);
    $context['results']['time'] = microtime(TRUE);
  }

  $operation = $_SESSION['vbo_options']['operation'];
  $params = unserialize($_SESSION['vbo_options']['params']);
  // Process rows in groups.
  $remaining = $context['sandbox']['max'] - $context['sandbox']['progress'];
  $count = min($params['entity_load_capacity'], $remaining);
  $row_group = array_slice($rows, $context['sandbox']['progress'], $count, TRUE);
  $entity_type = $operation['type'];
  $entities = _views_bulk_operations_entity_load($entity_type, array_keys($row_group), $params['revision']);
  foreach ($entities as $entity_id => $entity) {
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
      $context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
        '%action' => $operation['label'],
        '@type' => $entity_type,
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
      ));
      $context['sandbox']['progress']++;
      continue;
    }

    // Pass the selected row to the action if needed.
    if ($operation['pass rows']) {
      $params['rows'] = array($entity_id => $rows[$entity_id]);
    }
    _views_bulk_operations_action_do($operation, $entity_id, $entity, $params);

    $context['results']['log'][] = t('Performed %action on @type %title.', array(
      '@type' => $entity_type,
      '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
    $context['sandbox']['progress']++;
    unset($row_group[$entity_id]);
  }
  // Looks like some entities couldn't be loaded. Adjust the count and move on.
  if (count($row_group)) {
    $context['sandbox']['progress'] += count($row_group);
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    // Provide an estimation of the completion level we've reached.
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
Karim Ratib's avatar
Karim Ratib committed
  }
    // All done. Save the number of results processed, for the finish callback.
    $context['results']['rows'] = $context['sandbox']['progress'];
Karim Ratib's avatar
Karim Ratib committed
  }
 * Helper function for direct execution operations.
function _views_bulk_operations_direct_process($operation, $rows, $params, &$context) {
  $entity_type = $operation['type'];
  $entities = _views_bulk_operations_entity_load($entity_type, array_keys($rows), $params['revision']);
  foreach ($entities as $id => $entity) {
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
      unset($entities[$id]);
      $context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
        '@type' => $entity_type,
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
    // Pass the selected rows to the action if needed.
    if ($operation['pass rows']) {
      $params['rows'] = $rows;
    }
    _views_bulk_operations_action_aggregate_do($operation, $entities, $params);

    $context['results']['log'][] = t('Performed aggregate %action on @types %ids.', array(
      '@types' => format_plural(count($entities), $entity_type, $entity_type . 's'),
      '%ids' => implode(',', array_keys($entities)),
    $context['results']['rows'] += count($entities);
  }
  else {
    foreach ($entities as $entity_id => $entity) {
      // Pass the selected row to the action if needed.
      if ($operation['pass rows']) {
        $params['rows'] = array($entity_id => $rows[$entity_id]);
      }
      _views_bulk_operations_action_do($operation, $entity_id, $entity, $params);

      $context['results']['log'][] = t('Performed %action on @type %title.', array(
        '%action' => $operation['label'],
        '@type' => $entity_type,
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
      ));
      $context['results']['rows'] += 1;
    }
function _views_bulk_operations_execute_finished($success, $results, $operations, $elapsed, $options = NULL) {
  if ($success) {
    if (!empty($results['rows'])) {
      $row_count = format_plural($results['rows'], '1 row', '@count rows');
      $message = t('!row_count processed in about !time ms:', array('!row_count' => $row_count, '!time' => round((microtime(TRUE) - $results['time']) * 1000)));
    }
    else {
      $message = t('No rows were processed:');
    }
    $message .= "\n". theme('item_list', array('items' => $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 (empty($options)) {
    $options = $_SESSION['vbo_options'];
  }

  // Inform other modules that VBO has finished executing.
  module_invoke_all('views_bulk_operations_finish', $options['operation'], $options['params'], array('results' => $results));
  if (!empty($options['display_result'])) {
    drupal_set_message($message);
  }
  unset($_SESSION['vbo_options']); // unset the options which were used for just one invocation
Karim Ratib's avatar
Karim Ratib committed
/**
 * Helper function to execute one operation.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_action_do($operation, $entity_id, $entity, $params, $account = NULL) {
  _views_bulk_operations_action_permission($operation, $account);

  $params['entity_type'] = $operation['type'];
    actions_do($operation['callback'], $entity, $params);
    // Actions that specify 'changes_property' need to be explicitly saved.
    if (in_array('changes_property', $operation['behavior'])) {
      entity_save($operation['type'], $entity);
  elseif ($operation['source'] == 'rules_action') {
    call_user_func_array($operation['callback'], array('entity' => $entity, 'context' => $params));
  }
 * Helper function to execute an aggregate operation.
function _views_bulk_operations_action_aggregate_do($operation, $entities, $params) {
  _views_bulk_operations_action_permission($operation);
  $params['entity_type'] = $operation['type'];
  actions_do($operation['callback'], $entities, $params);
 * Helper function to verify access permission to execute operation.
function _views_bulk_operations_action_permission($operation, $account = NULL) {
  if (!$account) {
    $account = $user;
  }
  $user_label = _views_bulk_operations_entity_label('user', $account);

  if (module_exists('actions_permissions')) {
    $perm = actions_permissions_get_perm($operation['original label'], $operation['callback']);
    if (!user_access($perm, $account)) {
      watchdog('actions permissions', 'An attempt by user %user to !perm was blocked due to insufficient permissions.',
        array('!perm' => $perm, '%user' => $user_label), WATCHDOG_ALERT);

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

  // If this is a rules action, check its permissions.
  if ($operation['source'] == 'rules_action') {
    if (!rules_action('component_' . $operation['key'])->access()) {
      watchdog('actions permissions', 'An attempt by user %user to rules action !key was blocked due to insufficient permissions.',
        array('!key' => $operation['key'], '%user' => $user_label), WATCHDOG_ALERT);
 * Helper function to verify access permission to operate on an entity.
function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
  if (!entity_type_supports($entity_type, 'access')) {
  $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 operation'] & $bit) {
      if (!entity_access($op, $entity_type, $entity, $account)) {
Karim Ratib's avatar
Karim Ratib committed
/**
 * Helper function to let the configurable action provide its configuration form.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_action_form($form, &$form_state, $action, $view, $selection, $settings) {
  $action_form = $action['callback'].'_form';
  $context = array(
    'view' => $view,
    'selection' => $selection,
    'settings' => $settings,
    'action' => $action,
  );

  if ($action['source'] == 'rules_action') {
    $form = $action_form($form, $form_state, $context);
  }
  else {
    $form = $action_form($context);
  }

  return $form;
Karim Ratib's avatar
Karim Ratib committed
/**
 * Helper function to let the configurable action validate the form if it provides a validator.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_action_validate($action, $form, $form_state) {
  $action_validate = $action['callback'].'_validate';
  if (function_exists($action_validate)) {
Karim Ratib's avatar
Karim Ratib committed
/**
 * Helper function to let the configurable action process the configuration form.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_action_submit($action, $form, $form_state) {
  $action_submit = $action['callback'].'_submit';
  return $action_submit($form, $form_state);
/**
 * Theme function to show the confirmation page before executing the action.
 */
function theme_views_bulk_operations_confirmation($variables) {
  $vbo = $variables['vbo'];
  $entity_type = $vbo->get_entity_type();
  $rows = $variables['rows'];
  $result_count = count($vbo->view->result);
  $row_count = count($rows);
  // All rows on all pages have been selected.
  if ($result_count < $row_count) {
    $entity_ids = array_slice(array_keys($rows), 0, $result_count);
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $vbo->revision);
    foreach ($entities as $entity) {
      $items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
    }
    $items[] = t('...and <strong>!count</strong> more.', array('!count' => $row_count - $result_count));
    $entities = _views_bulk_operations_entity_load($entity_type, array_keys($rows), $vbo->revision);
    foreach ($entities as $entity) {
      $items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));