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' => '', ); } 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); } }