You are here

views_bulk_operations.module in Views Bulk Operations (VBO) 7.3

Allows operations to be performed on items selected in a view.

File

views_bulk_operations.module
View source
<?php

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

// Access operations.
define('VBO_ACCESS_OP_VIEW', 0x1);
define('VBO_ACCESS_OP_UPDATE', 0x2);
define('VBO_ACCESS_OP_CREATE', 0x4);
define('VBO_ACCESS_OP_DELETE', 0x8);

/**
 * Implements hook_action_info().
 *
 * Registers custom VBO actions as Drupal actions.
 */
function views_bulk_operations_action_info() {
  $actions = array();
  $files = views_bulk_operations_load_action_includes();
  foreach ($files as $filename) {
    $action_info_fn = 'views_bulk_operations_' . str_replace('.', '_', basename($filename, '.inc')) . '_info';
    if (is_callable($action_info_fn)) {
      $action_info = call_user_func($action_info_fn);
      if (is_array($action_info)) {
        $actions += $action_info;
      }
    }
    else {
      watchdog('views bulk operations', 'views_bulk_operations_action_info() expects action filenames to have a matching valid callback function named: %function', array(
        '%function' => $action_info_fn,
      ), WATCHDOG_WARNING);
    }
  }
  return $actions;
}

/**
 * Loads the VBO actions placed in their own include files (under actions/).
 *
 * @return
 *   An array of containing filenames of the included actions.
 */
function views_bulk_operations_load_action_includes() {
  static $loaded = FALSE;

  // The list of VBO actions is fairly static, so it's hardcoded for better
  // performance (hitting the filesystem with file_scan_directory(), and then
  // caching the result has its cost).
  $files = array(
    'archive.action',
    'argument_selector.action',
    'book.action',
    'change_owner.action',
    'delete.action',
    'modify.action',
    'script.action',
    'user_roles.action',
    'user_cancel.action',
  );
  if (!$loaded) {
    foreach ($files as $file) {
      module_load_include('inc', 'views_bulk_operations', 'actions/' . $file);
    }
    $loaded = TRUE;
  }
  return $files;
}

/**
 * Implements hook_cron().
 *
 * Deletes queue items belonging to VBO active queues (used by VBO's batches)
 * that are older than a day (since they can only be a result of VBO crashing
 * or the execution being interrupted in some other way). This is the interval
 * used to cleanup batches in system_cron(), so it can't be increased.
 *
 * Note: This code is specific to SystemQueue. Other queue implementations will
 * need to do their own garbage collection.
 */
function views_bulk_operations_cron() {
  db_delete('queue')
    ->condition('name', db_like('views_bulk_operations_active_queue_') . '%', 'LIKE')
    ->condition('created', REQUEST_TIME - 86400, '<')
    ->execute();
}

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

/**
 * Implements hook_views_api().
 */
function views_bulk_operations_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
  );
}

/**
 * Implements hook_theme().
 */
function views_bulk_operations_theme() {
  $themes = array(
    'views_bulk_operations_select_all' => array(
      'variables' => array(
        'view' => NULL,
        'enable_select_all_pages' => TRUE,
        'enable_select_this_page' => TRUE,
      ),
    ),
    'views_bulk_operations_confirmation' => array(
      'variables' => array(
        'rows' => NULL,
        'vbo' => NULL,
        'operation' => NULL,
        'select_all_pages' => FALSE,
      ),
    ),
  );
  $files = views_bulk_operations_load_action_includes();
  foreach ($files as $filename) {
    $action_theme_fn = 'views_bulk_operations_' . str_replace('.', '_', basename($filename, '.inc')) . '_theme';
    if (function_exists($action_theme_fn)) {
      $themes += call_user_func($action_theme_fn);
    }
  }
  return $themes;
}

/**
 * Implements hook_ctools_plugin_type().
 */
function views_bulk_operations_ctools_plugin_type() {
  return array(
    'operation_types' => array(
      'classes' => array(
        'handler',
      ),
    ),
  );
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function views_bulk_operations_ctools_plugin_directory($module, $plugin) {
  if ($module == 'views_bulk_operations') {
    return 'plugins/' . $plugin;
  }
}

/**
 * Fetch metadata for a specific operation type plugin.
 *
 * @param $operation_type
 *   Name of the plugin.
 *
 * @return
 *   An array with information about the requested operation type plugin.
 */
function views_bulk_operations_get_operation_type($operation_type) {
  ctools_include('plugins');
  return ctools_get_plugins('views_bulk_operations', 'operation_types', $operation_type);
}

/**
 * Fetch metadata for all operation type plugins.
 *
 * @return
 *   An array of arrays with information about all available operation types.
 */
function views_bulk_operations_get_operation_types() {
  ctools_include('plugins');
  return ctools_get_plugins('views_bulk_operations', 'operation_types');
}

/**
 * Gets the info array of an operation from the provider plugin.
 *
 * @param $operation_id
 *   The id of the operation for which the info shall be returned, or NULL
 *   to return an array with info about all operations.
 */
function views_bulk_operations_get_operation_info($operation_id = NULL) {
  $operations =& drupal_static(__FUNCTION__);
  if (!isset($operations)) {
    $operations = array();
    $plugins = views_bulk_operations_get_operation_types();
    foreach ($plugins as $plugin) {
      $operations += $plugin['list callback']();
    }
    uasort($operations, '_views_bulk_operations_sort_operations_by_label');
  }
  if (!empty($operation_id)) {
    return $operations[$operation_id];
  }
  else {
    return $operations;
  }
}

/**
 * Sort function used by uasort in views_bulk_operations_get_operation_info().
 *
 * A closure would be better suited for this, but closure support was added in
 * PHP 5.3 and D7 supports 5.2.
 */
function _views_bulk_operations_sort_operations_by_label($a, $b) {
  return strcasecmp($a['label'], $b['label']);
}

/**
 * Returns an operation instance.
 *
 * @param $operation_id
 *   The id of the operation to instantiate.
 *   For example: action::node_publish_action.
 * @param $entity_type
 *   The entity type on which the operation operates.
 * @param $options
 *   Options for this operation (label, operation settings, etc.)
 */
function views_bulk_operations_get_operation($operation_id, $entity_type, $options) {
  $operations =& drupal_static(__FUNCTION__);

  // Create a unique hash of the options.
  $cid = md5(serialize($options));

  // See if there's a cached copy of the operation, including entity type and
  // options.
  if (!isset($operations[$operation_id][$entity_type][$cid])) {

    // Intentionally not using views_bulk_operations_get_operation_info() here
    // since it's an expensive function that loads all the operations on the
    // system, despite the fact that we might only need a few.
    $id_fragments = explode('::', $operation_id);
    $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
    $operation_info = $plugin['list callback']($operation_id);
    if ($operation_info) {
      $operations[$operation_id][$entity_type][$cid] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options);
    }
    else {
      $operations[$operation_id][$entity_type][$cid] = FALSE;
    }
  }
  return $operations[$operation_id][$entity_type][$cid];
}

/**
 * Get all operations that match the current entity type.
 *
 * @param $entity_type
 *   Entity type.
 * @param $options
 *   An array of options for all operations, in the form of
 *   $operation_id => $operation_options.
 */
function views_bulk_operations_get_applicable_operations($entity_type, $options) {
  $operations = array();
  foreach (views_bulk_operations_get_operation_info() as $operation_id => $operation_info) {
    if ($operation_info['type'] == $entity_type || $operation_info['type'] == 'entity' || $operation_info['type'] == 'system') {
      $options[$operation_id] = !empty($options[$operation_id]) ? $options[$operation_id] : array();
      $operations[$operation_id] = views_bulk_operations_get_operation($operation_id, $entity_type, $options[$operation_id]);
    }
  }
  return $operations;
}

/**
 * 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;
    }
  }
  return FALSE;
}

/**
 * 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-table-select-all',
      ),
    ),
  );
  return array(
    $select_all_placeholder => drupal_render($select_all),
  );
}

/**
 * Implements hook_form_alter().
 */
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 basic VBO functionality.
  if ($form_state['step'] == 'views_form_views_form') {

    // The submit button added by Views Form API might be used by a non-VBO Views
    // Form handler. If there's no such handler on the view, hide the button.
    $has_other_views_form_handlers = FALSE;
    foreach ($vbo->view->field as $field) {
      if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
        if (!$field instanceof views_bulk_operations_handler_field_operations) {
          $has_other_views_form_handlers = TRUE;
        }
      }
    }
    if (!$has_other_views_form_handlers) {
      $form['actions']['#access'] = FALSE;
    }

    // The VBO field is excluded from display, stop here.
    if (!empty($vbo->options['exclude'])) {
      return;
    }
    $form = views_bulk_operations_form($form, $form_state, $vbo);
  }

  // Cache the built form to prevent it from being rebuilt prior to validation
  // and submission, which could lead to data being processed incorrectly,
  // because the views rows (and thus, the form elements as well) have changed
  // in the meantime. Matching views issue: http://drupal.org/node/1473276.
  $form_state['cache'] = TRUE;
  if (empty($vbo->view->override_url)) {

    // If the VBO view is embedded using views_embed_view(), or in a block,
    // $view->get_url() doesn't point to the current page, which means that
    // the form doesn't get processed.
    if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
      $vbo->view->override_url = $_GET['q'];

      // We are changing the override_url too late, the form action was already
      // set by Views to the previous URL, so it needs to be overriden as well.
      $query = drupal_get_query_parameters($_GET, array(
        'q',
      ));
      $form['#action'] = url($_GET['q'], array(
        'query' => $query,
      ));
    }
  }

  // Give other modules a chance to alter the form.
  drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
}

/**
 * Implements hook_views_post_build().
 *
 * Hides the VBO field if no operations are available.
 * This causes the entire VBO form to be hidden.
 *
 * @see views_bulk_operations_form_alter().
 */
function views_bulk_operations_views_post_build(&$view) {
  $vbo = _views_bulk_operations_get_field($view);
  if ($vbo && count($vbo
    ->get_selected_operations()) < 1) {
    $vbo->options['exclude'] = TRUE;
  }
}

/**
 * Returns the 'select all' div that gets inserted below the table header row
 * (for table style plugins with grouping disabled), or above the view results
 * (for non-table style plugins), 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'];
  $enable_select_all_pages = $variables['enable_select_all_pages'];
  $enable_select_this_page = $variables['enable_select_this_page'];
  $form = array();
  if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
    if (!$enable_select_all_pages) {
      return '';
    }
    $wrapper_class = 'vbo-table-select-all-markup';
    $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-table-select-all-pages',
        ),
      ),
      '#value' => t('Select all !row_count in this view.', array(
        '!row_count' => $all_pages_count,
      )),
      '#prefix' => '<span class="vbo-table-this-page">' . $this_page . ' &nbsp;',
      '#suffix' => '</span>',
    );
    if ($enable_select_this_page) {
      $form['select_this_page'] = array(
        '#type' => 'button',
        '#attributes' => array(
          'class' => array(
            'vbo-table-select-this-page',
          ),
        ),
        '#value' => t('Select only !row_count in this page.', array(
          '!row_count' => $this_page_count,
        )),
        '#prefix' => '<span class="vbo-table-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
        '#suffix' => '</span>',
      );
    }
  }
  else {
    $wrapper_class = 'vbo-select-all-markup';
    if ($enable_select_all_pages || $enable_select_this_page) {
      $form['select_all'] = array(
        '#type' => 'fieldset',
        '#attributes' => array(
          'class' => array(
            'vbo-fieldset-select-all',
          ),
        ),
      );
    }
    if ($enable_select_this_page) {
      $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 ($enable_select_all_pages && $enable_select_this_page) {
      $form['select_all']['or'] = array(
        '#type' => 'markup',
        '#markup' => '<em>' . t('OR') . '</em>',
      );
    }
    if ($enable_select_all_pages) {
      $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="' . $wrapper_class . '">';
  $output .= drupal_render($form);
  $output .= '</div>';
  return $output;
}

/**
 * Extend the views_form multistep form with elements for executing an operation.
 */
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']['js'][] = array(
    'data' => array(
      'vbo' => array(
        'row_clickable' => $vbo
          ->get_vbo_option('row_clickable'),
      ),
    ),
    'type' => 'setting',
  );
  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';

  // Wrap the form in a div with specific classes for JS targeting and theming.
  $class = 'vbo-views-form';
  if (empty($vbo->view->result)) {
    $class .= ' vbo-views-form-empty';
  }
  $form['#prefix'] = '<div class="' . $class . '">';
  $form['#suffix'] = '</div>';

  // Force browser to reload the page if Back is hit.
  if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
    drupal_add_http_header('Cache-Control', 'no-cache');

    // works for IE6+
  }
  else {
    drupal_add_http_header('Cache-Control', 'no-store');

    // works for Firefox and other browsers
  }

  // 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',
    ),
    '#default_value' => FALSE,
  );
  $form['select'] = array(
    '#type' => 'fieldset',
    '#title' => t('Operations'),
    '#collapsible' => FALSE,
    '#attributes' => array(
      'class' => array(
        'container-inline',
      ),
    ),
  );
  if ($vbo
    ->get_vbo_option('display_type') == 0) {
    $options = array(
      0 => t('- Choose an operation -'),
    );
    foreach ($vbo
      ->get_selected_operations() as $operation_id => $operation) {
      $options[$operation_id] = $operation
        ->label();
    }

    // Create dropdown and submit button.
    $form['select']['operation'] = array(
      '#type' => 'select',
      '#options' => $options,
    );
    $form['select']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Execute'),
      '#validate' => array(
        'views_bulk_operations_form_validate',
      ),
      '#submit' => array(
        'views_bulk_operations_form_submit',
      ),
    );
  }
  else {

    // Create buttons for operations.
    foreach ($vbo
      ->get_selected_operations() as $operation_id => $operation) {
      $form['select'][$operation_id] = array(
        '#type' => 'submit',
        '#value' => $operation
          ->label(),
        '#validate' => array(
          'views_bulk_operations_form_validate',
        ),
        '#submit' => array(
          'views_bulk_operations_form_submit',
        ),
        '#operation_id' => $operation_id,
      );
    }
  }

  // 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
    ->get_vbo_option('force_single')) {
    $enable_select_all_pages = FALSE;

    // If the view is paginated, and "select all items on all pages" is
    // enabled, tell that to the theme function.
    if (isset($vbo->view->total_rows) && count($vbo->view->result) != $vbo->view->total_rows && $vbo
      ->get_vbo_option('enable_select_all_pages')) {
      $enable_select_all_pages = TRUE;
    }
    $enable_select_this_page = $vbo
      ->get_vbo_option('enable_select_this_page');
    $form['select_all_markup'] = array(
      '#type' => 'markup',
      '#markup' => theme('views_bulk_operations_select_all', array(
        'view' => $vbo->view,
        'enable_select_all_pages' => $enable_select_all_pages,
        'enable_select_this_page' => $enable_select_this_page,
      )),
    );
  }
  return $form;
}

/**
 * Validation callback for the first step of the VBO form.
 */
function views_bulk_operations_form_validate($form, &$form_state) {
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  if (!empty($form_state['triggering_element']['#operation_id'])) {
    $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
  }
  if (!$form_state['values']['operation']) {
    form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
  }
  $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.'));
  }
}

/**
 * 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 = $form_state['operation'];
  drupal_set_title(t('Set parameters for %operation', array(
    '%operation' => $operation
      ->label(),
  )), PASS_THROUGH);
  $context = array(
    'entity_type' => $vbo
      ->get_entity_type(),
    // Pass the View along.
    // Has no performance penalty since objects are passed by reference,
    // but needing the full views object in a core action is in most cases
    // a sign of a wrong implementation. Do it only if you have to.
    'view' => $view,
  );
  $form += $operation
    ->form($form, $form_state, $context);
  $query = drupal_get_query_parameters($_GET, array(
    'q',
  ));
  $form['actions'] = array(
    '#type' => 'container',
    '#attributes' => array(
      'class' => array(
        'form-actions',
      ),
    ),
    '#weight' => 999,
  );
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Next'),
    '#validate' => array(
      'views_bulk_operations_config_form_validate',
    ),
    '#submit' => array(
      'views_bulk_operations_form_submit',
    ),
    '#suffix' => l(t('Cancel'), $vbo->view
      ->get_url(), array(
      'query' => $query,
    )),
  );
  return $form;
}

/**
 * Validation callback for the "configure" step.
 * Gives the operation a chance to validate its config form.
 */
function views_bulk_operations_config_form_validate($form, &$form_state) {
  $operation =& $form_state['operation'];
  $operation
    ->formValidate($form, $form_state);
}

/**
 * Multistep form callback for the "confirm" step.
 */
function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) {
  $vbo = _views_bulk_operations_get_field($view);
  $operation = $form_state['operation'];
  $rows = $form_state['selection'];
  $query = drupal_get_query_parameters($_GET, array(
    'q',
  ));
  $title = t('Are you sure you want to perform %operation on the selected items?', array(
    '%operation' => $operation
      ->label(),
  ));
  $form = confirm_form($form, $title, array(
    'path' => $view
      ->get_url(),
    'query' => $query,
  ), theme('views_bulk_operations_confirmation', array(
    'rows' => $rows,
    'vbo' => $vbo,
    'operation' => $operation,
    'select_all_pages' => $form_state['select_all_pages'],
  )));

  // Add VBO's submit handler to the Confirm button added by config_form().
  $form['actions']['submit']['#submit'] = array(
    'views_bulk_operations_form_submit',
  );

  // We can't set the View title here as $view is just a copy of the original,
  // and our settings changes won't "stick" for the first page load of the
  // confirmation form. We also can't just call drupal_set_title() directly
  // because our title will be clobbered by the actual View title later. So
  // let's tuck the title away in the form for use later.
  // @see views_bulk_operations_preprocess_views_view()
  $form['#vbo_confirm_form_title'] = $title;
  return $form;
}

/**
 * Theme function to show the confirmation page before executing the operation.
 */
function theme_views_bulk_operations_confirmation($variables) {
  $select_all_pages = $variables['select_all_pages'];
  $vbo = $variables['vbo'];
  $entity_type = $vbo
    ->get_entity_type();
  $rows = $variables['rows'];
  $items = array();

  // Load the entities from the current page, and show their titles.
  $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
  foreach ($entities as $entity) {
    $items[] = check_plain(entity_label($entity_type, $entity));
  }

  // All rows on all pages have been selected, so show a count of additional items.
  if ($select_all_pages) {
    $more_count = $vbo->view->total_rows - count($vbo->view->result);
    $items[] = t('...and %count more.', array(
      '%count' => $more_count,
    ));
  }
  $count = format_plural(count($entities), 'item', '@count items');
  $output = theme('item_list', array(
    'items' => $items,
    'title' => t('You selected the following %count:', array(
      '%count' => $count,
    )),
  ));
  return $output;
}

/**
 * Implements hook_preprocess_page().
 *
 * Hide action links on the configure and confirm pages.
 */
function views_bulk_operations_preprocess_page(&$variables) {
  if (isset($_POST['select_all'], $_POST['operation'])) {
    $variables['action_links'] = array();
  }
}

/**
 * Implements hook_preprocess_views_view().
 */
function views_bulk_operations_preprocess_views_view($variables) {

  // If we've stored a title for the confirmation form, retrieve it here and
  // retitle the View.
  // @see views_bulk_operations_confirm_form()
  if (array_key_exists('rows', $variables) && is_array($variables['rows']) && array_key_exists('#vbo_confirm_form_title', $variables['rows'])) {
    $variables['view']
      ->set_title($variables['rows']['#vbo_confirm_form_title']);
  }
}

/**
 * Goes through the submitted values, and returns
 * an array of selected rows, in the form of
 * $row_index => $entity_id.
 */
function _views_bulk_operations_get_selection($vbo, $form_state) {
  $selection = array();
  $field_name = $vbo->options['id'];
  if (!empty($form_state['values'][$field_name])) {

    // If using "force single", the selection needs to be converted to an array.
    if (is_array($form_state['values'][$field_name])) {
      $selection = array_filter($form_state['values'][$field_name]);
    }
    else {
      $selection = array(
        $form_state['values'][$field_name],
      );
    }
  }
  return $selection;
}

/**
 * Submit handler for all steps of the VBO multistep form.
 */
function views_bulk_operations_form_submit($form, &$form_state) {
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  $entity_type = $vbo
    ->get_entity_type();
  switch ($form_state['step']) {
    case 'views_form_views_form':
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
      $form_state['select_all_pages'] = $form_state['values']['select_all'];
      $options = $vbo
        ->get_operation_options($form_state['values']['operation']);
      $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
      if (!$operation
        ->configurable() && $operation
        ->getAdminOption('skip_confirmation')) {
        break;

        // Go directly to execution
      }
      $form_state['step'] = $operation
        ->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
      $form_state['rebuild'] = TRUE;
      return;
    case 'views_bulk_operations_config_form':
      $form_state['step'] = 'views_bulk_operations_confirm_form';
      $operation =& $form_state['operation'];
      $operation
        ->formSubmit($form, $form_state);
      if ($operation
        ->getAdminOption('skip_confirmation')) {
        break;

        // Go directly to execution
      }
      $form_state['rebuild'] = TRUE;
      return;
    case 'views_bulk_operations_confirm_form':
      break;
  }

  // Execute the operation.
  views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);

  // Redirect.
  $query = drupal_get_query_parameters($_GET, array(
    'q',
  ));
  $form_state['redirect'] = array(
    'path' => $vbo->view
      ->get_url(),
    array(
      'query' => $query,
    ),
  );
}

/**
 * Entry point for executing the chosen operation upon selected rows.
 *
 * If the selected operation is an aggregate operation (requiring all selected
 * items to be passed at the same time), restricted to a single value, or has
 * the skip_batching option set, the operation is executed directly.
 * This means that there is no batching & queueing, the PHP execution
 * time limit is ignored (if allowed), all selected entities are loaded and
 * processed.
 *
 * Otherwise, the selected entity ids are divided into groups not larger than
 * $entity_load_capacity, and enqueued for processing.
 * If all items on all pages should be processed, a batch job runs that
 * collects and enqueues the items from all pages of the view, page by page.
 *
 * Based on the "Enqueue the operation instead of executing it directly"
 * VBO field setting, the newly filled queue is either processed at cron
 * time by the VBO worker function, or right away in a new batch job.
 *
 * @param $vbo
 *   The VBO field, containing a reference to the view in $vbo->view.
 * @param $operation
 *   The operation object.
 * @param $selection
 *   An array in the form of $row_index => $entity_id.
 * @param $select_all_pages
 *   Whether all items on all pages should be selected.
 */
function views_bulk_operations_execute($vbo, $operation, $selection, $select_all_pages = FALSE) {
  global $user;

  // Determine if the operation needs to be executed directly.
  $aggregate = $operation
    ->aggregate();
  $skip_batching = $vbo
    ->get_vbo_option('skip_batching');
  $save_view = $vbo
    ->get_vbo_option('save_view_object_when_batching');
  $force_single = $vbo
    ->get_vbo_option('force_single');
  $execute_directly = $aggregate || $skip_batching || $force_single;

  // Try to load all rows without a batch if needed.
  if ($execute_directly && $select_all_pages) {
    views_bulk_operations_direct_adjust($selection, $vbo);
  }

  // Options that affect execution.
  $options = array(
    'revision' => $vbo->revision,
    'entity_load_capacity' => $vbo
      ->get_vbo_option('entity_load_capacity', 10),
    // The information needed to recreate the view, to avoid serializing the
    // whole object. Passed to the executed operation. Also used by
    // views_bulk_operations_adjust_selection().
    'view_info' => array(
      'name' => $vbo->view->name,
      'display' => $vbo->view->current_display,
      'arguments' => $vbo->view->args,
      'exposed_input' => $vbo->view
        ->get_exposed_input(),
    ),
  );

  // If defined, save the whole view object.
  if ($save_view) {
    $options['view_info']['view'] = $vbo->view;
  }

  // Create an array of rows in the needed format.
  $rows = array();
  $current = 1;
  foreach ($selection as $row_index => $entity_id) {
    $rows[$row_index] = array(
      'entity_id' => $entity_id,
      'views_row' => array(),
      // Some operations rely on knowing the position of the current item
      // in the execution set (because of specific things that need to be done
      // at the beginning or the end of the set).
      'position' => array(
        'current' => $current++,
        'total' => count($selection),
      ),
    );

    // Some operations require full selected rows.
    if ($operation
      ->needsRows()) {
      $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
    }
  }
  if ($execute_directly) {

    // Execute the operation directly and stop here.
    views_bulk_operations_direct_process($operation, $rows, $options);
    return;
  }

  // Determine the correct queue to use.
  if ($operation
    ->getAdminOption('postpone_processing')) {

    // Use the site queue processed on cron.
    $queue_name = 'views_bulk_operations';
  }
  else {

    // Use the active queue processed immediately by Batch API.
    $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
  }
  $batch = array(
    'operations' => array(),
    'finished' => 'views_bulk_operations_execute_finished',
    'progress_message' => '',
    'title' => t('Performing %operation on the selected items...', array(
      '%operation' => $operation
        ->label(),
    )),
  );

  // All items on all pages should be selected, add a batch job to gather
  // and enqueue them.
  if ($select_all_pages && ($vbo->view->query->pager
    ->has_more_records() || $vbo->view->query->pager
    ->get_current_page() > 0)) {
    $total_rows = $vbo->view->total_rows;
    $batch['operations'][] = array(
      'views_bulk_operations_adjust_selection',
      array(
        $queue_name,
        $operation,
        $options,
      ),
    );
  }
  else {
    $total_rows = count($rows);

    // We have all the items that we need, enqueue them right away.
    views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);

    // Provide a status message to the user, since this is the last step if
    // processing is postponed.
    if ($operation
      ->getAdminOption('postpone_processing')) {
      drupal_set_message(t('Enqueued the selected operation (%operation).', array(
        '%operation' => $operation
          ->label(),
      )));
    }
  }

  // Processing is not postponed, add a batch job to process the queue.
  if (!$operation
    ->getAdminOption('postpone_processing')) {
    $batch['operations'][] = array(
      'views_bulk_operations_active_queue_process',
      array(
        $queue_name,
        $operation,
        $total_rows,
      ),
    );
  }

  // If there are batch jobs to be processed, create the batch set.
  if (count($batch['operations'])) {
    batch_set($batch);
  }
}

/**
 * Batch API callback: loads the view page by page and enqueues all items.
 *
 * @param $queue_name
 *   The name of the queue to which the items should be added.
 * @param $operation
 *   The operation object.
 * @param $options
 *   An array of options that affect execution (revision, entity_load_capacity,
 *   view_info). Passed along with each new queue item.
 */
function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = 0;
  }
  $view_info = $options['view_info'];
  if (isset($view_info['view'])) {
    $view = $view_info['view'];

    // Because of the offset, we want our view to be re-build and re-executed.
    $view->built = FALSE;
    $view->executed = FALSE;
  }
  else {
    $view = views_get_view($view_info['name']);
    $view
      ->set_exposed_input($view_info['exposed_input']);
    $view
      ->set_arguments($view_info['arguments']);
    $view
      ->set_display($view_info['display']);
  }
  $view
    ->set_offset($context['sandbox']['progress']);
  $view
    ->build();
  $view
    ->execute($view_info['display']);

  // Note the total number of rows.
  if (empty($context['sandbox']['max'])) {
    $context['sandbox']['max'] = $view->total_rows;
  }
  $vbo = _views_bulk_operations_get_field($view);

  // Call views_handler_field_entity::pre_render() to get the entities.
  $vbo
    ->pre_render($view->result);
  $rows = array();
  foreach ($view->result as $row_index => $result) {

    // Set the row index.
    $view->row_index = $row_index;
    $rows[$row_index] = array(
      'entity_id' => $vbo
        ->get_value($result, $vbo->real_field),
      'views_row' => array(),
      'position' => array(
        'current' => ++$context['sandbox']['progress'],
        'total' => $context['sandbox']['max'],
      ),
    );

    // Some operations require full selected rows.
    if ($operation
      ->needsRows()) {
      $rows[$row_index]['views_row'] = $result;
    }
  }

  // Enqueue the gathered rows.
  views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
  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('Prepared @current out of @total', array(
      '@current' => $context['sandbox']['progress'],
      '@total' => $context['sandbox']['max'],
    ));
  }
  else {

    // Provide a status message to the user if this is the last batch job.
    if ($operation
      ->getAdminOption('postpone_processing')) {
      $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
        '%operation' => $operation
          ->label(),
      ));
    }
  }
}

/**
 * Divides the passed rows into groups and enqueues each group for processing
 *
 * @param $queue_name
 *   The name of the queue.
 * @param $rows
 *   The rows to be enqueued.
 * @param $operation
 *   The object representing the current operation.
 *   Passed along with each new queue item.
 * @param $options
 *   An array of options that affect execution (revision, entity_load_capacity).
 *   Passed along with each new queue item.
 */
function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
  global $user;
  $queue = DrupalQueue::get($queue_name, TRUE);
  $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
  foreach ($row_groups as $row_group) {
    $entity_ids = array();
    foreach ($row_group as $row) {
      $entity_ids[] = $row['entity_id'];
    }
    $job = array(
      'title' => t('Perform %operation on @type !entity_ids.', array(
        '%operation' => $operation
          ->label(),
        '@type' => $operation->entityType,
        '!entity_ids' => implode(',', $entity_ids),
      )),
      'uid' => $user->uid,
      'arguments' => array(
        $row_group,
        $operation,
        $options,
      ),
    );
    $queue
      ->createItem($job);
  }
}

/**
 * Batch API callback: processes the active queue.
 *
 * @param $queue_name
 *   The name of the queue to process.
 * @param $operation
 *   The object representing the current operation.
 * @param $total_rows
 *   The total number of processable items (across all queue items), used
 *   to report progress.
 *
 * @see views_bulk_operations_queue_item_process()
 */
function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
  static $queue;

  // It is still possible to hit the time limit.
  drupal_set_time_limit(0);

  // Prepare the sandbox.
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = $total_rows;
    $context['results']['log'] = array();
  }

  // Instantiate the queue.
  if (!isset($queue)) {
    $queue = DrupalQueue::get($queue_name, TRUE);
  }

  // Process the queue as long as it has items for us.
  $queue_item = $queue
    ->claimItem(3600);
  if ($queue_item) {

    // Process the queue item, and update the progress count.
    views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
    $queue
      ->deleteItem($queue_item);

    // Provide an estimation of the completion level we've reached.
    $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
    $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'],
    ));
  }
  if (!$queue_item || $context['finished'] === 1) {

    // All done. Provide a status message to the user.
    $context['results']['log'][] = t('Performed %operation on @items.', array(
      '%operation' => $operation
        ->label(),
      '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
    ));
  }
}

/**
 * Processes the provided queue item.
 *
 * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
 * to process the site queue, as well as by
 * views_bulk_operations_active_queue_process() to process the active queue.
 *
 * @param $queue_item_arguments
 *   The arguments of the queue item to process.
 * @param $log
 *   An injected array of log messages, to be modified by reference.
 *   If NULL, the function defaults to using watchdog.
 */
function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
  list($row_group, $operation, $options) = $queue_item_data['arguments'];
  $account = user_load($queue_item_data['uid']);
  $entity_type = $operation->entityType;
  $entity_ids = array();
  foreach ($row_group as $row_index => $row) {
    $entity_ids[] = $row['entity_id'];
  }
  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  foreach ($row_group as $row_index => $row) {
    $entity_id = $row['entity_id'];

    // A matching entity couldn't be loaded. Skip this item.
    if (!isset($entities[$entity_id])) {
      continue;
    }
    if ($options['revision']) {

      // Don't reload revisions for now, they are not statically cached and
      // usually don't run into the edge case described below.
      $entity = $entities[$entity_id];
    }
    else {

      // A previous action might have resulted in the entity being resaved
      // (e.g. node synchronization from a prior node in this batch), so try
      // to reload it. If no change occurred, the entity will be retrieved
      // from the static cache, resulting in no performance penalty.
      $entity = entity_load_single($entity_type, $entity_id);
      if (empty($entity)) {

        // The entity is no longer valid.
        continue;
      }
    }

    // If the current entity can't be accessed, skip it and log a notice.
    $skip_permission_check = $operation
      ->getAdminOption('skip_permission_check');
    if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
      $message = 'Skipped %operation on @type %title due to insufficient permissions.';
      $arguments = array(
        '%operation' => $operation
          ->label(),
        '@type' => $entity_type,
        '%title' => entity_label($entity_type, $entity),
      );
      if ($log) {
        $log[] = t($message, $arguments);
      }
      else {
        watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
      }
      continue;
    }
    $operation_context = array(
      'progress' => $row['position'],
      'view_info' => $options['view_info'],
    );
    if ($operation
      ->needsRows()) {
      $operation_context['rows'] = array(
        $row_index => $row['views_row'],
      );
    }
    $operation
      ->execute($entity, $operation_context);
    unset($row_group[$row_index]);
  }
}

/**
 * Adjusts the selection for the direct execution method.
 *
 * Just like the direct method itself, this is legacy code, used only for
 * aggregate actions.
 */
function views_bulk_operations_direct_adjust(&$selection, $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',
    'options' => array(),
  ));
  $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]);
    }
    else {

      // Get hold of the new VBO field.
      $new_vbo = $view->field[$field_name];
    }
  }
  $view
    ->execute($vbo->view->current_display);

  // Call views_handler_field_entity::pre_render() to get the entities.
  $new_vbo
    ->pre_render($view->result);
  $results = array();
  foreach ($view->result as $row_index => $result) {

    // Set the row index.
    $view->row_index = $row_index;
    $results[$row_index] = $new_vbo
      ->get_value($result, $new_vbo->real_field);
  }
  $selection = $results;
}

/**
 * Processes the passed rows directly (without batching and queueing).
 */
function views_bulk_operations_direct_process($operation, $rows, $options) {
  global $user;
  drupal_set_time_limit(0);

  // Prepare an array of status information. Imitates the Batch API naming
  // for consistency. Passed to views_bulk_operations_execute_finished().
  $context = array();
  $context['results']['progress'] = 0;
  $context['results']['log'] = array();
  if ($operation
    ->aggregate()) {

    // Load all entities.
    $entity_type = $operation->entityType;
    $entity_ids = array();
    foreach ($rows as $row_index => $row) {
      $entity_ids[] = $row['entity_id'];
    }
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
    $skip_permission_check = $operation
      ->getAdminOption('skip_permission_check');

    // Filter out entities that can't be accessed.
    foreach ($entities as $id => $entity) {
      if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $user)) {
        $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
          '%operation' => $operation
            ->label(),
          '@type' => $entity_type,
          '%title' => entity_label($entity_type, $entity),
        ));
        unset($entities[$id]);
      }
    }

    // If there are any entities left, execute the operation on them.
    if ($entities) {
      $operation_context = array(
        'view_info' => $options['view_info'],
      );

      // Pass the selected rows to the operation if needed.
      if ($operation
        ->needsRows()) {
        $operation_context['rows'] = array();
        foreach ($rows as $row_index => $row) {
          $operation_context['rows'][$row_index] = $row['views_row'];
        }
      }
      $operation
        ->execute($entities, $operation_context);
    }
  }
  else {

    // Imitate a queue and process the entities one by one.
    $queue_item_data = array(
      'uid' => $user->uid,
      'arguments' => array(
        $rows,
        $operation,
        $options,
      ),
    );
    views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
  }
  $context['results']['progress'] += count($rows);
  $context['results']['log'][] = t('Performed %operation on @items.', array(
    '%operation' => $operation
      ->label(),
    '@items' => format_plural(count($rows), '1 item', '@count items'),
  ));
  views_bulk_operations_execute_finished(TRUE, $context['results'], array());
}

/**
 * Helper function that runs after the execution process is complete.
 */
function views_bulk_operations_execute_finished($success, $results, $operations) {
  if ($success) {
    if (count($results['log']) > 1) {
      $message = theme('item_list', array(
        'items' => $results['log'],
      ));
    }
    else {
      $message = reset($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),
    ));
  }
  _views_bulk_operations_log($message);
}

/**
 * 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')) {
    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
      ->getAccessMask() & $bit) {
      if (!entity_access($op, $entity_type, $entity, $account)) {
        return FALSE;
      }
    }
  }
  return TRUE;
}

/**
 * Loads multiple entities by their entity or revision ids, and returns them,
 * keyed by the id used for loading.
 */
function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
  if (!$revision) {
    $entities = entity_load($entity_type, $ids);
  }
  else {

    // D7 can't load multiple entities by revision_id. Lovely.
    $info = entity_get_info($entity_type);
    $entities = array();
    foreach ($ids as $revision_id) {
      $loaded_entities = entity_load($entity_type, array(), array(
        $info['entity keys']['revision'] => $revision_id,
      ));
      $entities[$revision_id] = reset($loaded_entities);
    }
  }
  return $entities;
}

/**
 * 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)));
  }
}

/**
 * Display a message to the user through the relevant function.
 */
function _views_bulk_operations_log($msg) {

  // Is VBO being run through drush?
  if (function_exists('drush_log')) {
    drush_log(strip_tags($msg), 'ok');
  }
  else {
    drupal_set_message($msg);
  }
}

Functions

Namesort descending Description
theme_views_bulk_operations_confirmation Theme function to show the confirmation page before executing the operation.
theme_views_bulk_operations_select_all Returns the 'select all' div that gets inserted below the table header row (for table style plugins with grouping disabled), or above the view results (for non-table style plugins), providing a choice between selecting items on the current…
views_bulk_operations_action_info Implements hook_action_info().
views_bulk_operations_active_queue_process Batch API callback: processes the active queue.
views_bulk_operations_adjust_selection Batch API callback: loads the view page by page and enqueues all items.
views_bulk_operations_config_form Multistep form callback for the "configure" step.
views_bulk_operations_config_form_validate Validation callback for the "configure" step. Gives the operation a chance to validate its config form.
views_bulk_operations_confirm_form Multistep form callback for the "confirm" step.
views_bulk_operations_cron Implements hook_cron().
views_bulk_operations_cron_queue_info Implements of hook_cron_queue_info().
views_bulk_operations_ctools_plugin_directory Implements hook_ctools_plugin_directory().
views_bulk_operations_ctools_plugin_type Implements hook_ctools_plugin_type().
views_bulk_operations_direct_adjust Adjusts the selection for the direct execution method.
views_bulk_operations_direct_process Processes the passed rows directly (without batching and queueing).
views_bulk_operations_enqueue_rows Divides the passed rows into groups and enqueues each group for processing
views_bulk_operations_execute Entry point for executing the chosen operation upon selected rows.
views_bulk_operations_execute_finished Helper function that runs after the execution process is complete.
views_bulk_operations_form Extend the views_form multistep form with elements for executing an operation.
views_bulk_operations_form_alter Implements hook_form_alter().
views_bulk_operations_form_submit Submit handler for all steps of the VBO multistep form.
views_bulk_operations_form_validate Validation callback for the first step of the VBO form.
views_bulk_operations_get_applicable_operations Get all operations that match the current entity type.
views_bulk_operations_get_operation Returns an operation instance.
views_bulk_operations_get_operation_info Gets the info array of an operation from the provider plugin.
views_bulk_operations_get_operation_type Fetch metadata for a specific operation type plugin.
views_bulk_operations_get_operation_types Fetch metadata for all operation type plugins.
views_bulk_operations_load_action_includes Loads the VBO actions placed in their own include files (under actions/).
views_bulk_operations_preprocess_page Implements hook_preprocess_page().
views_bulk_operations_preprocess_views_view Implements hook_preprocess_views_view().
views_bulk_operations_queue_item_process Processes the provided queue item.
views_bulk_operations_theme Implements hook_theme().
views_bulk_operations_views_api Implements hook_views_api().
views_bulk_operations_views_form_substitutions Implements hook_views_form_substitutions().
views_bulk_operations_views_post_build Implements hook_views_post_build().
_views_bulk_operations_entity_access Helper function to verify access permission to operate on an entity.
_views_bulk_operations_entity_load Loads multiple entities by their entity or revision ids, and returns them, keyed by the id used for loading.
_views_bulk_operations_get_field Gets the VBO field if it exists on the passed-in view.
_views_bulk_operations_get_selection Goes through the submitted values, and returns an array of selected rows, in the form of $row_index => $entity_id.
_views_bulk_operations_log Display a message to the user through the relevant function.
_views_bulk_operations_report_error Helper function to report an error.
_views_bulk_operations_sort_operations_by_label Sort function used by uasort in views_bulk_operations_get_operation_info().

Constants