array(
'worker callback' => '_views_bulk_operations_queue_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),
),
'views_bulk_operations_confirmation' => array(
'variables' => array('rows' => NULL, 'vbo' => NULL),
),
);
$files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '/\.action\.inc$/');
if ($files) foreach ($files as $file) {
$action_theme_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($file->filename, '.inc')).'_theme';
if (function_exists($action_theme_fn)) {
$themes += call_user_func($action_theme_fn);
}
}
return $themes;
}
/**
* Implements hook_init().
*/
function views_bulk_operations_init() {
// Reset selection if we're not in the view anymore.
if (isset($_SESSION) && !isset($_SESSION['vbo_values'][$_GET['q']])) {
unset($_SESSION['vbo_values']);
}
// Automatically include the action files.
$files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '/\.action\.inc$/');
if ($files) {
foreach ($files as $file) {
require_once($file->uri);
}
}
}
/**
* Gets the VBO field if it exists on the passed-in view.
*
* @return
* The field object if found. Otherwise, FALSE.
*/
function _views_bulk_operations_get_field($view) {
foreach ($view->field as $field_name => $field) {
if ($field instanceof views_bulk_operations_handler_field_operations) {
// Add in the view object for convenience.
$field->view = $view;
return $field;
}
}
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('');
$select_all = array(
'#type' => 'checkbox',
'#default_value' => FALSE,
'#attributes' => array('class' => array('vbo-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 VBO's custom callbacks.
$form['#validate'][] = 'views_bulk_operations_form_validate';
$form['#submit'][] = 'views_bulk_operations_form_submit';
// Quickfix for VBO & exposed filters using ajax. See http://drupal.org/node/1191928.
$query = drupal_get_query_parameters($_GET, array('q'));
$form['#action'] = url($vbo->view->get_url(), array('query' => $query));
// Add basic VBO functionality.
if ($form_state['step'] == 'views_form_views_form') {
$form = views_bulk_operations_form($form, $form_state, $vbo);
}
}
/**
* Returns the 'select all' div that gets inserted below the table header row
* or above the view results (depending on the style plugin), providing a choice
* between selecting items on the current page, and on all pages.
*
* The actual insertion is done by JS, matching the degradation behavior
* of Drupal core (no JS - no select all).
*/
function theme_views_bulk_operations_select_all($variables) {
$view = $variables['view'];
$form = array();
if ($view->style_plugin instanceof views_plugin_style_table) {
// Table displays only get the "select all" markup if they are paginated.
if (count($view->result) == $view->total_rows) {
return '';
}
$this_page_count = format_plural(count($view->result), '1 row', '@count rows');
$this_page = t('Selected !row_count 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 !row_count in this view.', array('!row_count' => $all_pages_count));
$form['select_all_pages'] = array(
'#type' => 'button',
'#attributes' => array('class' => array('vbo-select-all-pages')),
'#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
'#prefix' => '' . $this_page . ' ',
'#suffix' => '',
);
$form['select_this_page'] = array(
'#type' => 'button',
'#attributes' => array('class' => array('vbo-select-this-page')),
'#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
'#prefix' => '' . $all_pages . ' ',
'#suffix' => '',
);
}
else {
$form['select_all'] = array(
'#type' => 'fieldset',
'#attributes' => array('class' => array('vbo-fieldset-select-all')),
);
$form['select_all']['this_page'] = array(
'#type' => 'checkbox',
'#title' => t('Select all items on this page'),
'#default_value' => '',
'#attributes' => array('class' => array('vbo-select-this-page')),
);
// If the view is paginated, offer an option to select rows on all pages.
if (count($view->result) != $view->total_rows) {
$form['select_all']['or'] = array(
'#type' => 'markup',
'#markup' => 'OR',
);
$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 = '
';
$output .= drupal_render($form);
$output .= '
';
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']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
$form['#prefix'] = '';
$form['#suffix'] = '
';
// 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+
}
else {
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'];
}
else {
$default_operation = NULL;
$select_all = FALSE;
}
// 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' => $select_all,
);
if (count($vbo->get_selected_operations()) == 1 && $vbo->options['vbo']['merge_single_action']) {
$form['single_operation'] = array(
'#type' => 'value',
'#value' => TRUE,
);
$ops = array_keys($vbo->get_selected_operations());
$operation = $vbo->get_operation_info($ops[0]);
$form['operation'] = array('#type' => 'value', '#value' => $ops[0]);
if ($operation['configurable']) {
$dummy_selection = array();
foreach ($vbo->view->result as $result) {
$dummy_selection[$result->{$vbo->view->base_field}] = $result;
}
$form += _views_bulk_operations_action_form($form, $form_state, $operation, $vbo->view, $dummy_selection, $vbo->get_operation_settings($operation));
}
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $operation['label'],
);
}
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,
);
$form['select']['submit'] = array(
'#type' => 'submit',
'#value' => t('Execute'),
);
}
else {
// Create buttons for actions.
foreach ($vbo->get_selected_operations() as $md5 => $description) {
$form['select'][$md5] = array(
'#type' => 'submit',
'#value' => $description,
'#hash' => $md5,
);
}
}
}
// Adds the "select all" functionality if the view has results.
// If the view is using a table style plugin, the markup gets moved to
// a table row below the header.
// If we are using radio buttons, we don't use select all at all.
if (!empty($vbo->view->result) && !$vbo->options['vbo']['force_single']) {
$form['select_all_markup'] = array(
'#type' => 'markup',
'#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view)),
);
}
return $form;
}
/**
* Multistep form callback for the "configure" step.
*/
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
$vbo = _views_bulk_operations_get_field($view);
$operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
$query = drupal_get_query_parameters($_GET, array('q'));
$form['operation'] = array('#type' => 'value', '#value' => $operation);
$form['actions'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('form-actions')),
'#weight' => 100,
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Next'),
'#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
);
drupal_set_title(t('Set parameters for %action', array('%action' => $operation['label'])), PASS_THROUGH);
$form += _views_bulk_operations_action_form(
$form,
$form_state,
$operation,
$vbo->view,
$form_state['views_bulk_operations']['views_form_views_form']['selection'],
$vbo->get_operation_settings($operation)
);
return $form;
}
/**
* 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 = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
$rows = $form_state['views_bulk_operations']['views_form_views_form']['selection'];
$query = drupal_get_query_parameters($_GET, array('q'));
$form = confirm_form($form,
t('Are you sure you want to perform %action on selected rows?', array('%action' => $operation['label'])),
array('path' => $view->get_url(), 'query' => $query),
theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo))
);
return $form;
}
/**
* Goes through the submitted values, and returns
* an array of selected rows, in the form of
* $entity_id => $row_index.
*/
function _views_bulk_operations_get_selection($vbo, $form_state) {
$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])) {
$selected_rows = array_filter($form_state['values'][$field_name]);
}
else {
$selected_rows = array($form_state['values'][$field_name]);
}
// At this point, $selected_rows is an array of $row_number => $entity_id.
// We need the $entity_id to be the key, so the array gets flipped.
$selection = array_flip($selected_rows);
}
return $selection;
}
/**
* 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]);
// This is not a VBO-enabled views form.
if (!$vbo) {
return;
}
switch ($form_state['step']) {
case 'views_form_views_form':
if ($form_state['values']['single_operation']) {
$operation = $vbo->get_operation_info($form_state['values']['operation']);
if ($operation['configurable']) {
_views_bulk_operations_action_validate($operation, $form, $form_state);
}
}
else {
if (!empty($form_state['clicked_button']['#hash'])) {
$form_state['values']['operation'] = $form_state['clicked_button']['#hash'];
}
if (!$form_state['values']['operation']) { // No action selected
form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
}
}
$field_name = $vbo->options['id'];
$selection = _views_bulk_operations_get_selection($vbo, $form_state);
if (!$selection) {
form_set_error($field_name, t('Please select at least one item.'));
}
// if using force single, make sure only one value was submitted.
if ($vbo->options['vbo']['force_single'] && count($selection) > 1) {
form_set_error($field_name, t('You may only select one item.'));
}
$_SESSION['vbo_values'][$_GET['q']] = array(
'operation' => $form_state['values']['operation'],
'rows' => array(
'selection' => $selection,
'select_all' => $form_state['values']['select_all'],
),
);
break;
case 'views_bulk_operations_config_form':
$operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
_views_bulk_operations_action_validate($operation, $form, $form_state);
break;
}
}
/**
* 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'));
$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[$result->{$vbo->field_alias}] = $row_index;
}
$selection = $results;
}
// Adjust sticky selection accordingly.
$_SESSION['vbo_values'][$_GET['q']]['rows'] = array('selection' => $selection, 'select_all' => $select_all);
}
/**
* 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]);
// This is not a VBO-enabled views form.
if (!$vbo) {
return;
}
switch ($form_state['step']) {
case 'views_form_views_form':
$form_state['views_bulk_operations']['views_form_views_form'] = $form_state['values'];
$form_state['views_bulk_operations']['views_form_views_form']['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
_views_bulk_operations_adjust_selection(
$form_state['views_bulk_operations']['views_form_views_form']['selection'],
$form_state['views_bulk_operations']['views_form_views_form']['select_all'],
$vbo
);
$operation = $vbo->get_operation_info($form_state['values']['operation']);
if ($form_state['values']['single_operation']) {
if ($operation['configurable']) {
$form_state['views_bulk_operations']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
}
if (!empty($operation['options']['skip_confirmation'])) {
break; // Go directly to execution
}
$form_state['step'] = 'views_bulk_operations_confirm_form';
}
else {
if (!$operation['configurable'] && !empty($operation['options']['skip_confirmation'])) {
break; // Go directly to execution
}
$form_state['step'] = $operation['configurable'] ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
}
$form_state['single_operation'] = $form_state['values']['single_operation'];
$form_state['rebuild'] = TRUE;
return;
case 'views_bulk_operations_config_form':
$form_state['step'] = 'views_bulk_operations_confirm_form';
$form_state['views_bulk_operations']['views_bulk_operations_config_form'] = $form_state['values'];
$operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
$form_state['views_bulk_operations']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state);
if (!empty($operation['options']['skip_confirmation'])) {
break; // Go directly to execution
}
$form_state['rebuild'] = TRUE;
return;
case 'views_bulk_operations_confirm_form':
break;
}
// Clean up unneeded SESSION variables.
unset($_SESSION['vbo_values'][$_GET['q']]);
// Execute the VBO.
$operation = $vbo->get_operation_info($form_state['views_bulk_operations']['views_form_views_form']['operation']);
$operation_arguments = array();
if ($operation['configurable']) {
if ($form_state['single_operation']) {
$form_state['values'] += $form_state['views_bulk_operations']['views_form_views_form'];
}
else {
$form_state['values'] += $form_state['views_bulk_operations']['views_bulk_operations_config_form'];
}
$operation_arguments = $form_state['views_bulk_operations']['operation_arguments'];
}
_views_bulk_operations_execute(
$vbo,
$form_state['views_bulk_operations']['views_form_views_form']['selection'],
$operation,
$operation_arguments,
array('execution_type' => $vbo->options['vbo']['execution_type'], 'display_result' => $vbo->options['vbo']['display_result'])
);
// Clean up the form.
unset($form_state['step']);
unset($form_state['views_bulk_operations']);
$query = drupal_get_query_parameters($_GET, array('q'));
$form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
}
/**
* Execute the chosen action upon selected entities.
*
* There are three different ways of executing the action:
* - Executing the action directly.
* Simple, fast, fragile. Tells PHP to ignore the execution time limit,
* loads all selected entities, runs the action. Of course, it's still
* limited by available memory, so it's very easy to try and load too many
* entities, which kills the script. This is the only execution type
* available for aggregate actions, because they need all selected entities,
* and Batch API / Drupal Queue are all about segmenting the entity loading
* and action executing in order to get around memory limits and timeouts.
* - Using Batch API
* The most commonly used method. Spreads the execution across several
* background page requests, while showing a progress bar to the user.
* Each time _views_bulk_operations_batch_process() runs, it tries to load
* and execute a safe number of entities ($entity_load_capacity, defaults
* to 10). If the total selected number of entities is less than that, the
* code falls back to the direct execution method, since the execution can
* fit into a single page request, and there's no gain in using Batch API.
* - Using the Drupal Queue.
* Adds each entity separately to the queue, to be processed by a worker
* function, which usually happens on cron.
*/
function _views_bulk_operations_execute($vbo, $rows, $operation, $operation_arguments, $options) {
global $user;
// Add action arguments, callback arguments, other settings needed by helper
// functions for specific execution types.
$params = array();
if ($operation['configurable'] && is_array($operation_arguments)) {
$params += $operation_arguments;
}
if (isset($operation['callback arguments'])) {
$params += $operation['callback arguments'];
}
$params['revision'] = $vbo->revision;
$params['entity_load_capacity'] = $vbo->options['vbo']['entity_load_capacity'];
// If the selected rows need to be passed to the action, add them in.
if ($operation['pass rows']) {
foreach ($rows as $entity_id => $row_index) {
$rows[$entity_id] = $vbo->view->result[$row_index];
}
}
if (!$operation['aggregate'] && !empty($operation['options']['use_queue'])) {
$entity_type = $vbo->get_entity_type();
unset($params['view']);
foreach ($rows as $entity_id => $row) {
$job = array(
'description' => t('Perform %action on @type !entity_id.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'!entity_id' => $entity_id
)),
'arguments' => array($entity_id, $row, $operation, $params, $user->uid, $options['display_result']),
);
$queue = DrupalQueue::get('views_bulk_operations');
$queue->createItem($job);
}
if ($options['display_result']) {
drupal_set_message(t('Enqueued the selected operation (%action).', array(
'%action' => $operation['label'],
)));
}
}
elseif (!$operation['aggregate'] && $params['entity_load_capacity'] < count($rows)) {
// Save the options in the session because Batch API doesn't give a way to
// send a parameter to the finished callback.
$_SESSION['vbo_options']['display_result'] = $options['display_result'];
$_SESSION['vbo_options']['operation'] = $operation;
$_SESSION['vbo_options']['params'] = serialize($params);
$batch = array(
'operations' => array(
array('_views_bulk_operations_batch_process', array($rows)),
),
'finished' => '_views_bulk_operations_execute_finished',
'progress_message' => '',
'title' => t('Performing %action on selected rows...', array('%action' => $operation['label'])),
);
batch_set($batch);
}
else {
@set_time_limit(0);
$context['results']['rows'] = 0;
$context['results']['time'] = microtime(TRUE);
_views_bulk_operations_direct_process($operation, $rows, $params, $context);
_views_bulk_operations_execute_finished(TRUE, $context['results'], array(), 0, $options + array('operation' => $operation, 'params' => $params));
}
}
/**
* Worker function to handle actions coming from the Drupal Queue.
*/
function _views_bulk_operations_queue_process($data) {
list($entity_id, $row, $operation, $params, $uid, $display_result) = $data['arguments'];
$entity_type = $operation['type'];
$entities = _views_bulk_operations_entity_load($entity_type, array($entity_id), $params['revision']);
$entity = reset($entities);
// No entity found. It might have been deleted in the meantime. Abort.
if (!$entity) {
return;
}
$account = user_load($uid);
if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
watchdog('views bulk operations', 'Skipped %action on @type %title due to insufficient permissions.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
), WATCHDOG_ALERT);
return;
}
// Pass the selected row to the action if needed.
if ($operation['pass rows']) {
$params['rows'] = array($entity_id => $row);
}
_views_bulk_operations_action_do($operation, $entity_id, $entity, $params);
if ($display_result) {
watchdog('views bulk operations', 'Performed %action on @type %title.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
), WATCHDOG_INFO);
}
}
/**
* Helper function to handle Batch API operations.
*/
function _views_bulk_operations_batch_process($rows, &$context) {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($rows);
$context['results']['time'] = microtime(TRUE);
}
$operation = $_SESSION['vbo_options']['operation'];
$params = unserialize($_SESSION['vbo_options']['params']);
// Process rows in groups.
$remaining = $context['sandbox']['max'] - $context['sandbox']['progress'];
$count = min($params['entity_load_capacity'], $remaining);
$row_group = array_slice($rows, $context['sandbox']['progress'], $count, TRUE);
$entity_type = $operation['type'];
$entities = _views_bulk_operations_entity_load($entity_type, array_keys($row_group), $params['revision']);
foreach ($entities as $entity_id => $entity) {
if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
$context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
));
$context['sandbox']['progress']++;
continue;
}
// Pass the selected row to the action if needed.
if ($operation['pass rows']) {
$params['rows'] = array($entity_id => $rows[$entity_id]);
}
_views_bulk_operations_action_do($operation, $entity_id, $entity, $params);
$context['results']['log'][] = t('Performed %action on @type %title.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
));
$context['sandbox']['progress']++;
unset($row_group[$entity_id]);
}
// Looks like some entities couldn't be loaded. Adjust the count and move on.
if (count($row_group)) {
$context['sandbox']['progress'] += count($row_group);
}
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
// Provide an estimation of the completion level we've reached.
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
$context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
}
else {
// All done. Save the number of results processed, for the finish callback.
$context['results']['rows'] = $context['sandbox']['progress'];
}
}
/**
* Helper function for direct execution operations.
*/
function _views_bulk_operations_direct_process($operation, $rows, $params, &$context) {
$entity_type = $operation['type'];
$entities = _views_bulk_operations_entity_load($entity_type, array_keys($rows), $params['revision']);
foreach ($entities as $id => $entity) {
if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
unset($entities[$id]);
$context['results']['log'][] = t('Skipped %action on @type %title due to insufficient permissions.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
));
}
}
if (empty($entities)) {
return;
}
if ($operation['aggregate']) {
// Pass the selected rows to the action if needed.
if ($operation['pass rows']) {
$params['rows'] = $rows;
}
_views_bulk_operations_action_aggregate_do($operation, $entities, $params);
$context['results']['log'][] = t('Performed aggregate %action on @types %ids.', array(
'%action' => $operation['label'],
'@types' => format_plural(count($entities), $entity_type, $entity_type . 's'),
'%ids' => implode(',', array_keys($entities)),
));
$context['results']['rows'] += count($entities);
}
else {
foreach ($entities as $entity_id => $entity) {
// Pass the selected row to the action if needed.
if ($operation['pass rows']) {
$params['rows'] = array($entity_id => $rows[$entity_id]);
}
_views_bulk_operations_action_do($operation, $entity_id, $entity, $params);
$context['results']['log'][] = t('Performed %action on @type %title.', array(
'%action' => $operation['label'],
'@type' => $entity_type,
'%title' => _views_bulk_operations_entity_label($entity_type, $entity),
));
$context['results']['rows'] += 1;
}
}
}
/**
* Helper function to cleanup operations.
*/
function _views_bulk_operations_execute_finished($success, $results, $operations, $elapsed, $options = NULL) {
if ($success) {
if (!empty($results['rows'])) {
$row_count = format_plural($results['rows'], '1 row', '@count rows');
$message = t('!row_count processed in about !time ms:', array('!row_count' => $row_count, '!time' => round((microtime(TRUE) - $results['time']) * 1000)));
}
else {
$message = t('No rows were processed:');
}
$message .= "\n". theme('item_list', array('items' => $results['log']));
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$message = t('An error occurred while processing @operation with arguments: @arguments',
array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
}
if (empty($options)) {
$options = $_SESSION['vbo_options'];
}
// Inform other modules that VBO has finished executing.
module_invoke_all('views_bulk_operations_finish', $options['operation'], $options['params'], array('results' => $results));
if (!empty($options['display_result'])) {
drupal_set_message($message);
}
unset($_SESSION['vbo_options']); // unset the options which were used for just one invocation
}
/**
* Helper function to execute one operation.
*/
function _views_bulk_operations_action_do($operation, $entity_id, $entity, $params, $account = NULL) {
_views_bulk_operations_action_permission($operation, $account);
$params['entity_type'] = $operation['type'];
if ($operation['source'] == 'action') {
actions_do($operation['callback'], $entity, $params);
// Actions that specify 'changes_property' need to be explicitly saved.
if (in_array('changes_property', $operation['behavior'])) {
entity_save($operation['type'], $entity);
}
}
elseif ($operation['source'] == 'rules_action') {
call_user_func_array($operation['callback'], array('entity' => $entity, 'context' => $params));
}
}
/**
* Helper function to execute an aggregate operation.
*/
function _views_bulk_operations_action_aggregate_do($operation, $entities, $params) {
_views_bulk_operations_action_permission($operation);
$params['entity_type'] = $operation['type'];
actions_do($operation['callback'], $entities, $params);
}
/**
* Helper function to verify access permission to execute operation.
*/
function _views_bulk_operations_action_permission($operation, $account = NULL) {
global $user;
if (!$account) {
$account = $user;
}
$user_label = _views_bulk_operations_entity_label('user', $account);
if (module_exists('actions_permissions')) {
$perm = actions_permissions_get_perm($operation['original label'], $operation['callback']);
if (!user_access($perm, $account)) {
watchdog('actions permissions', 'An attempt by user %user to !perm was blocked due to insufficient permissions.',
array('!perm' => $perm, '%user' => $user_label), WATCHDOG_ALERT);
drupal_access_denied();
drupal_exit();
}
}
// Check against additional permissions.
if (!empty($operation['permissions'])) {
foreach ($operation['permissions'] as $perm) {
if (!user_access($perm, $account)) {
watchdog('actions permissions', 'An attempt by user %user to !perm was blocked due to insufficient permissions.',
array('!perm' => $perm, '%user' => $user_label), WATCHDOG_ALERT);
drupal_access_denied();
drupal_exit();
}
}
}
// If this is a rules action, check its permissions.
if ($operation['source'] == 'rules_action') {
if (!rules_action('component_' . $operation['key'])->access()) {
watchdog('actions permissions', 'An attempt by user %user to rules action !key was blocked due to insufficient permissions.',
array('!key' => $operation['key'], '%user' => $user_label), WATCHDOG_ALERT);
drupal_access_denied();
drupal_exit();
}
}
}
/**
* 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['access operation'] & $bit) {
if (!entity_access($op, $entity_type, $entity, $account)) {
return FALSE;
}
}
}
return TRUE;
}
/**
* Helper function to let the configurable action provide its configuration form.
*/
function _views_bulk_operations_action_form($form, &$form_state, $action, $view, $selection, $settings) {
$action_form = $action['callback'].'_form';
$context = array(
'view' => $view,
'selection' => $selection,
'settings' => $settings,
'action' => $action,
);
if ($action['source'] == 'rules_action') {
$form = $action_form($form, $form_state, $context);
}
else {
$form = $action_form($context);
}
return $form;
}
/**
* Helper function to let the configurable action validate the form if it provides a validator.
*/
function _views_bulk_operations_action_validate($action, $form, $form_state) {
$action_validate = $action['callback'].'_validate';
if (function_exists($action_validate)) {
$action_validate($form, $form_state);
}
}
/**
* Helper function to let the configurable action process the configuration form.
*/
function _views_bulk_operations_action_submit($action, $form, $form_state) {
$action_submit = $action['callback'].'_submit';
return $action_submit($form, $form_state);
}
/**
* Theme function to show the confirmation page before executing the action.
*/
function theme_views_bulk_operations_confirmation($variables) {
$vbo = $variables['vbo'];
$entity_type = $vbo->get_entity_type();
$rows = $variables['rows'];
$result_count = count($vbo->view->result);
$row_count = count($rows);
$items = array();
// All rows on all pages have been selected.
if ($result_count < $row_count) {
$entity_ids = array_slice(array_keys($rows), 0, $result_count);
$entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $vbo->revision);
foreach ($entities as $entity) {
$items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
}
$items[] = t('...and !count more.', array('!count' => $row_count - $result_count));
}
else {
$entities = _views_bulk_operations_entity_load($entity_type, array_keys($rows), $vbo->revision);
foreach ($entities as $entity) {
$items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
}
}
$row_count = format_plural(count($entities), 'row', 'rows');
$output = theme('item_list', array('items' => $items, 'title' => t('You selected the following !row_count:', array('!row_count' => $row_count))));
return $output;
}
/**
* 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;
}
/**
* Label function for entities.
* Core entities don't declare the "label" key, so entity_label() fails,
* and a fallback is needed. This function provides that fallback.
*/
function _views_bulk_operations_entity_label($entity_type, $entity) {
$label = entity_label($entity_type, $entity);
if (!$label) {
$entity_info = entity_get_info($entity_type);
$id_key = $entity_info['entity keys']['id'];
// Many entity types (e.g. "user") have a name which fits the label perfectly.
if (isset($entity->name)) {
$label = $entity->name;
}
elseif (isset($entity->{$id_key})) {
// Fallback to the id key.
$label = $entity->{$id_key};
}
}
return $label;
}
/**
* Implements hook_action_info().
* Registers custom VBO actions as Drupal actions.
*/
function views_bulk_operations_action_info() {
$actions = array();
$files = file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '/\.action\.inc$/');
if ($files) {
foreach ($files as $file) {
require_once($file->uri);
$action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($file->filename, '.inc')).'_info';
$action_info = call_user_func($action_info_fn);
if (is_array($action_info)) {
$actions += $action_info;
}
}
}
return $actions;
}
/**
* API function to programmatically invoke a VBO.
*/
function views_bulk_operations_execute($vid, $operation_callback, $operation_arguments = array(), $view_exposed_input = array(), $view_arguments = array()) {
$view = views_get_view($vid);
if (!is_object($view)) {
_views_bulk_operations_report_error('Could not find view %vid.', array('%vid' => $vid));
return;
}
// Build the view, so that the VBO field can be found.
$view->set_exposed_input($view_exposed_input);
$view->set_arguments($view_arguments);
$view->build();
$view->query->set_limit(NULL); // reset the work done by the pager
$view->query->set_offset(NULL);
// Find the view display that has the VBO style.
$vbo = _views_bulk_operations_get_field($view);
if (!$vbo) {
_views_bulk_operations_report_error('Could not find a VBO field in view %vid.', array('%vid' => $vid));
return;
}
$view->execute();
// Find the selected operation.
$operations = $vbo->get_selected_operations();
if (!isset($operations[$operation_callback])) {
_views_bulk_operations_report_error('Could not find operation %operation in view %vid.', array('%operation' => $operation_callback, '%vid' => $vid));
return;
}
$operation = $vbo->get_operation_info($operation_callback);
$rows = array();
foreach ($view->result as $row) {
$rows[$row->{$vbo->field_alias}] = $row;
}
// Execute the operation on the view results.
$execution_type = $vbo->options['vbo']['execution_type'];
if ($execution_type == VBO_EXECUTION_BATCH) {
$execution_type = VBO_EXECUTION_DIRECT; // we don't yet support Batch API here
}
$display_result = $vbo->options['vbo']['display_result'];
_views_bulk_operations_execute(
$vbo,
$rows,
$operation,
$operation_arguments,
array('execution_type' => $execution_type, 'display_result' => $display_result)
);
}
/**
* Helper function to report an error.
*/
function _views_bulk_operations_report_error($msg, $arg) {
watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
if (function_exists('drush_set_error')) {
drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
}
}
/**
* Helper function to log an information.
*/
function _views_bulk_operations_log($msg) {
if (function_exists('drush_log')) {
drush_log(strip_tags($msg), 'ok');
}
else {
drupal_set_message($msg);
}
}