condition('name', db_like('views_bulk_operations_active_queue_'), 'LIKE') ->condition('created', REQUEST_TIME - 86400, '<') ->execute(); } /** * Implements of hook_cron_queue_info(). */ function views_bulk_operations_cron_queue_info() { return array( 'views_bulk_operations' => array( 'worker callback' => 'views_bulk_operations_queue_item_process', 'time' => 30, ), ); } /** * Implements hook_views_api(). */ function views_bulk_operations_views_api() { return array( 'api' => 3, 'path' => drupal_get_path('module', 'views_bulk_operations') . '/views', ); } /** * Implements hook_theme(). */ function views_bulk_operations_theme() { $themes = array( 'views_bulk_operations_select_all' => array( 'variables' => array('view' => NULL, 'enable_select_all_pages' => TRUE), ), 'views_bulk_operations_confirmation' => array( 'variables' => array('rows' => NULL, 'vbo' => NULL, 'operation' => NULL, 'select_all_pages' => FALSE), ), ); $files = views_bulk_operations_load_action_includes(); foreach ($files as $filename) { $action_theme_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_theme'; if (function_exists($action_theme_fn)) { $themes += call_user_func($action_theme_fn); } } return $themes; } /** * Implements hook_ctools_plugin_type(). */ function views_bulk_operations_ctools_plugin_type() { return array( 'operation_types' => array( 'classes' => array( 'handler', ), ), ); } /** * Implements hook_ctools_plugin_directory(). */ function views_bulk_operations_ctools_plugin_directory($module, $plugin) { if ($module == 'views_bulk_operations') { return 'plugins/' . $plugin; } } /** * Fetch metadata for a specific operation type plugin. * * @param $operation_type * Name of the plugin. * * @return * An array with information about the requested operation type plugin. */ function views_bulk_operations_get_operation_type($operation_type) { ctools_include('plugins'); return ctools_get_plugins('views_bulk_operations', 'operation_types', $operation_type); } /** * Fetch metadata for all operation type plugins. * * @return * An array of arrays with information about all available operation types. */ function views_bulk_operations_get_operation_types() { ctools_include('plugins'); return ctools_get_plugins('views_bulk_operations', 'operation_types'); } /** * Gets the info array of an operation from the provider plugin. * * @param $operation_id * The id of the operation for which the info shall be returned, or NULL * to return an array with info about all operations. */ function views_bulk_operations_get_operation_info($operation_id = NULL) { $operations = &drupal_static(__FUNCTION__); if (!isset($operations)) { $operations = array(); $plugins = views_bulk_operations_get_operation_types(); foreach ($plugins as $plugin) { $operations += $plugin['list callback'](); } uasort($operations, create_function('$a, $b', 'return strcasecmp($a["label"], $b["label"]);')); } if (!empty($operation_id)) { return $operations[$operation_id]; } else { return $operations; } } /** * Returns an operation instance. * * @param $operation_id * The id of the operation to instantiate. * For example: action::node_publish_action. * @param $entity_type * The entity type on which the operation operates. * @param $options * Options for this operation (label, operation settings, etc.) */ function views_bulk_operations_get_operation($operation_id, $entity_type, $options) { $operations = &drupal_static(__FUNCTION__); if (!isset($operations[$operation_id])) { // Intentionally not using views_bulk_operations_get_operation_info() here // since it's an expensive function that loads all the operations on the // system, despite the fact that we might only need a few. $id_fragments = explode('::', $operation_id); $plugin = views_bulk_operations_get_operation_type($id_fragments[0]); $operation_info = $plugin['list callback']($operation_id); if ($operation_info) { $operations[$operation_id] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options); } else { $operations[$operation_id] = FALSE; } } return $operations[$operation_id]; } /** * Get all operations that match the current entity type. * * @param $entity_type * Entity type. * @param $options * An array of options for all operations, in the form of * $operation_id => $operation_options. */ function views_bulk_operations_get_applicable_operations($entity_type, $options) { $operations = array(); foreach (views_bulk_operations_get_operation_info() as $operation_id => $operation_info) { if ($operation_info['type'] == $entity_type || $operation_info['type'] == 'entity' || $operation_info['type'] == 'system') { $options[$operation_id] = !empty($options[$operation_id]) ? $options[$operation_id] : array(); $operations[$operation_id] = views_bulk_operations_get_operation($operation_id, $entity_type, $options[$operation_id]); } } return $operations; } /** * Gets the VBO field if it exists on the passed-in view. * * @return * The field object if found. Otherwise, FALSE. */ function _views_bulk_operations_get_field($view) { foreach ($view->field as $field_name => $field) { if ($field instanceof views_bulk_operations_handler_field_operations) { // Add in the view object for convenience. $field->view = $view; return $field; } } return FALSE; } /** * Implements hook_views_form_substitutions(). */ function views_bulk_operations_views_form_substitutions() { // Views check_plains the column label, so VBO needs to do the same // in order for the replace operation to succeed. $select_all_placeholder = check_plain(''); $select_all = array( '#type' => 'checkbox', '#default_value' => FALSE, '#attributes' => array('class' => array('vbo-table-select-all')), ); return array( $select_all_placeholder => drupal_render($select_all), ); } /** * Implements hook_form_alter(). */ function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) { if (strpos($form_id, 'views_form_') === 0) { $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]); } // Not a VBO-enabled views form. if (empty($vbo)) { return; } // Add basic VBO functionality. if ($form_state['step'] == 'views_form_views_form') { // The submit button added by Views Form API might be used by a non-VBO Views // Form handler. If there's no such handler on the view, hide the button. $has_other_views_form_handlers = FALSE; foreach ($vbo->view->field as $field) { if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) { if (!($field instanceof views_bulk_operations_handler_field_operations)) { $has_other_views_form_handlers = TRUE; } } } if (!$has_other_views_form_handlers) { $form['actions']['#access'] = FALSE; } // The VBO field is excluded from display, stop here. if (!empty($vbo->options['exclude'])) { return; } $form = views_bulk_operations_form($form, $form_state, $vbo); } // Cache the built form to prevent it from being rebuilt prior to validation // and submission, which could lead to data being processed incorrectly, // because the views rows (and thus, the form elements as well) have changed // in the meantime. Matching views issue: http://drupal.org/node/1473276. $form_state['cache'] = TRUE; if (empty($vbo->view->override_url)) { // If the VBO view is embedded using views_embed_view(), or in a block, // $view->get_url() doesn't point to the current page, which means that // the form doesn't get processed. if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) { $vbo->view->override_url = $_GET['q']; // We are changing the override_url too late, the form action was already // set by Views to the previous URL, so it needs to be overriden as well. $query = drupal_get_query_parameters($_GET, array('q')); $form['#action'] = url($_GET['q'], array('query' => $query)); } } // Give other modules a chance to alter the form. drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo); } /** * Implements hook_views_post_build(). * * Hides the VBO field if no operations are available. * This causes the entire VBO form to be hidden. * * @see views_bulk_operations_form_alter(). */ function views_bulk_operations_views_post_build(&$view) { $vbo = _views_bulk_operations_get_field($view); if ($vbo && count($vbo->get_selected_operations()) < 1) { $vbo->options['exclude'] = TRUE; } } /** * Returns the 'select all' div that gets inserted below the table header row * (for table style plugins with grouping disabled), or above the view results * (for non-table style plugins), providing a choice between selecting items * on the current page, and on all pages. * * The actual insertion is done by JS, matching the degradation behavior * of Drupal core (no JS - no select all). */ function theme_views_bulk_operations_select_all($variables) { $view = $variables['view']; $enable_select_all_pages = $variables['enable_select_all_pages']; $form = array(); if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) { if (!$enable_select_all_pages) { return ''; } $wrapper_class = 'vbo-table-select-all-markup'; $this_page_count = format_plural(count($view->result), '1 row', '@count rows'); $this_page = t('Selected !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 ($enable_select_all_pages) { $form['select_all']['or'] = array( '#type' => 'markup', '#markup' => '' . t('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']['js'][] = array( 'data' => array('vbo' => array( 'row_clickable' => $vbo->get_vbo_option('row_clickable'), )), 'type' => 'setting', ); $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css'; // Wrap the form in a div with specific classes for JS targeting and theming. $class = 'vbo-views-form'; if (empty($vbo->view->result)) { $class .= ' vbo-views-form-empty'; } $form['#prefix'] = '
'; $form['#suffix'] = '
'; // Force browser to reload the page if Back is hit. if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) { drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+ } else { drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers } // Set by JS to indicate that all rows on all pages are selected. $form['select_all'] = array( '#type' => 'hidden', '#attributes' => array('class' => 'select-all-rows'), '#default_value' => FALSE, ); $form['select'] = array( '#type' => 'fieldset', '#title' => t('Operations'), '#collapsible' => FALSE, '#attributes' => array('class' => array('container-inline')), ); if ($vbo->get_vbo_option('display_type') == 0) { $options = array(0 => t('- Choose an operation -')); foreach ($vbo->get_selected_operations() as $operation_id => $operation) { $options[$operation_id] = $operation->label(); } // Create dropdown and submit button. $form['select']['operation'] = array( '#type' => 'select', '#options' => $options, ); $form['select']['submit'] = array( '#type' => 'submit', '#value' => t('Execute'), '#validate' => array('views_bulk_operations_form_validate'), '#submit' => array('views_bulk_operations_form_submit'), ); } else { // Create buttons for operations. foreach ($vbo->get_selected_operations() as $operation_id => $operation) { $form['select'][$operation_id] = array( '#type' => 'submit', '#value' => $operation->label(), '#validate' => array('views_bulk_operations_form_validate'), '#submit' => array('views_bulk_operations_form_submit'), '#operation_id' => $operation_id, ); } } // Adds the "select all" functionality if the view has results. // If the view is using a table style plugin, the markup gets moved to // a table row below the header. // If we are using radio buttons, we don't use select all at all. if (!empty($vbo->view->result) && !$vbo->get_vbo_option('force_single')) { $enable_select_all_pages = FALSE; // If the view is paginated, and "select all items on all pages" is // enabled, tell that to the theme function. if (count($vbo->view->result) != $vbo->view->total_rows && $vbo->get_vbo_option('enable_select_all_pages')) { $enable_select_all_pages = TRUE; } $form['select_all_markup'] = array( '#type' => 'markup', '#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view, 'enable_select_all_pages' => $enable_select_all_pages)), ); } return $form; } /** * Validation callback for the first step of the VBO form. */ function views_bulk_operations_form_validate($form, &$form_state) { $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]); if (!empty($form_state['triggering_element']['#operation_id'])) { $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id']; } if (!$form_state['values']['operation']) { form_set_error('operation', t('No operation selected. Please select an operation to perform.')); } $field_name = $vbo->options['id']; $selection = _views_bulk_operations_get_selection($vbo, $form_state); if (!$selection) { form_set_error($field_name, t('Please select at least one item.')); } } /** * Multistep form callback for the "configure" step. */ function views_bulk_operations_config_form($form, &$form_state, $view, $output) { $vbo = _views_bulk_operations_get_field($view); $operation = $form_state['operation']; drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH); $context = array( 'entity_type' => $vbo->get_entity_type(), // Pass the View along. // Has no performance penalty since objects are passed by reference, // but needing the full views object in a core action is in most cases // a sign of a wrong implementation. Do it only if you have to. 'view' => $view, ); $form += $operation->form($form, $form_state, $context); $query = drupal_get_query_parameters($_GET, array('q')); $form['actions'] = array( '#type' => 'container', '#attributes' => array('class' => array('form-actions')), '#weight' => 999, ); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Next'), '#validate' => array('views_bulk_operations_config_form_validate'), '#submit' => array('views_bulk_operations_form_submit'), '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)), ); return $form; } /** * Validation callback for the "configure" step. * Gives the operation a chance to validate its config form. */ function views_bulk_operations_config_form_validate($form, &$form_state) { $operation = &$form_state['operation']; $operation->formValidate($form, $form_state); } /** * Multistep form callback for the "confirm" step. */ function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) { $vbo = _views_bulk_operations_get_field($view); $operation = $form_state['operation']; $rows = $form_state['selection']; $query = drupal_get_query_parameters($_GET, array('q')); $title = t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation->label())); $form = confirm_form($form, $title, array('path' => $view->get_url(), 'query' => $query), theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo, 'operation' => $operation, 'select_all_pages' => $form_state['select_all_pages'])) ); // Add VBO's submit handler to the Confirm button added by config_form(). $form['actions']['submit']['#submit'] = array('views_bulk_operations_form_submit'); // We can't set the View title here as $view is just a copy of the original, // and our settings changes won't "stick" for the first page load of the // confirmation form. We also can't just call drupal_set_title() directly // because our title will be clobbered by the actual View title later. So // let's tuck the title away in the form for use later. // @see views_bulk_operations_preprocess_views_view() $form['#vbo_confirm_form_title'] = $title; return $form; } /** * Theme function to show the confirmation page before executing the operation. */ function theme_views_bulk_operations_confirmation($variables) { $select_all_pages = $variables['select_all_pages']; $vbo = $variables['vbo']; $entity_type = $vbo->get_entity_type(); $rows = $variables['rows']; $items = array(); // Load the entities from the current page, and show their titles. $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision); foreach ($entities as $entity) { $items[] = check_plain(entity_label($entity_type, $entity)); } // All rows on all pages have been selected, so show a count of additional items. if ($select_all_pages) { $more_count = $vbo->view->total_rows - count($vbo->view->result); $items[] = t('...and !count more.', array('!count' => $more_count)); } $count = format_plural(count($entities), 'item', '@count items'); $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following !count:', array('!count' => $count)))); return $output; } /** * Implements hook_preprocess_page(). * * Hide action links on the configure and confirm pages. */ function views_bulk_operations_preprocess_page(&$variables) { if (isset($_POST['select_all'], $_POST['operation'])) { $variables['action_links'] = array(); } } /** * Implements hook_preprocess_views_view(). */ function views_bulk_operations_preprocess_views_view($variables) { // If we've stored a title for the confirmation form, retrieve it here and // retitle the View. // @see views_bulk_operations_confirm_form() if (array_key_exists('rows', $variables) && is_array($variables['rows']) && array_key_exists('#vbo_confirm_form_title', $variables['rows'])) { $variables['view']->set_title($variables['rows']['#vbo_confirm_form_title']); } } /** * Goes through the submitted values, and returns * an array of selected rows, in the form of * $row_index => $entity_id. */ function _views_bulk_operations_get_selection($vbo, $form_state) { $selection = array(); $field_name = $vbo->options['id']; if (!empty($form_state['values'][$field_name])) { // If using "force single", the selection needs to be converted to an array. if (is_array($form_state['values'][$field_name])) { $selection = array_filter($form_state['values'][$field_name]); } else { $selection = array($form_state['values'][$field_name]); } } return $selection; } /** * Submit handler for all steps of the VBO multistep form. */ function views_bulk_operations_form_submit($form, &$form_state) { $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]); $entity_type = $vbo->get_entity_type(); switch ($form_state['step']) { case 'views_form_views_form': $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state); $form_state['select_all_pages'] = $form_state['values']['select_all']; $options = $vbo->get_operation_options($form_state['values']['operation']); $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options); if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) { break; // Go directly to execution } $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form'; $form_state['rebuild'] = TRUE; return; case 'views_bulk_operations_config_form': $form_state['step'] = 'views_bulk_operations_confirm_form'; $operation = &$form_state['operation']; $operation->formSubmit($form, $form_state); if ($operation->getAdminOption('skip_confirmation')) { break; // Go directly to execution } $form_state['rebuild'] = TRUE; return; case 'views_bulk_operations_confirm_form': break; } // Execute the operation. views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']); // Redirect. $query = drupal_get_query_parameters($_GET, array('q')); $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query)); } /** * Entry point for executing the chosen operation upon selected rows. * * If the selected operation is an aggregate operation (requiring all selected * items to be passed at the same time), restricted to a single value, or has * the skip_batching option set, the operation is executed directly. * This means that there is no batching & queueing, the PHP execution * time limit is ignored (if allowed), all selected entities are loaded and * processed. * * Otherwise, the selected entity ids are divided into groups not larger than * $entity_load_capacity, and enqueued for processing. * If all items on all pages should be processed, a batch job runs that * collects and enqueues the items from all pages of the view, page by page. * * Based on the "Enqueue the operation instead of executing it directly" * VBO field setting, the newly filled queue is either processed at cron * time by the VBO worker function, or right away in a new batch job. * * @param $vbo * The VBO field, containing a reference to the view in $vbo->view. * @param $operation * The operation object. * @param $selection * An array in the form of $row_index => $entity_id. * @param $select_all_pages * Whether all items on all pages should be selected. */ function views_bulk_operations_execute($vbo, $operation, $selection, $select_all_pages = FALSE) { global $user; // Determine if the operation needs to be executed directly. $aggregate = $operation->aggregate(); $skip_batching = $vbo->get_vbo_option('skip_batching'); $force_single = $vbo->get_vbo_option('force_single'); $execute_directly = ($aggregate || $skip_batching || $force_single); // Try to load all rows without a batch if needed. if ($execute_directly && $select_all_pages) { views_bulk_operations_direct_adjust($selection, $vbo); } // Options that affect execution. $options = array( 'revision' => $vbo->revision, 'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10), // The information needed to recreate the view, to avoid serializing the // whole object. Passed to the executed operation. Also used by // views_bulk_operations_adjust_selection(). 'view_info' => array( 'name' => $vbo->view->name, 'display' => $vbo->view->current_display, 'arguments' => $vbo->view->args, 'exposed_input' => $vbo->view->get_exposed_input(), ), ); // Create an array of rows in the needed format. $rows = array(); $current = 1; foreach ($selection as $row_index => $entity_id) { $rows[$row_index] = array( 'entity_id' => $entity_id, 'views_row' => array(), // Some operations rely on knowing the position of the current item // in the execution set (because of specific things that need to be done // at the beginning or the end of the set). 'position' => array( 'current' => $current++, 'total' => count($selection), ), ); // Some operations require full selected rows. if ($operation->needsRows()) { $rows[$row_index]['views_row'] = $vbo->view->result[$row_index]; } } if ($execute_directly) { // Execute the operation directly and stop here. views_bulk_operations_direct_process($operation, $rows, $options); return; } // Determine the correct queue to use. if ($operation->getAdminOption('postpone_processing')) { // Use the site queue processed on cron. $queue_name = 'views_bulk_operations'; } else { // Use the active queue processed immediately by Batch API. $queue_name = 'views_bulk_operations_active_queue_' . db_next_id(); } $batch = array( 'operations' => array(), 'finished' => 'views_bulk_operations_execute_finished', 'progress_message' => '', 'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())), ); // All items on all pages should be selected, add a batch job to gather // and enqueue them. if ($select_all_pages && $vbo->view->query->pager->has_more_records()) { $total_rows = $vbo->view->total_rows; $batch['operations'][] = array( 'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options), ); } else { $total_rows = count($rows); // We have all the items that we need, enqueue them right away. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options); // Provide a status message to the user, since this is the last step if // processing is postponed. if ($operation->getAdminOption('postpone_processing')) { drupal_set_message(t('Enqueued the selected operation (%operation).', array( '%operation' => $operation->label(), ))); } } // Processing is not postponed, add a batch job to process the queue. if (!$operation->getAdminOption('postpone_processing')) { $batch['operations'][] = array( 'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows), ); } // If there are batch jobs to be processed, create the batch set. if (count($batch['operations'])) { batch_set($batch); } } /** * Batch API callback: loads the view page by page and enqueues all items. * * @param $queue_name * The name of the queue to which the items should be added. * @param $operation * The operation object. * @param $options * An array of options that affect execution (revision, entity_load_capacity, * view_info). Passed along with each new queue item. */ function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) { if (!isset($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = 0; } $view_info = $options['view_info']; $view = views_get_view($view_info['name']); $view->set_exposed_input($view_info['exposed_input']); $view->set_arguments($view_info['arguments']); $view->set_display($view_info['display']); $view->set_offset($context['sandbox']['progress']); $view->build(); $view->execute($view_info['display']); // Note the total number of rows. if (empty($context['sandbox']['max'])) { $context['sandbox']['max'] = $view->total_rows; } $vbo = _views_bulk_operations_get_field($view); $rows = array(); foreach ($view->result as $row_index => $result) { $rows[$row_index] = array( 'entity_id' => $vbo->get_value($result), 'views_row' => array(), 'position' => array( 'current' => ++$context['sandbox']['progress'], 'total' => $context['sandbox']['max'], ), ); // Some operations require full selected rows. if ($operation->needsRows()) { $rows[$row_index]['views_row'] = $result; } } // Enqueue the gathered rows. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options); if ($context['sandbox']['progress'] != $context['sandbox']['max']) { // Provide an estimation of the completion level we've reached. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max'])); } else { // Provide a status message to the user if this is the last batch job. if ($operation->getAdminOption('postpone_processing')) { $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array( '%operation' => $operation->label(), )); } } } /** * Divides the passed rows into groups and enqueues each group for processing * * @param $queue_name * The name of the queue. * @param $rows * The rows to be enqueued. * @param $operation * The object representing the current operation. * Passed along with each new queue item. * @param $options * An array of options that affect execution (revision, entity_load_capacity). * Passed along with each new queue item. */ function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) { global $user; $queue = DrupalQueue::get($queue_name, TRUE); $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE); foreach ($row_groups as $row_group) { $entity_ids = array(); foreach ($row_group as $row) { $entity_ids[] = $row['entity_id']; } $job = array( 'title' => t('Perform %operation on @type !entity_ids.', array( '%operation' => $operation->label(), '@type' => $operation->entityType, '!entity_ids' => implode(',', $entity_ids), )), 'uid' => $user->uid, 'arguments' => array($row_group, $operation, $options), ); $queue->createItem($job); } } /** * Batch API callback: processes the active queue. * * @param $queue_name * The name of the queue to process. * @param $operation * The object representing the current operation. * @param $total_rows * The total number of processable items (across all queue items), used * to report progress. * * @see views_bulk_operations_queue_item_process() */ function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) { static $queue; // It is still possible to hit the time limit. drupal_set_time_limit(0); // Prepare the sandbox. if (!isset($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = $total_rows; $context['results']['log'] = array(); } // Instantiate the queue. if (!isset($queue)) { $queue = DrupalQueue::get($queue_name, TRUE); } // Process the queue as long as it has items for us. $queue_item = $queue->claimItem(3600); if ($queue_item) { // Process the queue item, and update the progress count. views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']); $queue->deleteItem($queue_item); // Provide an estimation of the completion level we've reached. $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]); $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max'])); } if (!$queue_item || $context['finished'] === 1) { // All done. Provide a status message to the user. $context['results']['log'][] = t('Performed %operation on @items.', array( '%operation' => $operation->label(), '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'), )); } } /** * Processes the provided queue item. * * Used as a worker callback defined by views_bulk_operations_cron_queue_info() * to process the site queue, as well as by * views_bulk_operations_active_queue_process() to process the active queue. * * @param $queue_item_arguments * The arguments of the queue item to process. * @param $log * An injected array of log messages, to be modified by reference. * If NULL, the function defaults to using watchdog. */ function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) { list($row_group, $operation, $options) = $queue_item_data['arguments']; $account = user_load($queue_item_data['uid']); $entity_type = $operation->entityType; $entity_ids = array(); foreach ($row_group as $row_index => $row) { $entity_ids[] = $row['entity_id']; } $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']); foreach ($row_group as $row_index => $row) { $entity_id = $row['entity_id']; // A matching entity couldn't be loaded. Skip this item. if (!isset($entities[$entity_id])) { continue; } if ($options['revision']) { // Don't reload revisions for now, they are not statically cached and // usually don't run into the edge case described below. $entity = $entities[$entity_id]; } else { // A previous action might have resulted in the entity being resaved // (e.g. node synchronization from a prior node in this batch), so try // to reload it. If no change occurred, the entity will be retrieved // from the static cache, resulting in no performance penalty. $entity = entity_load_single($entity_type, $entity_id); if (empty($entity)) { // The entity is no longer valid. continue; } } // If the current entity can't be accessed, skip it and log a notice. if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) { $message = 'Skipped %operation on @type %title due to insufficient permissions.'; $arguments = array( '%operation' => $operation->label(), '@type' => $entity_type, '%title' => entity_label($entity_type, $entity), ); if ($log) { $log[] = t($message, $arguments); } else { watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT); } continue; } $operation_context = array( 'progress' => $row['position'], 'view_info' => $options['view_info'], ); if ($operation->needsRows()) { $operation_context['rows'] = array($row_index => $row['views_row']); } $operation->execute($entity, $operation_context); unset($row_group[$row_index]); } } /** * Adjusts the selection for the direct execution method. * * Just like the direct method itself, this is legacy code, used only for * aggregate actions. */ function views_bulk_operations_direct_adjust(&$selection, $vbo) { // Adjust selection to select all rows across pages. $view = views_get_view($vbo->view->name); $view->set_exposed_input($vbo->view->get_exposed_input()); $view->set_arguments($vbo->view->args); $view->set_display($vbo->view->current_display); $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array())); $view->build(); // Unset every field except the VBO one (which holds the entity id). // That way the performance hit becomes much smaller, because there is no // chance of views_handler_field_field::post_execute() firing entity_load(). foreach ($view->field as $field_name => $field) { if ($field_name != $vbo->options['id']) { unset($view->field[$field_name]); } } $view->execute($vbo->view->current_display); $results = array(); foreach ($view->result as $row_index => $result) { $results[$row_index] = $vbo->get_value($result); } $selection = $results; } /** * Processes the passed rows directly (without batching and queueing). */ function views_bulk_operations_direct_process($operation, $rows, $options) { global $user; drupal_set_time_limit(0); // Prepare an array of status information. Imitates the Batch API naming // for consistency. Passed to views_bulk_operations_execute_finished(). $context = array(); $context['results']['progress'] = 0; $context['results']['log'] = array(); if ($operation->aggregate()) { // Load all entities. $entity_type = $operation->entityType; $entity_ids = array(); foreach ($rows as $row_index => $row) { $entity_ids[] = $row['entity_id']; } $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']); // Filter out entities that can't be accessed. foreach ($entities as $id => $entity) { 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' => entity_label($entity_type, $entity), )); unset($entities[$id]); } } // If there are any entities left, execute the operation on them. if ($entities) { $operation_context = array( 'view_info' => $options['view_info'], ); // Pass the selected rows to the operation if needed. if ($operation->needsRows()) { $operation_context['rows'] = array(); foreach ($rows as $row_index => $row) { $operation_context['rows'][$row_index] = $row['views_row']; } } $operation->execute($entities, $operation_context); } } else { // Imitate a queue and process the entities one by one. $queue_item_data = array( 'uid' => $user->uid, 'arguments' => array($rows, $operation, $options), ); views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']); } $context['results']['progress'] += count($rows); $context['results']['log'][] = t('Performed %operation on @items.', array( '%operation' => $operation->label(), '@items' => format_plural(count($rows), '1 item', '@count items'), )); views_bulk_operations_execute_finished(TRUE, $context['results'], array()); } /** * Helper function that runs after the execution process is complete. */ function views_bulk_operations_execute_finished($success, $results, $operations) { if ($success) { if (count($results['log']) > 1) { $message = theme('item_list', array('items' => $results['log'])); } else { $message = reset($results['log']); } } else { // An error occurred. // $operations contains the operations that remained unprocessed. $error_operation = reset($operations); $message = t('An error occurred while processing @operation with arguments: @arguments', array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE))); } _views_bulk_operations_log($message); } /** * Helper function to verify access permission to operate on an entity. */ function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) { if (!entity_type_supports($entity_type, 'access')) { return TRUE; } $access_ops = array( VBO_ACCESS_OP_VIEW => 'view', VBO_ACCESS_OP_UPDATE => 'update', VBO_ACCESS_OP_CREATE => 'create', VBO_ACCESS_OP_DELETE => 'delete', ); foreach ($access_ops as $bit => $op) { if ($operation->getAccessMask() & $bit) { if (!entity_access($op, $entity_type, $entity, $account)) { return FALSE; } } } return TRUE; } /** * Loads multiple entities by their entity or revision ids, and returns them, * keyed by the id used for loading. */ function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) { if (!$revision) { $entities = entity_load($entity_type, $ids); } else { // D7 can't load multiple entities by revision_id. Lovely. $info = entity_get_info($entity_type); $entities = array(); foreach ($ids as $revision_id) { $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id)); $entities[$revision_id] = reset($loaded_entities); } } return $entities; } /** * Helper function to report an error. */ function _views_bulk_operations_report_error($msg, $arg) { watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR); if (function_exists('drush_set_error')) { drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg))); } } /** * Display a message to the user through the relevant function. */ function _views_bulk_operations_log($msg) { // Is VBO being run through drush? if (function_exists('drush_log')) { drush_log(strip_tags($msg), 'ok'); } else { drupal_set_message($msg); } }