$vid), WATCHDOG_ERROR); return; } $vd = new views_bulk_operations_destructor($view); // this will take care of calling $view->destroy() on exit. // Find the view display that has the VBO style. $found = FALSE; foreach (array_keys($view->display) as $display) { $display_options = &$view->display[$display]->display_options; if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') { $view->set_display($display); $found = TRUE; break; } } if (!$found) { watchdog('vbo', 'Could not find a VBO display in view %vid.', array('%vid' => $vid), WATCHDOG_ERROR); return; } // Execute the view. $view->set_exposed_input($view_exposed_input); $view->set_arguments($view_arguments); $view->set_items_per_page($respect_limit ? $display_options['items_per_page'] : 0); $view->execute(); if (empty($view->result)) { watchdog('vbo', 'No results for view %vid.', array('%vid' => $vid), WATCHDOG_WARNING); return; } // Find the selected operation. $plugin = $view->style_plugin; $operations = $plugin->get_selected_operations(); if (!isset($operations[$operation_callback])) { watchdog('vbo', 'Could not find operation %operation in view %vid.', array('%operation' => $operation_callback, '%vid' => $vid), WATCHDOG_ERROR); return; } $operation = $plugin->get_operation_info($operation_callback); // Execute the operation on the view results. $execution_type = $plugin->options['execution_type']; if ($execution_type == VBO_EXECUTION_BATCH) { $execution_type = VBO_EXECUTION_DIRECT; // we don't yet support Batch API here } _views_bulk_operations_execute( $view, $view->result, $operation, $operation_arguments, array( 'execution_type' => $execution_type, 'display_result' => $plugin->options['display_result'], 'max_performance' => $plugin->options['max_performance'], 'settings' => $operation['options']['settings'], ) ); } /** * API function to add actions to a VBO. */ function views_bulk_operations_add_actions($vid, $actions) { $view = views_get_view($vid); if (!is_object($view)) { watchdog('vbo', 'Could not find view %vid.', array('%vid' => $vid), WATCHDOG_ERROR); return; } // Find the view display that has the VBO style. $found = FALSE; foreach (array_keys($view->display) as $display) { $display_options = &$view->display[$display]->display_options; if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') { $found = TRUE; break; } } if (!$found) { watchdog('vbo', 'Could not find a VBO display in view %vid.', array('%vid' => $vid), WATCHDOG_ERROR); return; } // Iterate on the desired actions. $operations = $display_options['style_options']['operations']; $ignored = $added = array(); if (!empty($actions)) foreach ($actions as $action) { $modified = FALSE; if (is_numeric($action)) { // aid $action_object = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action)); if (is_object($action_object)) { $parameters = unserialize($action_object->parameters); $key = $action_object->callback . (empty($parameters) ? '' : '-'. md5($action_object->parameters)); if (isset($operations[$key])) { // available for this view $display_options['style_options']['operations'][$key]['selected'] = TRUE; $modified = TRUE; } } } else { // callback or title if (isset($operations[$action])) { // callback and available for this view $display_options['style_options']['operations'][$action]['selected'] = TRUE; $modified = TRUE; } else { // try the title $action_object = db_fetch_object(db_query("SELECT * FROM {actions} WHERE description LIKE '%s'", db_escape_string($action))); if (is_object($action_object)) { $parameters = unserialize($action_object->parameters); $key = $action_object->callback . (empty($parameters) ? '' : '-'. md5($action_object->parameters)); if (isset($operations[$key])) { // available for this view $display_options['style_options']['operations'][$key]['selected'] = TRUE; $modified = TRUE; } } } } if ($modified) { $added[] = $action; } else { $ignored[] = $action; } } // Save the view if anything was changed. if (!empty($added)) { $view->save(); views_object_cache_clear('view', $vid); if (empty($ignored)) { watchdog('vbo', 'View %vid was successfully modified. The following actions were added: %added.', array( '%vid' => $vid, '%added' => implode(', ', $added) ), WATCHDOG_INFO); } else { watchdog('vbo', 'View %vid was modified. The following actions were added: %added. The following actions were ignored: %ignored.', array( '%vid' => $vid, '%added' => implode(', ', $added), '%ignored' => implode(', ', $ignored) ), WATCHDOG_WARNING); } } else { watchdog('vbo', 'View %vid was NOT modified. The following actions were ignored: %ignored.', array( '%vid' => $vid, '%ignored' => implode(', ', $ignored) ), WATCHDOG_ERROR); } } // Define the steps in the multistep form that executes operations. define('VBO_STEP_VIEW', 1); define('VBO_STEP_CONFIG', 2); define('VBO_STEP_CONFIRM', 3); define('VBO_STEP_SINGLE', 4); // Types of bulk execution. define('VBO_EXECUTION_DIRECT', 1); define('VBO_EXECUTION_BATCH', 2); define('VBO_EXECUTION_QUEUE', 3); // Types of aggregate actions. define('VBO_AGGREGATE_FORCED', 1); define('VBO_AGGREGATE_FORBIDDEN', 0); define('VBO_AGGREGATE_OPTIONAL', 2); // 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); /** * Implementation of hook_cron_queue_info(). */ function views_bulk_operations_cron_queue_info() { return array( 'views_bulk_operations' => array( 'worker callback' => '_views_bulk_operations_execute_queue', 'time' => 30, ), ); } /** * Implementation of hook_views_api(). */ function views_bulk_operations_views_api() { return array( 'api' => 2.0, ); } /** * Implementation of hook_elements(). */ function views_bulk_operations_elements() { $type['views_node_selector'] = array( '#input' => TRUE, '#view' => NULL, '#process' => array('views_node_selector_process'), ); return $type; } /** * Process function for views_node_selector element. * * @see views_bulk_operations_elements() */ function views_node_selector_process($element, $edit) { $view = $element['#view']; $view_id = _views_bulk_operations_view_id($view); $view_name = $view->name; // Gather options. $result = $view->style_plugin->options['preserve_selection'] ? $_SESSION['vbo_values'][$view_name][$view_id]['result'] : $view->style_plugin->result; $options = array(); foreach ($result as $k => $v) { $options[$k] = ''; } // Fix default value. $element['#default_value'] += array('selection' => array(), 'selectall' => FALSE); $element['#default_value']['selection'] = $element['#default_value']['selectall'] ? array_diff_key($options, array_filter($element['#default_value']['selection'], '_views_bulk_operations_filter_invert')) : array_intersect_key($options, array_filter($element['#default_value']['selection'])); // Create selection FAPI elements. $element['#tree'] = TRUE; $element['selection'] = array( '#options' => $options, '#value' => $element['#default_value']['selection'], '#attributes' => array('class' => 'select'), ); $element['selection'] = expand_checkboxes($element['selection']); $element['selectall'] = array( '#type' => 'hidden', '#default_value' => $element['#default_value']['selectall'] ); return $element; } /** * Implementation of hook_theme(). */ function views_bulk_operations_theme() { $themes = array( 'views_node_selector' => array( 'arguments' => array('element' => NULL), ), 'views_bulk_operations_confirmation' => array( 'arguments' => array('objects' => NULL, 'invert' => FALSE, 'view' => NULL), ), 'views_bulk_operations_select_all' => array( 'arguments' => array('colspan' => 0, 'selection' => 0, 'view' => NULL), ), 'views_bulk_operations_table' => array( 'arguments' => array('header' => array(), 'rows' => array(), 'attributes' => array(), 'title' => NULL, 'view' => NULL), 'pattern' => 'views_bulk_operations_table__', ), ); // Load action theme files. foreach (_views_bulk_operations_load_actions() as $file) { $action_theme_fn = 'views_bulk_operations_'. $file .'_action_theme'; if (function_exists($action_theme_fn)) { $themes += call_user_func($action_theme_fn); } } return $themes; } /** * Theme function for 'views_bulk_operations_table'. */ function theme_views_bulk_operations_table($header, $rows, $attributes, $title, $view) { return theme('table', $header, $rows, $attributes, $title); } /** * Template preprocessor for theme function 'views_bulk_operations_table'. */ function template_preprocess_views_bulk_operations_table(&$vars, $hook) { $view = $vars['view']; $options = $view->style_plugin->options; $handler = $view->style_plugin; $fields = &$view->field; $columns = $handler->sanitize_columns($options['columns'], $fields); $active = !empty($handler->active) ? $handler->active : ''; foreach ($columns as $field => $column) { $vars['fields'][$field] = views_css_safe($field); if ($active == $field) { $vars['fields'][$field] .= ' active'; } } $count = 0; foreach ($vars['rows'] as $r => &$row) { $vars['row_classes'][$r][] = ($count++ % 2 == 0) ? 'odd' : 'even'; $cells = $row; if (isset($row['class'])) { $vars['row_classes'][$r][] = $row['class']; } if (isset($row['data'])) { $cells = $row['data']; } foreach ($cells as $c => &$cell) { if (is_array($cell) && isset($cell['data'])) { $cell = $cell['data']; } } $row = $cells; } $vars['row_classes'][0][] = 'views-row-first'; $vars['row_classes'][count($vars['row_classes']) - 1][] = 'views-row-last'; $vars['class'] = 'views-bulk-operations-table'; if ($view->style_plugin->options['sticky']) { drupal_add_js('misc/tableheader.js'); $vars['class'] .= ' sticky-enabled'; } $vars['class'] .= ' cols-'. count($vars['rows']); $vars['class'] .= ' views-table'; } /** * Theme function for 'views_node_selector'. */ function theme_views_node_selector($element) { module_load_include('inc', 'views', 'theme/theme'); $output = ''; $view = $element['#view']; $sets = $element['#sets']; $vars = array( 'view' => $view, ); // Give each group its own headers row. foreach ($sets as $title => $records) { $headers = array(); // template_preprocess_views_view_table() expects the raw data in 'rows'. $vars['rows'] = $records; // Render the view as table. Use the hook from from views/theme/theme.inc // and allow overrides using the same algorithm as the theme system will // do calling the theme() function. $hook = 'views_view_table'; $hooks = theme_get_registry(); if (!isset($hooks[$hook])) { return ''; } $args = array(&$vars, $hook); foreach ($hooks[$hook]['preprocess functions'] as $func) { if (function_exists($func)) { call_user_func_array($func, $args); } } // Add checkboxes to the header and the rows. $rows = array(); if (empty($view->style_plugin->options['hide_selector'])) { $headers[] = array('class' => 'vbo-select-all'); // Add extra status row if needed. $items_per_page = method_exists($view, 'get_items_per_page') ? $view->get_items_per_page() : (isset($view->pager) ? $view->pager['items_per_page'] : 0); if ($items_per_page && $view->total_rows > $items_per_page) { $row = theme('views_bulk_operations_select_all', count($vars['header']) + 1, _views_bulk_operations_get_selection_count($view->style_plugin, $element['#default_value']), $view); $rows[] = $row; } } else { $headers[] = array('class' => 'no-select-all'); } foreach ($vars['header'] as $field => $label) { $headers[] = array('data' => $label, 'class' => "views-field views-field-{$vars['fields'][$field]}"); } foreach ($records as $num => $object) { $vars['row_classes'][$num][] = 'rowclick'; $row = array('class' => implode(' ', $vars['row_classes'][$num]), 'data' => array()); $row['data'][] = theme('checkbox', $element['selection'][_views_bulk_operations_hash_object($object, $view)]); foreach ($vars['rows'][$num] as $field => $content) { // Support field classes in Views 3, but provide a fallback for Views 2. if (views_api_version() == 2) { $row['data'][] = array('data' => $content, 'class' => 'views-field views-field-' . $vars['fields'][$field]); } else { $row['data'][] = array('data' => $content, 'class' => $vars['field_classes'][$field][$num]); } } $rows[] = $row; } $theme_functions = views_theme_functions('views_bulk_operations_table', $view, $view->display[$view->current_display]); $output .= theme($theme_functions, $headers, $rows, array('class' => $vars['class']), $title, $view); $output .= theme('hidden', $element['selectall']); } return theme('form_element', $element, $output); } /** * Theme function for 'views_bulk_operations_select_all'. */ function theme_views_bulk_operations_select_all($colspan, $selection, $view) { $clear_selection = t('Clear selection'); $select_label = t('Select all items:'); $this_page = t('on this page only'); $all_pages = t('across all pages'); $this_page_checked = $selection['selectall'] ? '' : ' checked="checked"'; $all_pages_checked = $selection['selectall'] ? ' checked="checked"' : ''; $selection_count = t('@selected items selected.', array('@selected' => $selection['selected'])); $output = << $select_label EOF; return array(array('data' => $output, 'class' => 'views-field views-field-select-all', 'colspan' => $colspan)); } /** * Form implementation for main VBO multistep form. */ function views_bulk_operations_form($form_state, $form_id, $plugin) { // Erase the form parameters from $_REQUEST for a clean pager. if (!empty($form_state['post'])) { $_REQUEST = array_diff($_REQUEST, $form_state['post']); } // 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_set_header("Cache-Control: no-cache"); // works for IE6+ } else { drupal_set_header("Cache-Control: no-store"); // works for Firefox and other browsers } // Which step is this? if (empty($form_state['storage']['step'])) { // If empty view, render the empty text. if (empty($plugin->view->result)) { $form['empty'] = array('#value' => $plugin->view->display_handler->render_empty()); return $form; } // If there's a session variable on this view, pre-load the old values. $view_id = _views_bulk_operations_view_id($plugin->view); $view_name = $plugin->view->name; if (isset($_SESSION['vbo_values'][$view_name][$view_id]) && $plugin->options['preserve_selection']) { // Avoid PHP warnings. $_SESSION['vbo_values'][$view_name][$view_id] += array( 'selection' => array(), 'selectall' => FALSE, 'operation' => NULL, ); $default_objects = array( 'selection' => $_SESSION['vbo_values'][$view_name][$view_id]['selection'], 'selectall' => $_SESSION['vbo_values'][$view_name][$view_id]['selectall'], ); $default_operation = $_SESSION['vbo_values'][$view_name][$view_id]['operation']; } else { $default_objects = array('selection' => array(), 'selectall' => FALSE); $default_operation = NULL; } if (count($plugin->get_selected_operations()) == 1 && $plugin->options['merge_single_action']) { $step = VBO_STEP_SINGLE; } else { $step = VBO_STEP_VIEW; } } else { _views_bulk_operations_strip_view($plugin->view); switch ($form_state['storage']['step']) { case VBO_STEP_VIEW: $operation = $form_state['storage']['operation']; if ($operation['configurable']) { $step = VBO_STEP_CONFIG; } else { $step = VBO_STEP_CONFIRM; } break; case VBO_STEP_SINGLE: case VBO_STEP_CONFIG: $step = VBO_STEP_CONFIRM; break; } } $form['step'] = array( '#type' => 'value', '#value' => $step ); $form['#plugin'] = $plugin; switch ($step) { case VBO_STEP_VIEW: $form['select'] = array( '#type' => 'fieldset', '#title' => t('Bulk operations'), '#prefix' => '
', '#suffix' => '
', ); $form['objects'] = array( '#type' => 'views_node_selector', '#view' => $plugin->view, '#sets' => $plugin->sets, '#default_value' => $default_objects, '#prefix' => '
', '#suffix' => '
', ); if ($plugin->options['display_type'] == 0) { // Create dropdown and submit button. $form['select']['operation'] = array( '#type' => 'select', '#options' => array(0 => t('- Choose an operation -')) + $plugin->get_selected_operations(), '#default_value' => $default_operation, '#prefix' => '
', '#suffix' => '
', ); $form['select']['submit'] = array( '#type' => 'submit', '#value' => t('Execute'), '#prefix' => '
', '#suffix' => '
', ); } else { // Create buttons for actions. foreach ($plugin->get_selected_operations() as $md5 => $description) { $form['select'][$md5] = array( '#type' => 'submit', '#value' => $description, '#hash' => $md5, ); } } break; case VBO_STEP_SINGLE: $operation_keys = array_keys($plugin->get_selected_operations()); $operation = $plugin->get_operation_info($operation_keys[0]); $form['operation'] = array('#type' => 'value', '#value' => $operation_keys[0]); if ($operation['configurable']) { $form += _views_bulk_operations_action_form( $operation, $plugin->view, $plugin->result, $operation['options']['settings'] ); } $form['submit'] = array( '#type' => 'submit', '#value' => $operation['label'], '#prefix' => '
', '#suffix' => '
', ); $form['objects'] = array( '#type' => 'views_node_selector', '#view' => $plugin->view, '#sets' => $plugin->sets, '#default_value' => $default_objects, '#prefix' => '
', '#suffix' => '
', ); break; case VBO_STEP_CONFIG: $operation = $form_state['storage']['operation']; $form += _views_bulk_operations_action_form( $operation, $plugin->view, _views_bulk_operations_get_selection_full($plugin, $form_state), $operation['options']['settings'] ); $form['execute'] = array( '#type' => 'submit', '#value' => t('Next'), '#weight' => 98, ); $query = drupal_query_string_encode($_GET, array('q')); $form['cancel'] = array( '#type' => 'markup', '#value' => l('Cancel', $_GET['q'], array('query' => $query)), '#weight' => 99, ); drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation['label']))); break; case VBO_STEP_CONFIRM: $operation = $form_state['storage']['operation']; $query = drupal_query_string_encode($_GET, array('q')); $form = confirm_form($form, t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation['label'])), array('path' => $_GET['q'], 'query' => $query), theme('views_bulk_operations_confirmation', $form_state['storage']['selection'], $form_state['storage']['selectall'], $plugin->view) ); break; } // Use views_bulk_operations_form_submit() for form submit, regardless of form_id. $form['#submit'][] = 'views_bulk_operations_form_submit'; $form['#validate'][] = 'views_bulk_operations_form_validate'; $form['#attributes']['class'] = 'views-bulk-operations-form views-bulk-operations-form-step-' . $step; // A view with ajax enabled, and a space in the url, has to be decoded to work fine, // @see http://drupal.org/node/1325632 $form['#action'] = urldecode(request_uri()); return $form; } /** * Implementation of hook_form_alter(). */ function views_bulk_operations_form_alter(&$form, &$form_state) { // Get the form ID here to add the JS settings. if (!empty($form['form_id']) && strpos($form['form_id']['#value'], 'views_bulk_operations_form') === 0 && !empty($form['#plugin'])) { _views_bulk_operations_add_js($form['#plugin'], $form['#id'], $form['form_id']['#value']); } } /** * Form validate function for views_bulk_operations_form(). */ function views_bulk_operations_form_validate($form, &$form_state) { $form_id = $form_state['values']['form_id']; $plugin = $form['#plugin']; $view_id = _views_bulk_operations_view_id($plugin->view); $view_name = $plugin->view->name; switch ($form_state['values']['step']) { case VBO_STEP_VIEW: if (!array_filter($form_state['values']['objects']['selection']) && (empty($_SESSION['vbo_values'][$view_name][$view_id]) || !array_filter($_SESSION['vbo_values'][$view_name][$view_id]['selection']))) { form_set_error('objects', t('No item selected. Please select one or more items.')); } 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.')); } if (form_get_errors()) { _views_bulk_operations_add_js($plugin, $form['#id'], $form_id); } break; case VBO_STEP_SINGLE: if (!array_filter($form_state['values']['objects']['selection']) && (empty($_SESSION['vbo_values'][$view_name][$view_id]) || !array_filter($_SESSION['vbo_values'][$view_name][$view_id]['selection']))) { form_set_error('objects', t('No item selected. Please select one or more items.')); } $operation = $plugin->get_operation_info($form_state['values']['operation']); if ($operation['configurable']) { _views_bulk_operations_action_validate($operation, $form, $form_state); } if (form_get_errors()) { _views_bulk_operations_add_js($plugin, $form['#id'], $form_id); } break; case VBO_STEP_CONFIG: $operation = $form_state['storage']['operation']; _views_bulk_operations_action_validate($operation, $form, $form_state); // If the action validation fails, Form API will bring us back to this step. // We need to strip the view here because the form function will not be called. // Also, the $plugin variable above was carried over from last submission, so it // does not represent the current instance of the plugin. // That's why we had to store instances of the plugin in this global array. if (form_get_errors()) { global $vbo_plugins; if (isset($vbo_plugins[$form_id])) { _views_bulk_operations_strip_view($vbo_plugins[$form_id]->view); } } break; } } /** * Form submit function for views_bulk_operations_form(). */ function views_bulk_operations_form_submit($form, &$form_state) { $form_id = $form_state['values']['form_id']; $plugin = $form['#plugin']; $view = $plugin->view; $view_id = _views_bulk_operations_view_id($view); $view_name = $view->name; $form_state['storage']['step'] = $step = $form_state['values']['step']; switch ($step) { case VBO_STEP_VIEW: $form_state['storage']['selection'] = _views_bulk_operations_get_selection($plugin, $form_state, $form_id); $form_state['storage']['selectall'] = $form_state['values']['objects']['selectall']; $form_state['storage']['operation'] = $operation = $plugin->get_operation_info($form_state['values']['operation']); $_SESSION['vbo_values'][$view_name][$view_id]['operation'] = $operation['key']; if (!$operation['configurable'] && !empty($operation['options']['skip_confirmation'])) { break; // Go directly to execution } return; case VBO_STEP_SINGLE: $form_state['storage']['selection'] = _views_bulk_operations_get_selection($plugin, $form_state, $form_id); $form_state['storage']['selectall'] = $form_state['values']['objects']['selectall']; $form_state['storage']['operation'] = $operation = $plugin->get_operation_info($form_state['values']['operation']); $_SESSION['vbo_values'][$view_name][$view_id]['operation'] = $operation['key']; if ($operation['configurable']) { $form_state['storage']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state); } if (!empty($operation['options']['skip_confirmation'])) { break; // Go directly to execution } return; case VBO_STEP_CONFIG: $operation = $form_state['storage']['operation']; $form_state['storage']['operation_arguments'] = _views_bulk_operations_action_submit($operation, $form, $form_state); if (!empty($operation['options']['skip_confirmation'])) { break; // Go directly to execution } return; case VBO_STEP_CONFIRM: break; } // Clean up unneeded SESSION variables. unset($_SESSION['vbo_values'][$view->name]); // Execute the VBO. $objects = _views_bulk_operations_get_selection_full($plugin, $form_state); $operation = $form_state['storage']['operation']; $operation_arguments = array(); if ($operation['configurable']) { $operation_arguments = $form_state['storage']['operation_arguments']; } _views_bulk_operations_execute( $view, $objects, $operation, $operation_arguments, array( 'execution_type' => $plugin->options['execution_type'], 'display_result' => $plugin->options['display_result'], 'max_performance' => $plugin->options['max_performance'], 'settings' => $operation['options']['settings'], ) ); // Clean up the form. $query = drupal_query_string_encode($_GET, array('q')); $form_state['redirect'] = array('path' => $view->get_url(), 'query' => $query); unset($form_state['storage']); } /** * Compute the selection based on the settings. */ function _views_bulk_operations_get_selection($plugin, $form_state, $form_id) { $result = $plugin->result; $selection = $form_state['values']['objects']['selection']; if ($plugin->options['preserve_selection']) { $view_id = _views_bulk_operations_view_id($plugin->view); $view_name = $plugin->view->name; $result = $_SESSION['vbo_values'][$view_name][$view_id]['result']; $selection = $_SESSION['vbo_values'][$view_name][$view_id]['selection']; } $selection = $form_state['values']['objects']['selectall'] ? array_intersect_key($result, array_filter($selection, '_views_bulk_operations_filter_invert')) : array_intersect_key($result, array_filter($selection)); return $selection; } /** * Compute the actual selected objects based on the settings. */ function _views_bulk_operations_get_selection_full($plugin, $form_state) { // Get the objects from the view if selectall was chosen. $view = $plugin->view; if ($form_state['storage']['selectall']) { $view_copy = views_get_view($view->name); $view_copy->set_exposed_input($view->exposed_input); $view_copy->set_arguments($view->args); $view_copy->set_items_per_page(0); $view_copy->skip_render = TRUE; // signal our plugin to skip the rendering $view_copy->render($view->current_display); $objects = array(); foreach ($view_copy->result as $row) { $objects[_views_bulk_operations_hash_object($row, $view_copy)] = $row; } $view_copy->destroy(); $objects = array_diff_key($objects, $form_state['storage']['selection']); } else { $objects = $form_state['storage']['selection']; } return $objects; } /** * Compute the actual number of selected items. */ function _views_bulk_operations_get_selection_count($plugin, $selection) { if ($plugin->options['preserve_selection']) { $view_id = _views_bulk_operations_view_id($plugin->view); $view_name = $plugin->view->name; $selection = $_SESSION['vbo_values'][$view_name][$view_id]; } return array( 'selectall' => $selection['selectall'], 'selected' => $selection['selectall'] ? $plugin->view->total_rows - count(array_filter($selection['selection'], '_views_bulk_operations_filter_invert')) : count(array_filter($selection['selection'])) ); } /** * Theme function to show the confirmation page before executing the action. */ function theme_views_bulk_operations_confirmation($objects, $invert, $view) { $selectall = $invert ? (count($objects) == 0) : (count($objects) == $view->total_rows); if ($selectall) { $output = format_plural( $view->total_rows, 'You selected the only item in this view.', 'You selected all @count items in this view.' ); } else { $object_info = _views_bulk_operations_object_info_for_view($view); $items = array(); foreach ($objects as $row) { $oid = $row->{$view->base_field}; if ($object = call_user_func($object_info['load'], $oid)) { $items[] = check_plain((string)$object->{$object_info['title']}); } } $output = theme('item_list', $items, $invert ? format_plural( count($objects), 'You selected all ' . $view->total_rows . ' but the following item:', 'You selected all ' . $view->total_rows . ' but the following @count items:' ) : format_plural( count($objects), 'You selected the following item:', 'You selected the following @count items:' ) ); } return $output; } /** * Implementation of hook_forms(). * * Force each instance of function to use the same callback. */ function views_bulk_operations_forms($form_id, $args) { // Ensure we map a callback for our form and not something else. $forms = array(); if (strpos($form_id, 'views_bulk_operations_form_') === 0) { // Let the forms API know where to get the form data corresponding // to this form id. $forms[$form_id] = array( 'callback' => 'views_bulk_operations_form', 'callback arguments' => array($form_id), ); } return $forms; } /** * Implementation of hook_views_bulk_operations_object_info() * * Hook used by VBO to be able to handle different objects as does Views 2 and the Drupal core action system. * * The array returned for each object type contains: * 'type' (required) => the object type name, should be the same as 'type' field in hook_action_info(). * 'context' (optional) => the context name that should receive the object, defaults to the value of 'type' above. * 'base_table' (required) => the Views 2 table name corresponding to that object type, should be the same as the $view->base_table attribute. * 'oid' (currently unused) => an attribute on the object that returns the unique object identifier (should be the same as $view->base_field). * 'load' (required) => a function($oid) that returns the corresponding object. * 'title' (required) => an attribute on the object that returns a human-friendly identifier of the object. * 'access' (optional) => a function($op, $node, $account = NULL) that behaves like node_access(). * * The following attributes allow VBO to show actions on view types different than the action's type: * 'hook' (optional) => the name of the hook supported by this object type, as defined in the 'hooks' attribute of hook_action_info(). * 'normalize' (optional) => a function($type, $object) that takes an object type and the object instance, returning additional context information for cross-type * * e.g., an action declaring hooks => array('user') while of type 'system' will be shown on user views, and VBO will call the user's 'normalize' function to * prepare the action to fit the user context. */ function views_bulk_operations_views_bulk_operations_object_info() { $object_info = array( 'node' => array( 'type' => 'node', 'base_table' => 'node', 'load' => '_views_bulk_operations_node_load', 'oid' => 'nid', 'title' => 'title', 'access' => 'node_access', 'hook' => 'nodeapi', 'normalize' => '_views_bulk_operations_normalize_node_context', ), 'user' => array( 'type' => 'user', 'base_table' => 'users', 'load' => 'user_load', 'oid' => 'uid', 'title' => 'name', 'context' => 'account', 'access' => '_views_bulk_operations_user_access', 'hook' => 'user', 'normalize' => '_views_bulk_operations_normalize_user_context', ), 'comment' => array( 'type' => 'comment', 'base_table' => 'comments', 'load' => '_comment_load', 'oid' => 'cid', 'title' => 'subject', 'access' => '_views_bulk_operations_comment_access', 'hook' => 'comment', 'normalize' => '_views_bulk_operations_normalize_comment_context', ), 'term' => array( 'type' => 'term', 'base_table' => 'term_data', 'load' => 'taxonomy_get_term', 'oid' => 'tid', 'title' => 'name', 'hook' => 'taxonomy', ), 'node_revision' => array( 'type' => 'node_revision', 'base_table' => 'node_revisions', 'load' => '_views_bulk_operations_node_revision_load', 'title' => 'name', ), 'file' => array( 'type' => 'file', 'base_table' => 'files', 'load' => '_views_bulk_operations_file_load', 'oid' => 'fid', 'title' => 'filename', ), ); return $object_info; } /** * Access function for objects of type 'user'. */ function _views_bulk_operations_user_access($op, $user, $account = NULL) { return user_access('access user profiles', $account); } /** * Access function for objects of type 'comments'. */ function _views_bulk_operations_comment_access($op, $comment, $account = NULL) { return user_access('access comments', $account); } /** * Load function for objects of type 'node'. */ function _views_bulk_operations_node_load($nid) { return node_load($nid, NULL, TRUE); } /** * Load function for objects of type 'file'. */ function _views_bulk_operations_file_load($fid) { return db_fetch_object(db_query("SELECT * FROM {files} WHERE fid = %d", $fid)); } /** * Load function for node revisions. */ function _views_bulk_operations_node_revision_load($vid) { $nid = db_result(db_query("SELECT nid FROM {node_revisions} WHERE vid = %d", $vid)); return node_load($nid, $vid, TRUE); } /** * Normalize function for node context. * * @see _trigger_normalize_node_context() */ function _views_bulk_operations_normalize_node_context($type, $node) { switch ($type) { // If an action that works on comments is being called in a node context, // the action is expecting a comment object. But we do not know which comment // to give it. The first? The most recent? All of them? So comment actions // in a node context are not supported. // An action that works on users is being called in a node context. // Load the user object of the node's author. case 'user': return user_load(array('uid' => $node->uid)); } } /** * Normalize function for comment context. * * @see _trigger_normalize_comment_context() */ function _views_bulk_operations_normalize_comment_context($type, $comment) { switch ($type) { // An action that works with nodes is being called in a comment context. case 'node': return node_load(is_array($comment) ? $comment['nid'] : $comment->nid); // An action that works on users is being called in a comment context. case 'user': return user_load(array('uid' => is_array($comment) ? $comment['uid'] : $comment->uid)); } } /** * Normalize function for user context. * * @see _trigger_normalize_user_context() */ function _views_bulk_operations_normalize_user_context($type, $account) { switch ($type) { // If an action that works on comments is being called in a user context, // the action is expecting a comment object. But we have no way of // determining the appropriate comment object to pass. So comment // actions in a user context are not supported. // An action that works with nodes is being called in a user context. // If a single node is being viewed, return the node. case 'node': // If we are viewing an individual node, return the node. if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) { return node_load(array('nid' => arg(1))); } } } /** * Implementation of hook_init(). */ function views_bulk_operations_init() { // Make sure our actions are loaded. _views_bulk_operations_load_actions(); } /** * Implementation of hook_action_info(). */ function views_bulk_operations_action_info() { $actions = array(); foreach (_views_bulk_operations_load_actions() as $file) { $action_info_fn = 'views_bulk_operations_'. $file .'_action_info'; $action_info = call_user_func($action_info_fn); if (is_array($action_info)) { $actions += $action_info; } } // Add VBO's own programmatic action. $actions['views_bulk_operations_action'] = array( 'description' => t('Execute a VBO programmatically'), 'type' => 'system', 'configurable' => TRUE, 'rules_ignore' => TRUE, ); return $actions; } /** * Implementation of hook_menu(). */ function views_bulk_operations_menu() { $items['views-bulk-operations/js/action'] = array( 'title' => 'VBO action form', 'description' => 'AHAH callback to display action form on VBO action page.', 'page callback' => 'views_bulk_operations_form_ahah', 'page arguments' => array('views_bulk_operations_action_form_operation'), 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['views-bulk-operations/js/select'] = array( 'title' => 'VBO select handler', 'description' => 'AJAX callback to update selection.', 'page callback' => 'views_bulk_operations_select', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); return $items; } /** * AJAX callback to update selection. */ function views_bulk_operations_select() { $view_id = $_REQUEST['view_id']; $view_name = $_REQUEST['view_name']; foreach (json_decode($_REQUEST['selection'], TRUE) as $selection => $value) { switch ($selection) { case 'operation': $_SESSION['vbo_values'][$view_name][$view_id]['operation'] = $value; break; case 'selectall': $_SESSION['vbo_values'][$view_name][$view_id]['selectall'] = $value > 0; if ($value == -1) { // -1 => reset selection $_SESSION['vbo_values'][$view_name][$view_id]['selection'] = array(); } break; default: $_SESSION['vbo_values'][$view_name][$view_id]['selection'][$selection] = $value > 0; break; } } drupal_json(array( 'selected' => count(array_filter($_SESSION['vbo_values'][$view_name][$view_id]['selection'])), 'unselected' => count(array_filter($_SESSION['vbo_values'][$view_name][$view_id]['selection'], '_views_bulk_operations_filter_invert')), 'selectall' => $_SESSION['vbo_values'][$view_name][$view_id]['selectall'], )); exit; } /** * Form function for views_bulk_operations_action action. */ function views_bulk_operations_action_form($context) { // Some views choke on being rebuilt at this moment because of validation errors in the action form. // So we save the error state, reset it, build the views, then reinstate the errors. // Also unset the error messages because they'll be displayed again after the loop. $errors = form_get_errors(); if (!empty($errors)) foreach ($errors as $message) { unset($_SESSION['messages']['error'][array_search($message, $_SESSION['messages']['error'])]); } form_set_error(NULL, '', TRUE); // Look for all views with VBO styles, and for each find the operations they use. // Distinguish between overridden and default views to simplify export. $views[0] = t('- Choose a view -'); $operations[0] = t('- Choose an operation -'); foreach (views_get_all_views() as $name => $view) { foreach (array_keys($view->display) as $display) { $display_options = &$view->display[$display]->display_options; if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') { $vid = $view->name; $views[$vid] = $view->name . (!empty($view->description) ? ': ' . $view->description : ''); $view_clone = clone($view); $style_plugin = views_get_plugin('style', $display_options['style_plugin']); $style_plugin->init($view_clone, $view_clone->display[$display], $display_options['style_options']); if (isset($context['view_vid']) && $vid == $context['view_vid']) { $form['#plugin'] = $style_plugin; } unset($view_clone); if (!empty($display_options['style_options']['operations'])) foreach ($display_options['style_options']['operations'] as $key => $options) { if (empty($options['selected'])) continue; $operations[$key] = $views_operations[$vid][$key] = $style_plugin->all_operations[$key]['label']; if (isset($context['operation_key']) && isset($context['view_vid']) && $key == $context['operation_key'] && $vid == $context['view_vid']) { $form['#operation'] = $style_plugin->get_operation_info($key); } } } } } if (!empty($errors)) foreach ($errors as $name => $message) { form_set_error($name, $message); } drupal_add_js(array('vbo' => array('action' => array('views_operations' => $views_operations))), 'setting'); drupal_add_js(drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.action.js'); $form['view_vid'] = array( '#type' => 'select', '#title' => t('View'), '#description' => t('Select the VBO to be executed.'), '#options' => $views, '#default_value' => @$context['view_vid'], '#attributes' => array('onchange' => 'Drupal.vbo.action.updateOperations(this.options[this.selectedIndex].value, true);'), ); $form['operation_key'] = array( '#type' => 'select', '#title' => t('Operation'), '#description' => t('Select the operation to be executed.'), '#options' => $operations, '#default_value' => @$context['operation_key'], '#ahah' => array( 'path' => 'views-bulk-operations/js/action', 'wrapper' => 'operation-wrapper', 'method' => 'replace', ), ); $form['operation_arguments'] = array( '#type' => 'fieldset', '#title' => t('Operation arguments'), '#description' => t('If the selected action is configurable, this section will show the action\'s arguments form, followed by a text field where a PHP script can be entered to programmatically assemble the arguments. '), ); $form['operation_arguments']['wrapper'] = array( '#type' => 'markup', '#value' => '', '#prefix' => '
', '#suffix' => '
', ); if (isset($form['#operation']) && $form['#operation']['configurable'] && isset($form['#plugin'])) { $form['operation_arguments']['wrapper']['operation_form'] = _views_bulk_operations_action_form( $form['#operation'], $form['#plugin']->view, NULL, $form['#operation']['options']['settings'], $context ); if (!empty($form['#operation']['form properties'])) foreach ($form['#operation']['form properties'] as $property) { if (isset($form['operation_arguments']['wrapper']['operation_form'][$property])) { $form[$property] = $form['operation_arguments']['wrapper']['operation_form'][$property]; } } $form['operation_arguments']['wrapper']['operation_arguments'] = array( '#type' => 'textarea', '#title' => t('Operation arguments'), '#description' => t('Enter PHP script that will assemble the operation arguments (and will override the arguments above). These arguments should be of the form: return array(\'argument1\' => \'value1\', ...); and they should correspond to the values returned by the action\'s form submit function. The variables &$object and $context are available to this script. '), '#default_value' => @$context['operation_arguments'], ); } else { $form['operation_arguments']['wrapper']['operation_form'] = array( '#type' => 'markup', '#value' => t('This operation is not configurable.'), ); $form['operation_arguments']['wrapper']['operation_arguments'] = array('#type' => 'value', '#value' => ''); } $form['view_exposed_input'] = array( '#type' => 'textarea', '#title' => t('View exposed input'), '#description' => t('Enter PHP script that will assemble the view exposed input (if the view accepts exposed input). These inputs should be of the form: return array(\'input1\' => \'value1\', ...); and they should correspond to the query values used on the view URL when exposed filters are applied. The variables &$object and $context are available to this script. '), '#default_value' => @$context['view_exposed_input'], ); $form['view_arguments'] = array( '#type' => 'textarea', '#title' => t('View arguments'), '#description' => t('Enter PHP script that will assemble the view arguments (if the view accepts arguments). These arguments should be of the form: return array(\'value1\', ...); and they should correspond to the arguments defined in the view. The variables &$object and $context are available to this script. '), '#default_value' => @$context['view_arguments'], ); $form['respect_limit'] = array( '#type' => 'checkbox', '#title' => t('Respect the view\'s item limit'), '#default_value' => @$context['respect_limit'], ); return $form; } /** * Generic AHAH callback to manipulate a form. */ function views_bulk_operations_form_ahah($callback) { $form_state = array('submitted' => FALSE); $form_build_id = $_POST['form_build_id']; // Add the new element to the stored form. Without adding the element to the // form, Drupal is not aware of this new elements existence and will not // process it. We retreive the cached form, add the element, and resave. $form = form_get_cache($form_build_id, $form_state); // Invoke the callback that will populate the form. $render =& $callback($form, array('values' => $_POST)); form_set_cache($form_build_id, $form, $form_state); $form += array( '#post' => $_POST, '#programmed' => FALSE, ); // Rebuild the form. $form = form_builder($_POST['form_id'], $form, $form_state); // Render the new output. $output_html = drupal_render($render); $output_js = drupal_get_js(); print drupal_to_js(array('data' => theme('status_messages') . $output_html . $output_js, 'status' => TRUE)); exit(); } /** * Form callback to update an action form when a new action is selected in views_bulk_operations_action form. */ function& views_bulk_operations_action_form_operation(&$form, $form_state) { // TODO: Replace this with autoloading of style plugin and view definitions to use $form['#plugin']. $view = views_get_view($form_state['values']['view_vid']); $vd = new views_bulk_operations_destructor($view); // this will take care of calling $view->destroy() on exit. foreach (array_keys($view->display) as $display) { $display_options = &$view->display[$display]->display_options; if (isset($display_options['style_plugin']) && $display_options['style_plugin'] == 'bulk') { $plugin = views_get_plugin('style', $display_options['style_plugin']); $plugin->init($view, $view->display[$display], $display_options['style_options']); break; } } $form['#operation'] = $plugin->get_operation_info($form_state['values']['operation_key']); if ($form['#operation']['configurable']) { $form['operation_arguments']['wrapper'] = array( '#type' => 'markup', '#value' => '', '#prefix' => '
', '#suffix' => '
', ); $form['operation_arguments']['wrapper']['operation_form'] = _views_bulk_operations_action_form( $form['#operation'], $plugin->view, NULL, $form['#operation']['options']['settings'] ); if (!empty($form['#operation']['form properties'])) foreach ($form['#operation']['form properties'] as $property) { if (isset($form['operation_arguments']['wrapper']['operation_form'][$property])) { $form[$property] = $form['operation_arguments']['wrapper']['operation_form'][$property]; } } $form['operation_arguments']['wrapper']['operation_arguments'] = array( '#type' => 'textarea', '#title' => t('Operation arguments'), '#description' => t('Enter PHP script that will assemble the operation arguments (and will override the operation form above). These arguments should be of the form: return array(\'argument1\' => \'value1\', ...); and they should correspond to the values returned by the action\'s form submit function. The variables &$object and $context are available to this script. '), ); } else { $form['operation_arguments']['wrapper']['operation_form'] = array( '#type' => 'markup', '#value' => t('This operation is not configurable.'), ); $form['operation_arguments']['wrapper']['operation_arguments'] = array('#type' => 'value', '#value' => ''); } return $form['operation_arguments']['wrapper']; } /** * Form validate function for views_bulk_operations_action action. */ function views_bulk_operations_action_validate($form, $form_state) { if (empty($form_state['values']['view_vid'])) { form_set_error('view_vid', t('You must choose a view to be executed.')); } if (empty($form_state['values']['operation_key'])) { form_set_error('operation_callback', t('You must choose an operation to be executed.')); } if ($form['#operation']) { module_invoke_all('action_info'); // some validate functions are created dynamically... _views_bulk_operations_action_validate($form['#operation'], $form, $form_state); } } /** * Form submit function for views_bulk_operations_action action. */ function views_bulk_operations_action_submit($form, $form_state) { $submit = array( 'view_vid' => $form_state['values']['view_vid'], 'operation_key' => $form_state['values']['operation_key'], 'operation_arguments' => $form_state['values']['operation_arguments'], 'view_exposed_input' => $form_state['values']['view_exposed_input'], 'view_arguments' => $form_state['values']['view_arguments'], 'respect_limit' => $form_state['values']['respect_limit'], ); if ($form['#operation'] && function_exists($form['#operation']['callback'] . '_submit')) { $submit = array_merge($submit, _views_bulk_operations_action_submit($form['#operation'], $form, $form_state)); } return $submit; } /** * Execution function for views_bulk_operations_action action. */ function views_bulk_operations_action(&$object, $context) { $view_exposed_input = array(); if (!empty($context['view_exposed_input'])) { $view_exposed_input = eval($context['view_exposed_input']); } $view_arguments = array(); if (!empty($context['view_arguments'])) { $view_arguments = eval($context['view_arguments']); } if (!empty($context['operation_arguments'])) { $operation_arguments = eval($context['operation_arguments']); } else { $operation_arguments = $context; foreach (array('operation_key', 'operation_arguments', 'views_vid', 'view_exposed_input', 'view_arguments') as $key) { unset($operation_arguments[$key]); } } views_bulk_operations_execute($context['view_vid'], $context['operation_key'], $operation_arguments, $view_exposed_input, $view_arguments, $context['respect_limit']); } /** * Helper function to execute the chosen action upon selected objects. */ function _views_bulk_operations_execute($view, $objects, $operation, $operation_arguments, $options) { global $user; // Get the object info we're dealing with. $object_info = _views_bulk_operations_object_info_for_view($view); if (!$object_info) return; // Add action arguments. $params = array(); if ($operation['configurable'] && is_array($operation_arguments)) { $params += $operation_arguments; } // Add static callback arguments. Note that in the case of actions, static arguments // are picked up from the database in actions_do(). if (isset($operation['callback arguments'])) { $params += $operation['callback arguments']; } // Add this view as parameter. $params['view'] = array( 'vid' => !empty($view->vid) ? $view->vid : $view->name, 'exposed_input' => $view->get_exposed_input(), 'arguments' => $view->args, ); // Add static settings to the params. if (!empty($options['settings'])) { $params['settings'] = $options['settings']; } // Add object info to the params. $params['object_info'] = $object_info; if ($operation['aggregate'] != VBO_AGGREGATE_FORCED && $options['execution_type'] == VBO_EXECUTION_BATCH) { // 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'] = $params; $_SESSION['vbo_options']['object_info'] = $object_info; $batch = array( 'title' => t('Performing %operation on selected items...', array('%operation' => $operation['label'])), 'finished' => '_views_bulk_operations_execute_finished', ); // If they have max performance checked, use the high performant batch process. if ($options['max_performance']) { $batch += array( 'operations' => array( array('_views_bulk_operations_execute_multiple', array($view->base_field, $operation, $objects, $params, $object_info, TRUE)), ), ); } else { $operations = array(); foreach ($objects as $num => $row) { $oid = $row->{$view->base_field}; $operations[] = array('_views_bulk_operations_execute_single', array($oid, $row)); } $batch += array( 'operations' => $operations, ); } batch_set($batch); } else if ($operation['aggregate'] != VBO_AGGREGATE_FORCED && module_exists('drupal_queue') && $options['execution_type'] == VBO_EXECUTION_QUEUE) { drupal_queue_include(); foreach ($objects as $row) { $oid = $row->{$view->base_field}; $job = array( 'description' => t('Perform %operation on @type %oid.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%oid' => $oid )), 'arguments' => array($oid, $row, $operation, $params, $user->uid, $options['display_result'], $object_info), ); $queue = DrupalQueue::get('views_bulk_operations'); $queue->createItem($job); $oids[] = $oid; } if ($options['display_result']) { drupal_set_message(t('Enqueued %operation on @types %oid.', array( '%operation' => $operation['label'], '@types' => format_plural(count($objects), $object_info['type'], $object_info['type'] . 's'), '%oid' => implode(', ', $oids), ))); } } else /*if ($options['execution_type'] == VBO_EXECUTION_DIRECT)*/ { @set_time_limit(0); $context['results']['rows'] = 0; $context['results']['time'] = microtime(TRUE); _views_bulk_operations_execute_multiple($view->base_field, $operation, $objects, $params, $object_info, FALSE, $context); _views_bulk_operations_execute_finished(TRUE, $context['results'], array(), $options + array('operation' => $operation, 'params' => $params)); } } /** * Helper function to handle Drupal Queue operations. */ function _views_bulk_operations_execute_queue($data) { module_load_include('inc', 'node', 'node.admin'); list($oid, $row, $operation, $params, $uid, $display_result, $object_info) = $data['arguments']; $object = call_user_func($object_info['load'], $oid); if (!$object) { watchdog('vbo', 'Skipped %operation on @type id %oid because it was not found.', array( '%operation' => $operation['label'], '@type' => t($operation['type']), '%oid' => $oid, ), WATCHDOG_ALERT); return; } $account = user_load(array('uid' => $uid)); if (!_views_bulk_operations_object_permission($operation, $object, $object_info, $account)) { watchdog('vbo', 'Skipped %operation on @type %title due to insufficient permissions.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, ), WATCHDOG_ALERT); return; } _views_bulk_operations_action_do($operation, $oid, $object, $row, $params, $object_info, $account); if ($display_result) { watchdog('vbo', 'Performed %operation on @type %title.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, ), WATCHDOG_INFO); } } /** * Helper function to handle Batch API operations. */ function _views_bulk_operations_execute_single($oid, $row, &$context) { module_load_include('inc', 'node', 'node.admin'); $operation = $_SESSION['vbo_options']['operation']; $params = $_SESSION['vbo_options']['params']; $object_info = $_SESSION['vbo_options']['object_info']; if (!isset($context['results']['time'])) { $context['results']['time'] = microtime(TRUE); $context['results']['rows'] = 0; } $object = call_user_func($object_info['load'], $oid); if (!$object) { $context['results']['log'][] = t('Skipped %operation on @type id %oid because it was not found.', array( '%operation' => $operation['label'], '@type' => t($operation['type']), '%oid' => $oid, )); return; } if (!_views_bulk_operations_object_permission($operation, $object, $object_info)) { $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, )); return; } _views_bulk_operations_action_do($operation, $oid, $object, $row, $params, $object_info); $context['results']['log'][] = $context['message'] = t('Performed %operation on @type %title.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, )); $context['results']['rows'] += 1; } /** * Gets the next item in the loop taking into consideration server limits for high performance batching. * * @return - The next object in the objects array. */ function _views_bulk_operations_execute_next($index, $objects, $batch) { static $loop = 0, $last_mem = -1, $last_time = 0, $memory_limit = 0, $time_limit = 0; // Early return if we're done. if ($index >= count($objects)) { return FALSE; } // Get the array keys. $keys = array_keys($objects); if ($batch) { // Keep track of how many loops we have taken. $loop++; // Memory limit in bytes. $memory_limit = $memory_limit ? $memory_limit : ((int)preg_replace('/[^\d\s]/', '', ini_get('memory_limit'))) * 1048576; // Max time execution limit. $time_limit = $time_limit ? $time_limit : (int)ini_get('max_execution_time'); // Current execution time in seconds. $current_time = time() - $_SERVER['REQUEST_TIME']; $time_left = $time_limit - $current_time; if ($loop == 1) { $last_time = $current_time; // Never break the first loop. return $objects[$keys[$index]]; } // Break when current free memory past threshold. Default to 32 MB. if (($memory_limit - memory_get_usage()) < variable_get('batch_free_memory_threshold', 33554432)) { return FALSE; } // Break when peak free memory past threshold. Default to 8 MB. if (($memory_limit - memory_get_peak_usage()) < variable_get('batch_peak_free_memory_threshold', 8388608)) { return $objects[$keys[$index]]; } // Break when execution time remaining past threshold. Default to 15 sec. if (($time_limit - $current_time) < variable_get('batch_time_remaining_threshold', 15)) { return FALSE; } $last_time = $current_time; return $objects[$keys[$index]]; } else { return $objects[$keys[$index]]; } } /** * Helper function for multiple execution operations. */ function _views_bulk_operations_execute_multiple($base_field, $operation, $objects, $params, $object_info, $batch, &$context) { // Setup our batch process. if (empty($context['sandbox'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = count($objects); } if (empty($context['results']['time'])) { $context['results']['time'] = microtime(TRUE); $context['results']['rows'] = 0; } if ($operation['aggregate'] != VBO_AGGREGATE_FORBIDDEN) { $oids = array(); while ($row = _views_bulk_operations_execute_next($context['sandbox']['progress'], $objects, $batch)) { $context['sandbox']['progress']++; $oid = $row->{$base_field}; if (isset($object_info['access'])) { $object = call_user_func($object_info['load'], $oid); if (!$object) { unset($objects[$num]); $context['results']['log'][] = t('Skipped %operation on @type %oid because it was not found.', array( '%operation' => $operation['label'], '@type' => t($operation['type']), '%oid' => $oid, )); continue; } if (!_views_bulk_operations_object_permission($operation, $object, $object_info)) { unset($objects[$num]); $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, )); continue; } } $oids[] = $oid; } if (!empty($objects)) { _views_bulk_operations_action_aggregate_do($operation, $oids, $objects, $params, $object_info); $context['results']['log'][] = t('Performed aggregate %operation on @types %oids.', array( '%operation' => $operation['label'], '@types' => format_plural(count($objects), $object_info['type'], $object_info['type'] . 's'), '%oids' => implode(',', $oids), )); $context['message'] = t('Performed aggregate %operation on !count @types.', array( '%operation' => $operation['label'], '!count' => count($objects), '@types' => format_plural(count($objects), $object_info['type'], $object_info['type'] . 's'), )); $context['results']['rows'] += count($objects); } } else { $oids = array(); while ($row = _views_bulk_operations_execute_next($context['sandbox']['progress'], $objects, $batch)) { $context['sandbox']['progress']++; $oid = $row->{$base_field}; $object = call_user_func($object_info['load'], $oid); if (!$object) { $context['results']['log'][] = t('Skipped %operation on @type id %oid because it was not found.', array( '%operation' => $operation['label'], '@type' => t($operation['type']), '%oid' => $oid, )); continue; } if (!_views_bulk_operations_object_permission($operation, $object, $object_info)) { $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, )); continue; } _views_bulk_operations_action_do($operation, $oid, $object, $row, $params, $object_info); $context['results']['log'][] = t('Performed %operation on @type %title.', array( '%operation' => $operation['label'], '@type' => t($object_info['type']), '%title' => $object->{$object_info['title']}, )); $context['results']['rows'] += 1; $oids[] = $oid; } $context['message'] = t('Performed %operation on !count @types.', array( '%operation' => $operation['label'], '!count' => count($oids), '@types' => format_plural(count($oids), $object_info['type'], $object_info['type'] . 's'), )); } // Update batch progress. $context['finished'] = empty($context['sandbox']['max']) ? 1 : ($context['sandbox']['progress'] / $context['sandbox']['max']); } /** * Helper function to cleanup operations. */ function _views_bulk_operations_execute_finished($success, $results, $operations, $options = NULL) { if ($success) { if ($results['rows'] > 0) { $message = t('!results items processed in about !time ms:', array('!results' => $results['rows'], '!time' => round((microtime(TRUE) - $results['time']) * 1000))); } else { $message = t('No items were processed:'); } $message .= "\n". theme('item_list', $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, $oid, $object, $row, $params, $object_info, $account = NULL) { _views_bulk_operations_action_permission($operation, $account); // Add the object to the context. if (!empty($object_info['context'])) { $params[$object_info['context']] = $object; } else { $params[$object_info['type']] = $object; } // If the operation type is different from the view type, normalize the context first. $actual_object = $object; if ($object_info['type'] != $operation['type']) { if (isset($object_info['normalize']) && function_exists($object_info['normalize'])) { $actual_object = call_user_func($object_info['normalize'], $operation['type'], $object); } $params['hook'] = $object_info['hook']; } if (is_null($actual_object)) { // Normalize function can return NULL: we don't want that $actual_object = $object; } $params['row'] = $row; // Expose the original view row to the action if ($operation['source'] == 'action') { actions_do($operation['callback'], $actual_object, $params); if ($operation['type'] == 'node' && ($operation['access op'] & VBO_ACCESS_OP_UPDATE)) { // Save nodes explicitly if needed $node_options = variable_get('node_options_'. $actual_object->type, array('status', 'promote')); if (in_array('revision', $node_options) && !isset($actual_object->revision)) { $actual_object->revision = TRUE; $actual_object->log = ''; } node_save($actual_object); } } else { // source == 'operation' $args = array_merge(array(array($oid)), $params); call_user_func_array($operation['callback'], $args); } } /** * Helper function to execute an aggregate operation. */ function _views_bulk_operations_action_aggregate_do($operation, $oids, $objects, $params, $object_info) { _views_bulk_operations_action_permission($operation); $params[$operation['type']] = $objects; if ($operation['source'] == 'action') { actions_do($operation['callback'], $oids, $params); } else { $args = array_merge(array($oids), $params); call_user_func_array($operation['callback'], $args); } } /** * Helper function to verify access permission to execute operation. */ function _views_bulk_operations_action_permission($operation, $account = NULL) { if (module_exists('actions_permissions')) { $perm = actions_permissions_get_perm($operation['perm label'], $operation['callback']); if (!user_access($perm, $account)) { global $user; watchdog('vbo', 'An attempt by user %user to !perm was blocked due to insufficient permissions.', array( '!perm' => $perm, '%user' => isset($account) ? $account->name : $user->name ), WATCHDOG_ALERT); drupal_access_denied(); exit(); } } // Check against additional permissions. if (!empty($operation['permissions'])) foreach ($operation['permissions'] as $perm) { if (!user_access($perm, $account)) { global $user; watchdog('vbo', 'An attempt by user %user to !perm was blocked due to insufficient permissions.', array( '!perm' => $perm, '%user' => isset($account) ? $account->name : $user->name ), WATCHDOG_ALERT); drupal_access_denied(); exit(); } } } /** * Helper function to verify access permission to operate on object. */ function _views_bulk_operations_object_permission($operation, $object, $object_info, $account = NULL) { // Check against object access permissions. if (!isset($object_info['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 op'] & $bit) { if (!call_user_func($object_info['access'], $op, $object, $account)) { return FALSE; } } } return TRUE; } /** * Helper function to let the configurable action provide its configuration form. */ function _views_bulk_operations_action_form($action, $view, $selection, $settings, $context = array()) { $action_form = $action['callback'] . '_form'; $context = array_merge($context, array('view' => $view, 'selection' => $selection, 'settings' => $settings, 'object_info' => _views_bulk_operations_object_info_for_view($view))); if (isset($action['callback arguments'])) { $context = array_merge($context, $action['callback arguments']); } $form = call_user_func($action_form, $context); return is_array($form) ? $form : array(); } /** * Helper function to let the configurable action validate the form if it provides a validator. */ function _views_bulk_operations_action_validate($action, $form, $form_values) { $action_validate = $action['callback'] . '_validate'; if (function_exists($action_validate)) { call_user_func($action_validate, $form, $form_values); } } /** * 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 call_user_func($action_submit, $form, $form_state); } /** * Helper function to return all object info. */ function _views_bulk_operations_get_object_info($reset = FALSE) { static $object_info = array(); if ($reset || empty($object_info)) { $object_info = module_invoke_all('views_bulk_operations_object_info'); } drupal_alter('views_bulk_operations_object_info', $object_info); return $object_info; } /** * Helper function to return object info for a given view. */ function _views_bulk_operations_object_info_for_view($view) { foreach (_views_bulk_operations_get_object_info() as $object_info) { if ($object_info['base_table'] == $view->base_table) { return $object_info + array( 'context' => '', 'oid' => '', 'access' => NULL, 'hook' => '', 'normalize' => NULL, ); } } watchdog('vbo', 'Could not find object info for view table @table.', array('@table' => $view->base_table), WATCHDOG_ERROR); return NULL; } /** * Helper to include all action files. */ function _views_bulk_operations_load_actions() { static $files = NULL; if (!empty($files)) { return $files; } $files = cache_get('views_bulk_operations_actions'); if (empty($files) || empty($files->data)) { $files = array(); foreach (file_scan_directory(drupal_get_path('module', 'views_bulk_operations') . '/actions', '\.action\.inc$') as $file) { list($files[],) = explode('.', $file->name); } cache_set('views_bulk_operations_actions', $files); } else { $files = $files->data; } foreach ($files as $file) { module_load_include('inc', 'views_bulk_operations', "actions/$file.action"); } return $files; } /** * Helper callback for array_walk(). */ function _views_bulk_operations_get_oid($row, $base_field) { return $row->$base_field; } /** * Helper callback for array_filter(). */ function _views_bulk_operations_filter_invert($item) { return empty($item); } /** * Helper to add needed JavaScript files to VBO. */ function _views_bulk_operations_add_js($plugin, $form_dom_id, $form_id) { static $views = NULL; if (!isset($views[$form_id])) { drupal_add_js(drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js'); drupal_add_js(drupal_get_path('module', 'views_bulk_operations') . '/js/json2.js'); drupal_add_css(drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.css', 'module'); drupal_add_js(array('vbo' => array($form_dom_id => array( 'form_id' => $form_id, 'view_name' => $plugin->view->name, 'view_id' => _views_bulk_operations_view_id($plugin->view), 'options' => $plugin->options, 'ajax_select' => url('views-bulk-operations/js/select'), 'view_path' => url($plugin->view->get_path()), 'total_rows' => $plugin->view->total_rows, ))), 'setting'); $views[$form_id] = TRUE; } } /** * Implement hook_ajax_data_alter(). */ function views_bulk_operations_ajax_data_alter(&$object, $type, $view) { if ($type == 'views' && $view->display_handler->get_option('style_plugin') == 'bulk') { $object->vbo = array( 'view_id' => _views_bulk_operations_view_id($view), 'form_id' => $view->style_plugin->form_id, ); $object->__callbacks[] = 'Drupal.vbo.ajaxViewResponse'; } } /** * Helper function to calculate hash of an object. * * The default "hashing" is to use the object's primary/unique id. This would fail for VBOs that return many rows with * the same primary key (e.g. a *node* view returning all node *comments*). Because we don't know in advance what kind of * hashing is needed, we allow for a module to implement its own hashing via * * hook_views_bulk_operations_object_hash_alter(&$hash, $object, $view). */ function _views_bulk_operations_hash_object($object, $view) { $hash = $object->{$view->base_field}; drupal_alter('views_bulk_operations_object_hash', $hash, $object, $view); return $hash; } /** * Helper function to strip of a view of all decorations. */ function _views_bulk_operations_strip_view($view) { if (isset($view->query->pager)) { $view->query->pager = NULL; } else { $view->set_use_pager(FALSE); } $view->exposed_widgets = NULL; $view->display_handler->set_option('header', ''); $view->display_handler->set_option('footer', ''); $view->display_handler->set_option('use_pager', FALSE); $view->attachment_before = ''; $view->attachment_after = ''; $view->feed_icon = NULL; } /** * Helper function to get a unique ID for a view, taking arguments and exposed filters into consideration. */ function _views_bulk_operations_view_id($view) { // Normalize exposed input. $exposed_input = array(); foreach ($view->filter as $filter) { if (!empty($filter->options['exposed']) && isset($view->exposed_input[ $filter->options['expose']['identifier'] ])) { $exposed_input[ $filter->options['expose']['identifier'] ] = $view->exposed_input[ $filter->options['expose']['identifier'] ]; } } $exposed_input = array_filter($exposed_input); $view_id = md5(serialize(array($view->name, $view->args, $exposed_input))); return $view_id; } /** * Helper function to identify VBO displays for a view. */ function _views_bulk_operations_displays($view) { $displays = array(); foreach ($view->display as $display_id => $display) { if ($display->get_option('style_plugin') == 'bulk') { $displays[] = $display_id; } } return $displays; } /** * Functor to destroy view on exit. */ class views_bulk_operations_destructor { function __construct($view) { $this->view = $view; } function __destruct() { $this->view->destroy(); } private $view; }