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 = 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_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']]) && $_GET['q'] != 'views/ajax') { unset($_SESSION['vbo_values']); } } /** * 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; } } 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-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 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']; $form = array(); if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) { // Table displays only get the "select all" markup if they are paginated. if (count($view->result) == $view->total_rows) { return ''; } $wrapper_class = 'vbo-table-select-all-markup'; $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-table-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-table-select-this-page')), '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)), '#prefix' => '', ); } else { $wrapper_class = 'vbo-select-all-markup'; $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, ); $selected_operations = array_keys($vbo->get_selected_operations()); $operation = $vbo->get_operation($selected_operations[0]); if ($operation->configurable()) { $dummy_selection = array(); foreach ($vbo->view->result as $row_index => $result) { $dummy_selection[$row_index] = $result->{$vbo->field_alias}; } $context = array('selection' => $dummy_selection); $form += $operation->form($form, $form_state, $context); } $form_state['operation'] = $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 operations. 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 = $form_state['operation']; drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH); $context = 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( '#type' => 'submit', '#value' => t('Next'), '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)), ); 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 = $form_state['operation']; $rows = $form_state['selection']; $query = drupal_get_query_parameters($_GET, array('q')); $form = confirm_form($form, 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)) ); return $form; } /** * Theme function to show the confirmation page before executing the operation. */ 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); $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_values($rows), $vbo->revision); 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 !count:', array('!count' => $count)))); 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) { $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; } /** * 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())); $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}; } $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); } } else { if (!empty($form_state['triggering_element']['#hash'])) { $form_state['values']['operation'] = $form_state['triggering_element']['#hash']; } 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.')); } // 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.')); } break; case 'views_bulk_operations_config_form': $operation = &$form_state['operation']; $operation->formValidate($form, $form_state); break; } } /** * 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]); 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'; } else { $form_state['operation'] = $operation = $vbo->get_operation($form_state['values']['operation']); 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; } // 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']); // 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. * Routes to the right process function (based on the chosen execution method). * * There are three different ways of executing the operation: * - Directly. * Simple, fast, fragile. Tells PHP to ignore the execution time limit, * 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 * available for aggregate operations, because they need all selected entities, * and Batch API / Drupal Queue are all about segmenting the entity loading * 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. * * @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) { global $user; // 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')) { $entity_type = $vbo->get_entity_type(); foreach ($rows as $row_index => $row) { $job = array( 'description' => t('Perform %operation on @type !entity_id.', array( '%operation' => $operation->label(), '@type' => $entity_type, '!entity_id' => $row['entity_id'], )), 'arguments' => array($row_index, $row, $operation, $options, $user->uid), ); $queue = DrupalQueue::get('views_bulk_operations'); $queue->createItem($job); } if ($options['display_result']) { drupal_set_message(t('Enqueued the selected operation (%operation).', array( '%operation' => $operation->label(), ))); } } 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())), ); batch_set($batch); } else { @set_time_limit(0); $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)); } } /** * Process function for the Drupal Queue execution type. */ function _views_bulk_operations_queue_process($data) { list($row_index, $row, $operation, $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; } $account = user_load($uid); if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) { 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; } $operation_context = array(); // 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); // @todo Provide a way to spam less here. 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), ), WATCHDOG_INFO); } } /** * Process function for the Batch API execution type. */ function _views_bulk_operations_batch_process($rows, $operation, $options, &$context) { 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)) { $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; } $operation_context = array(); // 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'])); } else { // 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'), )); } } } /** * 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]); $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 (empty($entities)) { return; } 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]; $operation_context = array(); // 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']['rows'] += 1; } $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); } /** * Helper function that runs after the execution process is complete. */ function _views_bulk_operations_execute_finished($success, $results, $operations, $elapsed, $options = NULL) { 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))); } if (empty($options)) { // If the execution went through Batch API, the options are in $results. $options = $results['options']; } if (!empty($options['display_result'])) { drupal_set_message($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; } /** * 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; } /** * 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($operation_callback); if ($operation_arguments) { $operation->formOptions = $operation_arguments; } // Select all rows. $selection = array(); foreach ($view->result as $row_index => $row) { $selection[$row_index] = $row->{$vbo->field_alias}; } // Execute the operation on the view results. _views_bulk_operations_execute($vbo, $selection, $operation, TRUE); } /** * 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); } }