Skip to content
views_bulk_operations.module 39.6 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

Bojan Živanović's avatar
Bojan Živanović committed
 * 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';
    $action_info = call_user_func($action_info_fn);
    if (is_array($action_info)) {
      $actions += $action_info;
Bojan Živanović's avatar
Bojan Živanović committed
    }
  }

  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).
  $path = drupal_get_path('module', 'views_bulk_operations') . '/actions/';
  $files = array(
    'argument_selector.action.inc',
    'delete.action.inc',
    'script.action.inc',
    'user_roles.action.inc',
  );

  if (!$loaded) {
    foreach ($files as $file) {
      include_once $path . $file;
    }
    $loaded = TRUE;
  }

  return $files;
}

Bojan Živanović's avatar
Bojan Živanović committed
/**
 * 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_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, 'enable_select_all_pages' => TRUE),
    'views_bulk_operations_confirmation' => array(
      'variables' => array('rows' => NULL, 'vbo' => NULL),
Karim Ratib's avatar
Karim Ratib committed
  );
  $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);
    }
  }
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']]) && $_GET['q'] != 'views/ajax') {
/**
 * 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 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-table-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';

  // Allow VBO to work when embedded using views_embed_view(), or in a block.
  if (empty($vbo->view->override_path)) {
    if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
      $vbo->view->override_path = $_GET['q'];
    }
  // 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
 * (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'];
  if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
    $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>',
    );
    $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;',
    $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')),
    );
      $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 .= 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',
Bojan Živanović's avatar
Bojan Živanović committed
    $selected_operations = array_keys($vbo->get_selected_operations());
    $operation = $vbo->get_operation($selected_operations[0]);
    if ($operation->configurable()) {
Bojan Živanović's avatar
Bojan Živanović committed
      foreach ($vbo->view->result as $row_index => $result) {
        $dummy_selection[$row_index] = $result->{$vbo->field_alias};
Karim Ratib's avatar
Karim Ratib committed
      }

      $context = array(
        'settings' => $operation->getAdminOption('settings', array()),
        'selection' => $dummy_selection,
      );
      $form += $operation->form($form, $form_state, $context);
    $form_state['operation'] = $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
      );
Bojan Živanović's avatar
Bojan Živanović committed
      // Create buttons for operations.
      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']) {
    $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 (count($vbo->view->result) != $vbo->view->total_rows && $vbo->options['vbo']['enable_select_all_pages']) {
      $enable_select_all_pages = TRUE;
    }
    $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)),
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 = $form_state['operation'];
  drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);

  $context = array(
    'settings' => $operation->getAdminOption('settings', array()),
    'selection' => $form_state['selection'],
  );
  $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' => 100,
  );
  $form['actions']['submit'] = array(
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
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 = $form_state['operation'];
  $rows = $form_state['selection'];
  $query = drupal_get_query_parameters($_GET, array('q'));
    t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation->label())),
    array('path' => $view->get_url(), 'query' => $query),
    theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo))
Bojan Živanović's avatar
Bojan Živanović committed
/**
 * Theme function to show the confirmation page before executing the operation.
Bojan Živanović's avatar
Bojan Živanović committed
 */
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);
  $items = array();
  // All rows on all pages have been selected.
  if ($result_count < $row_count) {
    $entity_ids = array_slice(array_values($rows), 0, $result_count);
Bojan Živanović's avatar
Bojan Živanović committed
    $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));
  }
  else {
    $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
Bojan Živanović's avatar
Bojan Živanović committed
    foreach ($entities as $entity) {
      $items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
    }
  }

  $count = format_plural(count($entities), 'item', 'items');
  $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following <strong>!count</strong>:', array('!count' => $count))));
Bojan Živanović's avatar
Bojan Živanović committed
  return $output;
}

/**
 * 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) {
    // 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]);
      $selection = array($form_state['values'][$field_name]);
Bojan Živanović's avatar
Bojan Živanović committed
/**
 * Helper function to adjust the selected set of rows.
 */
function _views_bulk_operations_adjust_selection(&$selection, $select_all, $vbo) {
  if ($select_all) {
    // 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()));
Bojan Živanović's avatar
Bojan Živanović committed
    $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);
    $results = array();
    foreach ($view->result as $row_index => $result) {
      $results[$row_index] = $result->{$vbo->field_alias};
Bojan Živanović's avatar
Bojan Živanović committed
    }
    $selection = $results;
  }
}

/**
 * Validate all steps of the VBO multistep form.
 */
function views_bulk_operations_form_validate($form, &$form_state) {
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);

  switch ($form_state['step']) {
    case 'views_form_views_form':
      if ($form_state['values']['single_operation']) {
        $operation = &$form_state['operation'];
        if ($operation->configurable()) {
          $operation->formValidate($form, $form_state);
Karim Ratib's avatar
Karim Ratib committed
      }
Bojan Živanović's avatar
Bojan Živanović committed
        if (!empty($form_state['triggering_element']['#hash'])) {
          $form_state['values']['operation'] = $form_state['triggering_element']['#hash'];
Bojan Živanović's avatar
Bojan Živanović committed
        if (!$form_state['values']['operation']) {
          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.'));
    case 'views_bulk_operations_config_form':
      $operation = &$form_state['operation'];
      $operation->formValidate($form, $form_state);
Karim Ratib's avatar
Karim Ratib committed
      break;
Karim Ratib's avatar
Karim Ratib committed
}

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]);
  switch ($form_state['step']) {
    case 'views_form_views_form':
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
      _views_bulk_operations_adjust_selection($form_state['selection'], $form_state['values']['select_all'], $vbo);
      // Remember form element values.
      $_SESSION['vbo_values'][$_GET['q']] = array(
        'rows' => array(
          'selection' => $form_state['selection'],
          'select_all' => $form_state['values']['select_all'],
        ),
      );
      if (!$form_state['values']['single_operation']) {
        $_SESSION['vbo_values'][$_GET['q']]['operation'] = $form_state['values']['operation'];
      }

      if ($form_state['values']['single_operation']) {
        $operation = &$form_state['operation'];
        if ($operation->configurable()) {
          $operation->formSubmit($form, $form_state);
        else {
          $operation->formOptions = $form_state['values'];
        }

        if ($operation->getAdminOption('skip_confirmation')) {
          break; // Go directly to execution
        }
        $form_state['step'] = 'views_bulk_operations_confirm_form';
        $form_state['operation'] = $operation = $vbo->get_operation($form_state['values']['operation']);
        if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
        $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
Karim Ratib's avatar
Karim Ratib committed
      }
      $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';
      $operation = &$form_state['operation'];
      $operation->formSubmit($form, $form_state);

      if ($operation->getAdminOption('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']]);
  // Execute the operation.
  _views_bulk_operations_execute($vbo, $form_state['selection'], $form_state['operation']);
Bojan Živanović's avatar
Bojan Živanović committed
  // 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.
Bojan Živanović's avatar
Bojan Živanović committed
 * Routes to the right process function (based on the chosen execution method).
Bojan Živanović's avatar
Bojan Živanović committed
 * There are three different ways of executing the operation:
 * - Directly.
 *     Simple, fast, fragile. Tells PHP to ignore the execution time limit,
Bojan Živanović's avatar
Bojan Živanović committed
 *     loads all selected entities, runs the operation. 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
Bojan Živanović's avatar
Bojan Živanović committed
 *     available for aggregate operations, because they need all selected entities,
 *     and Batch API / Drupal Queue are all about segmenting the entity loading
Bojan Živanović's avatar
Bojan Živanović committed
 *     and operation 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.
Bojan Živanović's avatar
Bojan Živanović committed
 *
 * @param $vbo
 *   The VBO field, containing a reference to the view in $vbo->view.
 * @param $selection
 *   An array in the form of $row_index => $entity_id.
 * @param $operation
 *   The operation object.
 * @param $force_direct
 *   Whether to force the direct method (when ran through drush, for example).
function _views_bulk_operations_execute($vbo, $selection, $operation, $force_direct = FALSE) {
  // Options that affect execution.
  $options = array(
    'revision' => $vbo->revision,
    'display_result' => $vbo->options['vbo']['display_result'],
    'entity_load_capacity' => $vbo->options['vbo']['entity_load_capacity'],
  );
  // An operation that needs aggregated results can only be executed directly.
  if ($operation->aggregate()) {
    $force_direct = TRUE;
  }
  // Create an array of rows with the data needed by the process functions.
  $rows = array();
  foreach ($selection as $row_index => $entity_id) {
    $rows[$row_index] = array(
      'entity_id' => $entity_id,
      'views_row' => array(),
    );
    // Some operations require full selected rows.
    // @todo Make this work when all rows on all pages are selected?
    if ($operation->needsRows() && isset($vbo->view->result[$row_index])) {
      $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
    }
  }
  if (!$force_direct && $operation->getAdminOption('use_queue')) {
    foreach ($rows as $row_index => $row) {
      // Some operations rely on knowing their position in the execution set
      // (because of specific things that need to be done at the beginning
      // or the end of the set).
      $context = array(
        'progress' => array(
          'current' => $current,
          'total' => count($rows),
        ),
      );

        'description' => t('Perform %operation on @type !entity_id.', array(
          '%operation' => $operation->label(),
          '!entity_id' => $row['entity_id'],
        'arguments' => array($row_index, $row, $operation, $context, $options, $user->uid),
      $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 (%operation).', array(
        '%operation' => $operation->label(),
Karim Ratib's avatar
Karim Ratib committed
  }
  elseif (!$force_direct && $options['entity_load_capacity'] < count($rows)) {
    $batch = array(
      'operations' => array(
        array('_views_bulk_operations_batch_process', array($rows, $operation, $options)),
      ),
      'finished' => '_views_bulk_operations_execute_finished',
      'progress_message' => '',
      'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
    $context['results']['rows'] = 0;
    $context['results']['time'] = microtime(TRUE);

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

Bojan Živanović's avatar
Bojan Živanović committed
 * Process function for the Drupal Queue execution type.
function _views_bulk_operations_queue_process($data) {
  list($row_index, $row, $operation, $operation_context, $options, $uid) = $data['arguments'];
  $entity_type = $operation->entityType;
  $entities = _views_bulk_operations_entity_load($entity_type, array($row['entity_id']), $options['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)) {
Bojan Živanović's avatar
Bojan Živanović committed
    watchdog('views bulk operations', 'Skipped %operation on @type %title due to insufficient permissions.', array(
      '%operation' => $operation->label(),
      '@type' => $entity_type,
      '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
    ), WATCHDOG_ALERT);
    return;
  }

  // Pass the selected row to the operation if needed.
  if ($operation->needsRows()) {
    $operation_context['rows'] = array($row_index => $row['views_row']);
  _views_bulk_operations_operation_do($operation, $entity, $operation_context, $account);
  if ($options['display_result']) {
    watchdog('views bulk operations', 'Performed %operation on @type %title.', array(
      '%operation' => $operation->label(),
      '@type' => $entity_type,
      '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
Karim Ratib's avatar
Karim Ratib committed
/**
Bojan Živanović's avatar
Bojan Živanović committed
 * Process function for the Batch API execution type.
Karim Ratib's avatar
Karim Ratib committed
 */
function _views_bulk_operations_batch_process($rows, $operation, $options, &$context) {
  // It is still possible to hit the time limit.
  set_time_limit(0);

  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($rows);
    $context['results']['time'] = microtime(TRUE);
  }

  // Process the rows in groups.
  $remaining = $context['sandbox']['max'] - $context['sandbox']['progress'];
  $count = min($options['entity_load_capacity'], $remaining);
  $row_group = array_slice($rows, $context['sandbox']['progress'], $count, TRUE);
  $entity_type = $operation->entityType;
  $entity_ids = array();
  foreach ($row_group as $row_id => $row) {
    $entity_ids[] = $row['entity_id'];
  }

  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  foreach ($row_group as $row_id => $row) {
    $entity_id = $row['entity_id'];
    // A matching entity couldn't be loaded. Adjust the count and move on.
    if (!isset($entities[$entity_id])) {
      $context['sandbox']['progress']++;
      unset($row_group[$row_id]);
      continue;
    }
    $entity = $entities[$entity_id];
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
Bojan Živanović's avatar
Bojan Živanović committed
      $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
        '%operation' => $operation->label(),
        '@type' => $entity_type,
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
      ));
      $context['sandbox']['progress']++;
      continue;
    }

    // Some operations rely on knowing their position in the execution set
    // (because of specific things that need to be done at the beginning
    // or the end of the set).
    $operation_context = array(
      'progress' => array(
        'current' => $context['sandbox']['progress'] + 1,
        'total' => $context['sandbox']['max'],
      ),
    );
    // Pass the selected row to the operation if needed.
    if ($operation->needsRows()) {
      $operation_context['rows'] = array($row_index => $row['views_row']);
    _views_bulk_operations_operation_do($operation, $entity, $operation_context);

    $context['sandbox']['progress']++;
    unset($row_group[$row_id]);
  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 data for the finish callback.
    $context['results']['rows'] = $context['sandbox']['progress'];
    $context['results']['options'] = $options;

    if ($options['display_result']) {
      $context['results']['log'][] = t('Performed %operation on @items.', array(
        '%operation' => $operation->label(),
        '@items' => format_plural($context['results']['rows'], '1 item', '@count items'),
      ));
    }
Karim Ratib's avatar
Karim Ratib committed
  }
Bojan Živanović's avatar
Bojan Živanović committed
 * Process function for the direct execution type.
function _views_bulk_operations_direct_process($operation, $rows, $options, &$context) {
  $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']);
  foreach ($entities as $id => $entity) {
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
      unset($entities[$id]);
Bojan Živanović's avatar
Bojan Živanović committed
      $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
        '%operation' => $operation->label(),
        '@type' => $entity_type,
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
  if ($operation->aggregate()) {
    // Pass the selected rows to the operation if needed.
    $operation_context = array();
    if ($operation->needsRows()) {
      $operation_context['rows'] = array();
      foreach ($rows as $row_index => $row) {
        $operation_context['rows'][$row_index] = $row['views_row'];
      }
    _views_bulk_operations_operation_do($operation, $entities, $operation_context);
    $context['results']['log'][] = t('Performed aggregate %operation on @items.', array(
      '%operation' => $operation->label(),
      '@items' => format_plural(count($entities), '1 item', '@count items'),
    $context['results']['rows'] += count($entities);
  }
  else {
    foreach ($rows as $row_index => $row) {
      $entity_id = $row['entity_id'];
      // A matching entity couldn't be loaded.
      if (!isset($entities[$entity_id])) {
        continue;
      }

      $entity = $entities[$entity_id];
      // Some operations rely on knowing their position in the execution set
      // (because of specific things that need to be done at the beginning
      // or the end of the set).
      $operation_context = array(
        'progress' => array(
          'current' => $context['results']['rows'] + 1,
          'total' => count($rows),
        ),
      );
      // Pass the selected rows to the operation if needed.
      if ($operation->needsRows()) {
        $operation_context['rows'] = array($row_index => $row['views_row']);
      _views_bulk_operations_operation_do($operation, $entity, $operation_context);

    $context['results']['log'][] = t('Performed %operation on @items.', array(
      '%operation' => $operation->label(),
      '@items' => format_plural(count($entities), '1 item', '@count items'),
    ));
/**
 * Executes the operation. Called from the process functions.
 */
function _views_bulk_operations_operation_do($operation, $entity, $context, $account = NULL) {
  global $user;

  // If no account was provided, fallback to the current user.
  if (!$account) {
    $account = $user;
  }

  if (!$operation->access($account)) {
    watchdog('actions permissions', 'An attempt by user %user to run operation %operation was blocked due to insufficient permissions.',
      array('%operation' => $operation->label(), '%user' => format_username($account)), WATCHDOG_ALERT);
    drupal_access_denied();
    drupal_exit();
  }

  $operation->execute($entity, $context);
}