summaryrefslogtreecommitdiff
path: root/sites/all/modules/views_bulk_operations/views_bulk_operations.module
diff options
context:
space:
mode:
Diffstat (limited to 'sites/all/modules/views_bulk_operations/views_bulk_operations.module')
-rw-r--r--sites/all/modules/views_bulk_operations/views_bulk_operations.module1298
1 files changed, 1298 insertions, 0 deletions
diff --git a/sites/all/modules/views_bulk_operations/views_bulk_operations.module b/sites/all/modules/views_bulk_operations/views_bulk_operations.module
new file mode 100644
index 000000000..e74eeb1df
--- /dev/null
+++ b/sites/all/modules/views_bulk_operations/views_bulk_operations.module
@@ -0,0 +1,1298 @@
+<?php
+
+/**
+ * @file
+ * Allows operations to be performed on items selected in a view.
+ */
+
+// Access operations.
+define('VBO_ACCESS_OP_VIEW', 0x01);
+define('VBO_ACCESS_OP_UPDATE', 0x02);
+define('VBO_ACCESS_OP_CREATE', 0x04);
+define('VBO_ACCESS_OP_DELETE', 0x08);
+
+/**
+ * Implements hook_action_info().
+ * Registers custom VBO actions as Drupal actions.
+ */
+function views_bulk_operations_action_info() {
+ $actions = array();
+ $files = views_bulk_operations_load_action_includes();
+ foreach ($files as $filename) {
+ $action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_info';
+ $action_info = call_user_func($action_info_fn);
+ if (is_array($action_info)) {
+ $actions += $action_info;
+ }
+ }
+
+ return $actions;
+}
+
+/**
+ * Loads the VBO actions placed in their own include files (under actions/).
+ *
+ * @return
+ * An array of containing filenames of the included actions.
+ */
+function views_bulk_operations_load_action_includes() {
+ static $loaded = FALSE;
+
+ // The list of VBO actions is fairly static, so it's hardcoded for better
+ // performance (hitting the filesystem with file_scan_directory(), and then
+ // caching the result has its cost).
+ $files = array(
+ 'archive.action',
+ 'argument_selector.action',
+ 'book.action',
+ 'delete.action',
+ 'modify.action',
+ 'script.action',
+ 'user_roles.action',
+ 'user_cancel.action',
+ );
+
+ if (!$loaded) {
+ foreach ($files as $file) {
+ module_load_include('inc', 'views_bulk_operations', 'actions/' . $file);
+ }
+ $loaded = TRUE;
+ }
+
+ return $files;
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * Deletes queue items belonging to VBO active queues (used by VBO's batches)
+ * that are older than a day (since they can only be a result of VBO crashing
+ * or the execution being interrupted in some other way). This is the interval
+ * used to cleanup batches in system_cron(), so it can't be increased.
+ *
+ * Note: This code is specific to SystemQueue. Other queue implementations will
+ * need to do their own garbage collection.
+ */
+function views_bulk_operations_cron() {
+ db_delete('queue')
+ ->condition('name', db_like('views_bulk_operations_active_queue_'), 'LIKE')
+ ->condition('created', REQUEST_TIME - 86400, '<')
+ ->execute();
+}
+
+/**
+ * Implements of hook_cron_queue_info().
+ */
+function views_bulk_operations_cron_queue_info() {
+ return array(
+ 'views_bulk_operations' => array(
+ 'worker callback' => 'views_bulk_operations_queue_item_process',
+ 'time' => 30,
+ ),
+ );
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function views_bulk_operations_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
+ );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function views_bulk_operations_theme() {
+ $themes = array(
+ 'views_bulk_operations_select_all' => array(
+ 'variables' => array('view' => NULL, 'enable_select_all_pages' => TRUE),
+ ),
+ '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('<!--views-bulk-operations-select-all-->');
+ $select_all = array(
+ '#type' => 'checkbox',
+ '#default_value' => FALSE,
+ '#attributes' => array('class' => array('vbo-table-select-all')),
+ );
+
+ return array(
+ $select_all_placeholder => drupal_render($select_all),
+ );
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
+ if (strpos($form_id, 'views_form_') === 0) {
+ $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
+ }
+ // Not a VBO-enabled views form.
+ if (empty($vbo)) {
+ return;
+ }
+
+ // Add basic VBO functionality.
+ if ($form_state['step'] == 'views_form_views_form') {
+ // The submit button added by Views Form API might be used by a non-VBO Views
+ // Form handler. If there's no such handler on the view, hide the button.
+ $has_other_views_form_handlers = FALSE;
+ foreach ($vbo->view->field as $field) {
+ if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
+ if (!($field instanceof views_bulk_operations_handler_field_operations)) {
+ $has_other_views_form_handlers = TRUE;
+ }
+ }
+ }
+ if (!$has_other_views_form_handlers) {
+ $form['actions']['#access'] = FALSE;
+ }
+ // The VBO field is excluded from display, stop here.
+ if (!empty($vbo->options['exclude'])) {
+ return;
+ }
+
+ $form = views_bulk_operations_form($form, $form_state, $vbo);
+ }
+
+ // Cache the built form to prevent it from being rebuilt prior to validation
+ // and submission, which could lead to data being processed incorrectly,
+ // because the views rows (and thus, the form elements as well) have changed
+ // in the meantime. Matching views issue: http://drupal.org/node/1473276.
+ $form_state['cache'] = TRUE;
+
+ if (empty($vbo->view->override_url)) {
+ // If the VBO view is embedded using views_embed_view(), or in a block,
+ // $view->get_url() doesn't point to the current page, which means that
+ // the form doesn't get processed.
+ if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
+ $vbo->view->override_url = $_GET['q'];
+ // We are changing the override_url too late, the form action was already
+ // set by Views to the previous URL, so it needs to be overriden as well.
+ $query = drupal_get_query_parameters($_GET, array('q'));
+ $form['#action'] = url($_GET['q'], array('query' => $query));
+ }
+ }
+
+ // Give other modules a chance to alter the form.
+ drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
+}
+
+/**
+ * Implements hook_views_post_build().
+ *
+ * Hides the VBO field if no operations are available.
+ * This causes the entire VBO form to be hidden.
+ *
+ * @see views_bulk_operations_form_alter().
+ */
+function views_bulk_operations_views_post_build(&$view) {
+ $vbo = _views_bulk_operations_get_field($view);
+ if ($vbo && count($vbo->get_selected_operations()) < 1) {
+ $vbo->options['exclude'] = TRUE;
+ }
+}
+
+/**
+ * Returns the 'select all' div that gets inserted below the table header row
+ * (for table style plugins with grouping disabled), or above the view results
+ * (for non-table style plugins), providing a choice between selecting items
+ * on the current page, and on all pages.
+ *
+ * The actual insertion is done by JS, matching the degradation behavior
+ * of Drupal core (no JS - no select all).
+ */
+function theme_views_bulk_operations_select_all($variables) {
+ $view = $variables['view'];
+ $enable_select_all_pages = $variables['enable_select_all_pages'];
+ $form = array();
+
+ if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
+ if (!$enable_select_all_pages) {
+ return '';
+ }
+
+ $wrapper_class = 'vbo-table-select-all-markup';
+ $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
+ $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
+ $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
+ $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));
+
+ $form['select_all_pages'] = array(
+ '#type' => 'button',
+ '#attributes' => array('class' => array('vbo-table-select-all-pages')),
+ '#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
+ '#prefix' => '<span class="vbo-table-this-page">' . $this_page . ' &nbsp;',
+ '#suffix' => '</span>',
+ );
+ $form['select_this_page'] = array(
+ '#type' => 'button',
+ '#attributes' => array('class' => array('vbo-table-select-this-page')),
+ '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
+ '#prefix' => '<span class="vbo-table-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
+ '#suffix' => '</span>',
+ );
+ }
+ 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' => '<em>' . t('OR') . '</em>',
+ );
+ $form['select_all']['all_pages'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Select all items on all pages'),
+ '#default_value' => '',
+ '#attributes' => array('class' => array('vbo-select-all-pages')),
+ );
+ }
+ }
+
+ $output = '<div class="' . $wrapper_class . '">';
+ $output .= drupal_render($form);
+ $output .= '</div>';
+
+ return $output;
+}
+
+/**
+ * Extend the views_form multistep form with elements for executing an operation.
+ */
+function views_bulk_operations_form($form, &$form_state, $vbo) {
+ $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
+ $form['#attached']['js'][] = array(
+ 'data' => array('vbo' => array(
+ 'row_clickable' => $vbo->get_vbo_option('row_clickable'),
+ )),
+ 'type' => 'setting',
+ );
+
+ $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
+ // Wrap the form in a div with specific classes for JS targeting and theming.
+ $class = 'vbo-views-form';
+ if (empty($vbo->view->result)) {
+ $class .= ' vbo-views-form-empty';
+ }
+ $form['#prefix'] = '<div class="' . $class . '">';
+ $form['#suffix'] = '</div>';
+
+ // Force browser to reload the page if Back is hit.
+ if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
+ drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+
+ }
+ else {
+ drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers
+ }
+
+ // Set by JS to indicate that all rows on all pages are selected.
+ $form['select_all'] = array(
+ '#type' => 'hidden',
+ '#attributes' => array('class' => 'select-all-rows'),
+ '#default_value' => FALSE,
+ );
+ $form['select'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Operations'),
+ '#collapsible' => FALSE,
+ '#attributes' => array('class' => array('container-inline')),
+ );
+ if ($vbo->get_vbo_option('display_type') == 0) {
+ $options = array(0 => t('- Choose an operation -'));
+ foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
+ $options[$operation_id] = $operation->label();
+ }
+
+ // Create dropdown and submit button.
+ $form['select']['operation'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ );
+ $form['select']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Execute'),
+ '#validate' => array('views_bulk_operations_form_validate'),
+ '#submit' => array('views_bulk_operations_form_submit'),
+ );
+ }
+ else {
+ // Create buttons for operations.
+ foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
+ $form['select'][$operation_id] = array(
+ '#type' => 'submit',
+ '#value' => $operation->label(),
+ '#validate' => array('views_bulk_operations_form_validate'),
+ '#submit' => array('views_bulk_operations_form_submit'),
+ '#operation_id' => $operation_id,
+ );
+ }
+ }
+
+ // Adds the "select all" functionality if the view has results.
+ // If the view is using a table style plugin, the markup gets moved to
+ // a table row below the header.
+ // If we are using radio buttons, we don't use select all at all.
+ if (!empty($vbo->view->result) && !$vbo->get_vbo_option('force_single')) {
+ $enable_select_all_pages = FALSE;
+ // If the view is paginated, and "select all items on all pages" is
+ // enabled, tell that to the theme function.
+ if (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 <strong>!count</strong> 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 <strong>!count</strong>:', 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);
+ }
+}