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.moduleView 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 . ' ',
'#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 . ' ',
'#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
Name | 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(). |