views_bulk_operations.module 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223
  1. <?php
  2. /**
  3. * @file
  4. * Allows operations to be performed on items selected in a view.
  5. */
  6. // Access operations.
  7. define('VBO_ACCESS_OP_VIEW', 0x01);
  8. define('VBO_ACCESS_OP_UPDATE', 0x02);
  9. define('VBO_ACCESS_OP_CREATE', 0x04);
  10. define('VBO_ACCESS_OP_DELETE', 0x08);
  11. /**
  12. * Implements hook_action_info().
  13. * Registers custom VBO actions as Drupal actions.
  14. */
  15. function views_bulk_operations_action_info() {
  16. $actions = array();
  17. $files = views_bulk_operations_load_action_includes();
  18. foreach ($files as $filename) {
  19. $action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_info';
  20. $action_info = call_user_func($action_info_fn);
  21. if (is_array($action_info)) {
  22. $actions += $action_info;
  23. }
  24. }
  25. return $actions;
  26. }
  27. /**
  28. * Loads the VBO actions placed in their own include files (under actions/).
  29. *
  30. * @return
  31. * An array of containing filenames of the included actions.
  32. */
  33. function views_bulk_operations_load_action_includes() {
  34. static $loaded = FALSE;
  35. // The list of VBO actions is fairly static, so it's hardcoded for better
  36. // performance (hitting the filesystem with file_scan_directory(), and then
  37. // caching the result has its cost).
  38. $path = drupal_get_path('module', 'views_bulk_operations') . '/actions/';
  39. $files = array(
  40. 'archive.action.inc',
  41. 'argument_selector.action.inc',
  42. 'delete.action.inc',
  43. 'modify.action.inc',
  44. 'script.action.inc',
  45. 'user_roles.action.inc',
  46. );
  47. if (!$loaded) {
  48. foreach ($files as $file) {
  49. include_once $path . $file;
  50. }
  51. $loaded = TRUE;
  52. }
  53. return $files;
  54. }
  55. /**
  56. * Implements hook_cron().
  57. *
  58. * Deletes queue items belonging to VBO active queues (used by VBO's batches)
  59. * that are older than a day (since they can only be a result of VBO crashing
  60. * or the execution being interrupted in some other way). This is the interval
  61. * used to cleanup batches in system_cron(), so it can't be increased.
  62. *
  63. * Note: This code is specific to SystemQueue. Other queue implementations will
  64. * need to do their own garbage collection.
  65. */
  66. function views_bulk_operations_cron() {
  67. db_delete('queue')
  68. ->condition('name', db_like('views_bulk_operations_active_queue_'), 'LIKE')
  69. ->condition('created', REQUEST_TIME - 864000, '<')
  70. ->execute();
  71. }
  72. /**
  73. * Implements of hook_cron_queue_info().
  74. */
  75. function views_bulk_operations_cron_queue_info() {
  76. return array(
  77. 'views_bulk_operations' => array(
  78. 'worker callback' => 'views_bulk_operations_queue_item_process',
  79. 'time' => 30,
  80. ),
  81. );
  82. }
  83. /**
  84. * Implements hook_views_api().
  85. */
  86. function views_bulk_operations_views_api() {
  87. return array(
  88. 'api' => 3,
  89. 'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
  90. );
  91. }
  92. /**
  93. * Implements hook_theme().
  94. */
  95. function views_bulk_operations_theme() {
  96. $themes = array(
  97. 'views_bulk_operations_select_all' => array(
  98. 'variables' => array('view' => NULL, 'enable_select_all_pages' => TRUE),
  99. ),
  100. 'views_bulk_operations_confirmation' => array(
  101. 'variables' => array('rows' => NULL, 'vbo' => NULL),
  102. ),
  103. );
  104. $files = views_bulk_operations_load_action_includes();
  105. foreach ($files as $filename) {
  106. $action_theme_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_theme';
  107. if (function_exists($action_theme_fn)) {
  108. $themes += call_user_func($action_theme_fn);
  109. }
  110. }
  111. return $themes;
  112. }
  113. /**
  114. * Implements hook_ctools_plugin_type().
  115. */
  116. function views_bulk_operations_ctools_plugin_type() {
  117. return array(
  118. 'operation_types' => array(
  119. 'classes' => array(
  120. 'handler',
  121. ),
  122. ),
  123. );
  124. }
  125. /**
  126. * Implements hook_ctools_plugin_directory().
  127. */
  128. function views_bulk_operations_ctools_plugin_directory($module, $plugin) {
  129. if ($module == 'views_bulk_operations') {
  130. return 'plugins/' . $plugin;
  131. }
  132. }
  133. /**
  134. * Fetch metadata for a specific operation type plugin.
  135. *
  136. * @param $operation_type
  137. * Name of the plugin.
  138. *
  139. * @return
  140. * An array with information about the requested operation type plugin.
  141. */
  142. function views_bulk_operations_get_operation_type($operation_type) {
  143. ctools_include('plugins');
  144. return ctools_get_plugins('views_bulk_operations', 'operation_types', $operation_type);
  145. }
  146. /**
  147. * Fetch metadata for all operation type plugins.
  148. *
  149. * @return
  150. * An array of arrays with information about all available operation types.
  151. */
  152. function views_bulk_operations_get_operation_types() {
  153. ctools_include('plugins');
  154. return ctools_get_plugins('views_bulk_operations', 'operation_types');
  155. }
  156. /**
  157. * Gets the info array of an operation from the provider plugin.
  158. *
  159. * @param $operation_id
  160. * The id of the operation for which the info shall be returned, or NULL
  161. * to return an array with info about all operations.
  162. */
  163. function views_bulk_operations_get_operation_info($operation_id = NULL) {
  164. $operations = &drupal_static(__FUNCTION__);
  165. if (!isset($operations)) {
  166. $operations = array();
  167. $plugins = views_bulk_operations_get_operation_types();
  168. foreach ($plugins as $plugin) {
  169. $operations += $plugin['list callback']();
  170. }
  171. uasort($operations, create_function('$a, $b', 'return strcasecmp($a["label"], $b["label"]);'));
  172. }
  173. if (!empty($operation_id)) {
  174. return $operations[$operation_id];
  175. }
  176. else {
  177. return $operations;
  178. }
  179. }
  180. /**
  181. * Returns an operation instance.
  182. *
  183. * @param $operation_id
  184. * The id of the operation to instantiate.
  185. * For example: action::node_publish_action.
  186. * @param $entity_type
  187. * The entity type on which the operation operates.
  188. * @param $options
  189. * Options for this operation (label, operation settings, etc.)
  190. */
  191. function views_bulk_operations_get_operation($operation_id, $entity_type, $options) {
  192. $operations = &drupal_static(__FUNCTION__);
  193. if (!isset($operations[$operation_id])) {
  194. // Intentionally not using views_bulk_operations_get_operation_info() here
  195. // since it's an expensive function that loads all the operations on the
  196. // system, despite the fact that we might only need a few.
  197. $id_fragments = explode('::', $operation_id);
  198. $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
  199. $operation_info = $plugin['list callback']($operation_id);
  200. if ($operation_info) {
  201. $operations[$operation_id] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options);
  202. }
  203. else {
  204. $operations[$operation_id] = FALSE;
  205. }
  206. }
  207. return $operations[$operation_id];
  208. }
  209. /**
  210. * Get all operations that match the current entity type.
  211. *
  212. * @param $entity_type
  213. * Entity type.
  214. * @param $options
  215. * An array of options for all operations, in the form of
  216. * $operation_id => $operation_options.
  217. */
  218. function views_bulk_operations_get_applicable_operations($entity_type, $options) {
  219. $operations = array();
  220. foreach (views_bulk_operations_get_operation_info() as $operation_id => $operation_info) {
  221. if ($operation_info['type'] == $entity_type || $operation_info['type'] == 'entity' || $operation_info['type'] == 'system') {
  222. $options[$operation_id] = !empty($options[$operation_id]) ? $options[$operation_id] : array();
  223. $operations[$operation_id] = views_bulk_operations_get_operation($operation_id, $entity_type, $options[$operation_id]);
  224. }
  225. }
  226. return $operations;
  227. }
  228. /**
  229. * Gets the VBO field if it exists on the passed-in view.
  230. *
  231. * @return
  232. * The field object if found. Otherwise, FALSE.
  233. */
  234. function _views_bulk_operations_get_field($view) {
  235. foreach ($view->field as $field_name => $field) {
  236. if ($field instanceof views_bulk_operations_handler_field_operations) {
  237. // Add in the view object for convenience.
  238. $field->view = $view;
  239. return $field;
  240. }
  241. }
  242. return FALSE;
  243. }
  244. /**
  245. * Implements hook_views_form_substitutions().
  246. */
  247. function views_bulk_operations_views_form_substitutions() {
  248. // Views check_plains the column label, so VBO needs to do the same
  249. // in order for the replace operation to succeed.
  250. $select_all_placeholder = check_plain('<!--views-bulk-operations-select-all-->');
  251. $select_all = array(
  252. '#type' => 'checkbox',
  253. '#default_value' => FALSE,
  254. '#attributes' => array('class' => array('vbo-table-select-all')),
  255. );
  256. return array(
  257. $select_all_placeholder => drupal_render($select_all),
  258. );
  259. }
  260. /**
  261. * Implements hook_form_alter().
  262. */
  263. function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
  264. if (strpos($form_id, 'views_form_') === 0) {
  265. $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  266. }
  267. // Not a VBO-enabled views form.
  268. if (empty($vbo)) {
  269. return;
  270. }
  271. // Cache the built form to prevent it from being rebuilt prior to validation
  272. // and submission, which could lead to data being processed incorrectly,
  273. // because the views rows (and thus, the form elements as well) have changed
  274. // in the meantime. Matching views issue: http://drupal.org/node/1473276.
  275. $form_state['cache'] = TRUE;
  276. if (empty($vbo->view->override_url)) {
  277. // If the VBO view is embedded using views_embed_view(), or in a block,
  278. // $view->get_url() doesn't point to the current page, which means that
  279. // the form doesn't get processed.
  280. if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
  281. $vbo->view->override_url = $_GET['q'];
  282. // We are changing the override_url too late, the form action was already
  283. // set by Views to the previous URL, so it needs to be overriden as well.
  284. $query = drupal_get_query_parameters($_GET, array('q'));
  285. $form['#action'] = url($_GET['q'], array('query' => $query));
  286. }
  287. }
  288. // Add basic VBO functionality.
  289. if ($form_state['step'] == 'views_form_views_form') {
  290. $form = views_bulk_operations_form($form, $form_state, $vbo);
  291. }
  292. // Give other modules a chance to alter the form.
  293. drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
  294. }
  295. /**
  296. * Returns the 'select all' div that gets inserted below the table header row
  297. * (for table style plugins with grouping disabled), or above the view results
  298. * (for non-table style plugins), providing a choice between selecting items
  299. * on the current page, and on all pages.
  300. *
  301. * The actual insertion is done by JS, matching the degradation behavior
  302. * of Drupal core (no JS - no select all).
  303. */
  304. function theme_views_bulk_operations_select_all($variables) {
  305. $view = $variables['view'];
  306. $enable_select_all_pages = $variables['enable_select_all_pages'];
  307. $form = array();
  308. if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
  309. if (!$enable_select_all_pages) {
  310. return '';
  311. }
  312. $wrapper_class = 'vbo-table-select-all-markup';
  313. $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
  314. $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
  315. $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
  316. $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));
  317. $form['select_all_pages'] = array(
  318. '#type' => 'button',
  319. '#attributes' => array('class' => array('vbo-table-select-all-pages')),
  320. '#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
  321. '#prefix' => '<span class="vbo-table-this-page">' . $this_page . ' &nbsp;',
  322. '#suffix' => '</span>',
  323. );
  324. $form['select_this_page'] = array(
  325. '#type' => 'button',
  326. '#attributes' => array('class' => array('vbo-table-select-this-page')),
  327. '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
  328. '#prefix' => '<span class="vbo-table-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
  329. '#suffix' => '</span>',
  330. );
  331. }
  332. else {
  333. $wrapper_class = 'vbo-select-all-markup';
  334. $form['select_all'] = array(
  335. '#type' => 'fieldset',
  336. '#attributes' => array('class' => array('vbo-fieldset-select-all')),
  337. );
  338. $form['select_all']['this_page'] = array(
  339. '#type' => 'checkbox',
  340. '#title' => t('Select all items on this page'),
  341. '#default_value' => '',
  342. '#attributes' => array('class' => array('vbo-select-this-page')),
  343. );
  344. if ($enable_select_all_pages) {
  345. $form['select_all']['or'] = array(
  346. '#type' => 'markup',
  347. '#markup' => '<em>OR</em>',
  348. );
  349. $form['select_all']['all_pages'] = array(
  350. '#type' => 'checkbox',
  351. '#title' => t('Select all items on all pages'),
  352. '#default_value' => '',
  353. '#attributes' => array('class' => array('vbo-select-all-pages')),
  354. );
  355. }
  356. }
  357. $output = '<div class="' . $wrapper_class . '">';
  358. $output .= drupal_render($form);
  359. $output .= '</div>';
  360. return $output;
  361. }
  362. /**
  363. * Extend the views_form multistep form with elements for executing an operation.
  364. */
  365. function views_bulk_operations_form($form, &$form_state, $vbo) {
  366. $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
  367. $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
  368. $form['#prefix'] = '<div class="vbo-views-form">';
  369. $form['#suffix'] = '</div>';
  370. // Force browser to reload the page if Back is hit.
  371. if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
  372. drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+
  373. }
  374. else {
  375. drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers
  376. }
  377. // Set by JS to indicate that all rows on all pages are selected.
  378. $form['select_all'] = array(
  379. '#type' => 'hidden',
  380. '#attributes' => array('class' => 'select-all-rows'),
  381. '#default_value' => FALSE,
  382. );
  383. // The submit button added by Views Form API might be used by a non-VBO Views
  384. // Form handler. If there's no such handler on the view, hide the button.
  385. $has_other_views_form_handlers = FALSE;
  386. foreach ($vbo->view->field as $field) {
  387. if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
  388. if (!($field instanceof views_bulk_operations_handler_field_operations)) {
  389. $has_other_views_form_handlers = TRUE;
  390. }
  391. }
  392. }
  393. if (!$has_other_views_form_handlers) {
  394. $form['actions']['submit']['#access'] = FALSE;
  395. }
  396. $form['select'] = array(
  397. '#type' => 'fieldset',
  398. '#title' => t('Operations'),
  399. '#collapsible' => TRUE,
  400. '#attributes' => array('class' => array('container-inline')),
  401. );
  402. if ($vbo->get_vbo_option('display_type') == 0) {
  403. $options = array(0 => t('- Choose an operation -'));
  404. foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
  405. $options[$operation_id] = $operation->label();
  406. }
  407. // Create dropdown and submit button.
  408. $form['select']['operation'] = array(
  409. '#type' => 'select',
  410. '#options' => $options,
  411. );
  412. $form['select']['submit'] = array(
  413. '#type' => 'submit',
  414. '#value' => t('Execute'),
  415. '#validate' => array('views_bulk_operations_form_validate'),
  416. '#submit' => array('views_bulk_operations_form_submit'),
  417. );
  418. }
  419. else {
  420. // Create buttons for operations.
  421. foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
  422. $form['select'][$operation_id] = array(
  423. '#type' => 'submit',
  424. '#value' => $operation->label(),
  425. '#validate' => array('views_bulk_operations_form_validate'),
  426. '#submit' => array('views_bulk_operations_form_submit'),
  427. '#operation_id' => $operation_id,
  428. );
  429. }
  430. }
  431. // Adds the "select all" functionality if the view has results.
  432. // If the view is using a table style plugin, the markup gets moved to
  433. // a table row below the header.
  434. // If we are using radio buttons, we don't use select all at all.
  435. if (!empty($vbo->view->result) && !$vbo->get_vbo_option('force_single')) {
  436. $enable_select_all_pages = FALSE;
  437. // If the view is paginated, and "select all items on all pages" is
  438. // enabled, tell that to the theme function.
  439. if (count($vbo->view->result) != $vbo->view->total_rows && $vbo->get_vbo_option('enable_select_all_pages')) {
  440. $enable_select_all_pages = TRUE;
  441. }
  442. $form['select_all_markup'] = array(
  443. '#type' => 'markup',
  444. '#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view, 'enable_select_all_pages' => $enable_select_all_pages)),
  445. );
  446. }
  447. return $form;
  448. }
  449. /**
  450. * Validation callback for the first step of the VBO form.
  451. */
  452. function views_bulk_operations_form_validate($form, &$form_state) {
  453. $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  454. if (!empty($form_state['triggering_element']['#operation_id'])) {
  455. $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
  456. }
  457. if (!$form_state['values']['operation']) {
  458. form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
  459. }
  460. $field_name = $vbo->options['id'];
  461. $selection = _views_bulk_operations_get_selection($vbo, $form_state);
  462. if (!$selection) {
  463. form_set_error($field_name, t('Please select at least one item.'));
  464. }
  465. }
  466. /**
  467. * Multistep form callback for the "configure" step.
  468. */
  469. function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
  470. $vbo = _views_bulk_operations_get_field($view);
  471. $operation = $form_state['operation'];
  472. drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
  473. $context = array(
  474. 'entity_type' => $vbo->get_entity_type(),
  475. // Pass the View along.
  476. // Has no performance penalty since objects are passed by reference,
  477. // but needing the full views object in a core action is in most cases
  478. // a sign of a wrong implementation. Do it only if you have to.
  479. 'view' => $view,
  480. );
  481. $form += $operation->form($form, $form_state, $context);
  482. $query = drupal_get_query_parameters($_GET, array('q'));
  483. $form['actions'] = array(
  484. '#type' => 'container',
  485. '#attributes' => array('class' => array('form-actions')),
  486. '#weight' => 999,
  487. );
  488. $form['actions']['submit'] = array(
  489. '#type' => 'submit',
  490. '#value' => t('Next'),
  491. '#validate' => array('views_bulk_operations_config_form_validate'),
  492. '#submit' => array('views_bulk_operations_form_submit'),
  493. '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
  494. );
  495. return $form;
  496. }
  497. /**
  498. * Validation callback for the "configure" step.
  499. * Gives the operation a chance to validate its config form.
  500. */
  501. function views_bulk_operations_config_form_validate($form, &$form_state) {
  502. $operation = &$form_state['operation'];
  503. $operation->formValidate($form, $form_state);
  504. }
  505. /**
  506. * Multistep form callback for the "confirm" step.
  507. */
  508. function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) {
  509. $vbo = _views_bulk_operations_get_field($view);
  510. $operation = $form_state['operation'];
  511. $rows = $form_state['selection'];
  512. $query = drupal_get_query_parameters($_GET, array('q'));
  513. $form = confirm_form($form,
  514. t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation->label())),
  515. array('path' => $view->get_url(), 'query' => $query),
  516. theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo, 'select_all_pages' => $form_state['select_all_pages']))
  517. );
  518. // Add VBO's submit handler to the Confirm button added by config_form().
  519. $form['actions']['submit']['#submit'] = array('views_bulk_operations_form_submit');
  520. return $form;
  521. }
  522. /**
  523. * Theme function to show the confirmation page before executing the operation.
  524. */
  525. function theme_views_bulk_operations_confirmation($variables) {
  526. $select_all_pages = $variables['select_all_pages'];
  527. $vbo = $variables['vbo'];
  528. $entity_type = $vbo->get_entity_type();
  529. $rows = $variables['rows'];
  530. $items = array();
  531. // Load the entities from the current page, and show their titles.
  532. $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
  533. foreach ($entities as $entity) {
  534. $items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
  535. }
  536. // All rows on all pages have been selected, so show a count of additional items.
  537. if ($select_all_pages) {
  538. $more_count = $vbo->view->total_rows - count($vbo->view->result);
  539. $items[] = t('...and <strong>!count</strong> more.', array('!count' => $more_count));
  540. }
  541. $count = format_plural(count($entities), 'item', '@count items');
  542. $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following <strong>!count</strong>:', array('!count' => $count))));
  543. return $output;
  544. }
  545. /**
  546. * Goes through the submitted values, and returns
  547. * an array of selected rows, in the form of
  548. * $row_index => $entity_id.
  549. */
  550. function _views_bulk_operations_get_selection($vbo, $form_state) {
  551. $selection = array();
  552. $field_name = $vbo->options['id'];
  553. if (!empty($form_state['values'][$field_name])) {
  554. // If using "force single", the selection needs to be converted to an array.
  555. if (is_array($form_state['values'][$field_name])) {
  556. $selection = array_filter($form_state['values'][$field_name]);
  557. }
  558. else {
  559. $selection = array($form_state['values'][$field_name]);
  560. }
  561. }
  562. return $selection;
  563. }
  564. /**
  565. * Submit handler for all steps of the VBO multistep form.
  566. */
  567. function views_bulk_operations_form_submit($form, &$form_state) {
  568. $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
  569. $entity_type = $vbo->get_entity_type();
  570. switch ($form_state['step']) {
  571. case 'views_form_views_form':
  572. $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
  573. $form_state['select_all_pages'] = $form_state['values']['select_all'];
  574. $options = $vbo->get_operation_options($form_state['values']['operation']);
  575. $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
  576. if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
  577. break; // Go directly to execution
  578. }
  579. $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
  580. $form_state['rebuild'] = TRUE;
  581. return;
  582. case 'views_bulk_operations_config_form':
  583. $form_state['step'] = 'views_bulk_operations_confirm_form';
  584. $operation = &$form_state['operation'];
  585. $operation->formSubmit($form, $form_state);
  586. if ($operation->getAdminOption('skip_confirmation')) {
  587. break; // Go directly to execution
  588. }
  589. $form_state['rebuild'] = TRUE;
  590. return;
  591. case 'views_bulk_operations_confirm_form':
  592. break;
  593. }
  594. // Execute the operation.
  595. views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);
  596. // Redirect.
  597. $query = drupal_get_query_parameters($_GET, array('q'));
  598. $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
  599. }
  600. /**
  601. * Entry point for executing the chosen operation upon selected rows.
  602. *
  603. * If the selected operation is an aggregate operation (requiring all selected
  604. * items to be passed at the same time), or the execution is being triggered
  605. * through Drush, the operation is executed directly.
  606. * This means that there is no batching & queueing, the PHP execution
  607. * time limit is ignored (if allowed), all selected entities are loaded and
  608. * processed.
  609. *
  610. * Otherwise, the selected entity ids are divided into groups not larger than
  611. * $entity_load_capacity, and enqueued for processing.
  612. * If all items on all pages should be processed, a batch job runs that
  613. * collects and enqueues the items from all pages of the view, page by page.
  614. *
  615. * Based on the "Enqueue the operation instead of executing it directly"
  616. * VBO field setting, the newly filled queue is either processed at cron
  617. * time by the VBO worker function, or right away in a new batch job.
  618. *
  619. * @param $vbo
  620. * The VBO field, containing a reference to the view in $vbo->view.
  621. * @param $operation
  622. * The operation object.
  623. * @param $selection
  624. * An array in the form of $row_index => $entity_id.
  625. * @param $select_all_pages
  626. * Whether all items on all pages should be selected.
  627. */
  628. function views_bulk_operations_execute($vbo, $operation, $selection, $select_all_pages = FALSE) {
  629. global $user;
  630. // This is an aggregate operation, and it requires all rows to be selected.
  631. // Try to load them without a batch.
  632. if ($operation->aggregate() && $select_all_pages) {
  633. views_bulk_operations_direct_adjust($selection, $vbo);
  634. }
  635. // Options that affect execution.
  636. $options = array(
  637. 'revision' => $vbo->revision,
  638. 'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
  639. );
  640. // Create an array of rows in the needed format.
  641. $rows = array();
  642. $current = 1;
  643. foreach ($selection as $row_index => $entity_id) {
  644. $rows[$row_index] = array(
  645. 'entity_id' => $entity_id,
  646. 'views_row' => array(),
  647. // Some operations rely on knowing the position of the current item
  648. // in the execution set (because of specific things that need to be done
  649. // at the beginning or the end of the set).
  650. 'position' => array(
  651. 'current' => $current++,
  652. 'total' => count($selection),
  653. ),
  654. );
  655. // Some operations require full selected rows.
  656. if ($operation->needsRows()) {
  657. $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
  658. }
  659. }
  660. // Aggregate operations can only be executed directly.
  661. if ($operation->aggregate()) {
  662. views_bulk_operations_direct_process($operation, $rows, $options);
  663. return;
  664. }
  665. // Determine the correct queue to use.
  666. if ($operation->getAdminOption('postpone_processing')) {
  667. // Use the site queue processed on cron.
  668. $queue_name = 'views_bulk_operations';
  669. }
  670. else {
  671. // Use the active queue processed immediately by Batch API.
  672. $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
  673. }
  674. $batch = array(
  675. 'operations' => array(),
  676. 'finished' => 'views_bulk_operations_execute_finished',
  677. 'progress_message' => '',
  678. 'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
  679. );
  680. // All items on all pages should be selected, add a batch job to gather
  681. // and enqueue them.
  682. if ($select_all_pages && $vbo->view->query->pager->has_more_records()) {
  683. $total_rows = $vbo->view->total_rows;
  684. // Pass information needed to recreate the view inside a batch,
  685. // to avoid having to serialize the current object (which is expensive).
  686. $view_info = array(
  687. 'name' => $vbo->view->name,
  688. 'display' => $vbo->view->current_display,
  689. 'arguments' => $vbo->view->args,
  690. 'exposed_input' => $vbo->view->get_exposed_input(),
  691. 'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
  692. );
  693. $batch['operations'][] = array(
  694. 'views_bulk_operations_adjust_selection', array($view_info, $queue_name, $operation, $options),
  695. );
  696. }
  697. else {
  698. $total_rows = count($rows);
  699. // We have all the items that we need, enqueue them right away.
  700. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
  701. // Provide a status message to the user, since this is the last step if
  702. // processing is postponed.
  703. if ($operation->getAdminOption('postpone_processing')) {
  704. drupal_set_message(t('Enqueued the selected operation (%operation).', array(
  705. '%operation' => $operation->label(),
  706. )));
  707. }
  708. }
  709. // Processing is not postponed, add a batch job to process the queue.
  710. if (!$operation->getAdminOption('postpone_processing')) {
  711. $batch['operations'][] = array(
  712. 'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
  713. );
  714. }
  715. // If there are batch jobs to be processed, create the batch set.
  716. if (count($batch['operations'])) {
  717. batch_set($batch);
  718. }
  719. }
  720. /**
  721. * Batch API callback: loads the view page by page and enqueues all items.
  722. *
  723. * @param $view_info
  724. * An array of information about the view, used to recreate and re-execute it.
  725. * @param $queue_name
  726. * The name of the queue to which the items should be added.
  727. * @param $operation
  728. * The operation object.
  729. * @param $options
  730. * An array of options that affect execution (revision, entity_load_capacity).
  731. * Passed along with each new queue item.
  732. */
  733. function views_bulk_operations_adjust_selection($view_info, $queue_name, $operation, $options, &$context) {
  734. if (!isset($context['sandbox']['progress'])) {
  735. $context['sandbox']['progress'] = 0;
  736. $context['sandbox']['max'] = 0;
  737. }
  738. $view = views_get_view($view_info['name']);
  739. $view->set_exposed_input($view_info['exposed_input']);
  740. $view->set_arguments($view_info['arguments']);
  741. $view->set_display($view_info['display']);
  742. $view->set_offset($context['sandbox']['progress']);
  743. $view->build();
  744. $view->execute($view_info['display']);
  745. // Note the total number of rows.
  746. if (empty($context['sandbox']['max'])) {
  747. $context['sandbox']['max'] = $view->total_rows;
  748. }
  749. $vbo = _views_bulk_operations_get_field($view);
  750. $rows = array();
  751. foreach ($view->result as $row_index => $result) {
  752. $rows[$row_index] = array(
  753. 'entity_id' => $vbo->get_value($result),
  754. 'views_row' => array(),
  755. 'position' => array(
  756. 'current' => ++$context['sandbox']['progress'],
  757. 'total' => $view->total_rows,
  758. ),
  759. );
  760. // Some operations require full selected rows.
  761. if ($operation->needsRows()) {
  762. $rows[$row_index]['views_row'] = $result;
  763. }
  764. }
  765. // Enqueue the gathered rows.
  766. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
  767. if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
  768. // Provide an estimation of the completion level we've reached.
  769. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  770. $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
  771. }
  772. else {
  773. // Provide a status message to the user if this is the last batch job.
  774. if ($operation->getAdminOption('postpone_processing')) {
  775. $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
  776. '%operation' => $operation->label(),
  777. ));
  778. }
  779. }
  780. }
  781. /**
  782. * Divides the passed rows into groups and enqueues each group for processing
  783. *
  784. * @param $queue_name
  785. * The name of the queue.
  786. * @param $rows
  787. * The rows to be enqueued.
  788. * @param $operation
  789. * The object representing the current operation.
  790. * Passed along with each new queue item.
  791. * @param $options
  792. * An array of options that affect execution (revision, entity_load_capacity).
  793. * Passed along with each new queue item.
  794. */
  795. function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
  796. global $user;
  797. $queue = DrupalQueue::get($queue_name, TRUE);
  798. $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
  799. foreach ($row_groups as $row_group) {
  800. $entity_ids = array();
  801. foreach ($row_group as $row) {
  802. $entity_ids[] = $row['entity_id'];
  803. }
  804. $job = array(
  805. 'title' => t('Perform %operation on @type !entity_ids.', array(
  806. '%operation' => $operation->label(),
  807. '@type' => $operation->entityType,
  808. '!entity_ids' => implode(',', $entity_ids),
  809. )),
  810. 'uid' => $user->uid,
  811. 'arguments' => array($row_group, $operation, $options),
  812. );
  813. $queue->createItem($job);
  814. }
  815. }
  816. /**
  817. * Batch API callback: processes the active queue.
  818. *
  819. * @param $queue_name
  820. * The name of the queue to process.
  821. * @param $operation
  822. * The object representing the current operation.
  823. * @param $total_rows
  824. * The total number of processable items (across all queue items), used
  825. * to report progress.
  826. *
  827. * @see views_bulk_operations_queue_item_process()
  828. */
  829. function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
  830. static $queue;
  831. // It is still possible to hit the time limit.
  832. @set_time_limit(0);
  833. // Prepare the sandbox.
  834. if (!isset($context['sandbox']['progress'])) {
  835. $context['sandbox']['progress'] = 0;
  836. $context['sandbox']['max'] = $total_rows;
  837. $context['results']['log'] = array();
  838. }
  839. // Instantiate the queue.
  840. if (!isset($queue)) {
  841. $queue = DrupalQueue::get($queue_name, TRUE);
  842. }
  843. // Process the queue as long as it has items for us.
  844. $queue_item = $queue->claimItem(3600);
  845. if ($queue_item) {
  846. // Process the queue item, and update the progress count.
  847. views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
  848. $queue->deleteItem($queue_item);
  849. // Provide an estimation of the completion level we've reached.
  850. $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
  851. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  852. $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
  853. }
  854. if (!$queue_item || $context['finished'] === 1) {
  855. // All done. Provide a status message to the user.
  856. $context['results']['log'][] = t('Performed %operation on @items.', array(
  857. '%operation' => $operation->label(),
  858. '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
  859. ));
  860. }
  861. }
  862. /**
  863. * Processes the provided queue item.
  864. *
  865. * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
  866. * to process the site queue, as well as by
  867. * views_bulk_operations_active_queue_process() to process the active queue.
  868. *
  869. * @param $queue_item_arguments
  870. * The arguments of the queue item to process.
  871. * @param $log
  872. * An injected array of log messages, to be modified by reference.
  873. * If NULL, the function defaults to using watchdog.
  874. */
  875. function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
  876. list($row_group, $operation, $options) = $queue_item_data['arguments'];
  877. $account = user_load($queue_item_data['uid']);
  878. $entity_type = $operation->entityType;
  879. $entity_ids = array();
  880. foreach ($row_group as $row_id => $row) {
  881. $entity_ids[] = $row['entity_id'];
  882. }
  883. $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  884. foreach ($row_group as $row_id => $row) {
  885. $entity_id = $row['entity_id'];
  886. // A matching entity couldn't be loaded. Skip this item.
  887. if (!isset($entities[$entity_id])) {
  888. continue;
  889. }
  890. $entity = $entities[$entity_id];
  891. // If the current entity can't be accessed, skip it and log a notice.
  892. if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
  893. $message = 'Skipped %operation on @type %title due to insufficient permissions.';
  894. $arguments = array(
  895. '%operation' => $operation->label(),
  896. '@type' => $entity_type,
  897. '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
  898. );
  899. if ($log) {
  900. $log[] = t($message, $arguments);
  901. }
  902. else {
  903. watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
  904. }
  905. continue;
  906. }
  907. $operation_context = array(
  908. 'progress' => $row['position'],
  909. 'rows' => $row['views_row'],
  910. );
  911. $operation->execute($entity, $operation_context);
  912. unset($row_group[$row_id]);
  913. }
  914. }
  915. /**
  916. * Adjusts the selection for the direct execution method.
  917. *
  918. * Just like the direct method itself, this is legacy code, used only for
  919. * aggregate actions.
  920. */
  921. function views_bulk_operations_direct_adjust(&$selection, $vbo) {
  922. // Adjust selection to select all rows across pages.
  923. $view = views_get_view($vbo->view->name);
  924. $view->set_exposed_input($vbo->view->get_exposed_input());
  925. $view->set_arguments($vbo->view->args);
  926. $view->set_display($vbo->view->current_display);
  927. $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
  928. $view->build();
  929. // Unset every field except the VBO one (which holds the entity id).
  930. // That way the performance hit becomes much smaller, because there is no
  931. // chance of views_handler_field_field::post_execute() firing entity_load().
  932. foreach ($view->field as $field_name => $field) {
  933. if ($field_name != $vbo->options['id']) {
  934. unset($view->field[$field_name]);
  935. }
  936. }
  937. $view->execute($vbo->view->current_display);
  938. $results = array();
  939. foreach ($view->result as $row_index => $result) {
  940. $results[$row_index] = $vbo->get_value($result);
  941. }
  942. $selection = $results;
  943. }
  944. /**
  945. * Processes the passed rows directly (without batching and queueing).
  946. *
  947. * This is a legacy function that is now only used for aggregate operations.
  948. */
  949. function views_bulk_operations_direct_process($operation, $rows, $options) {
  950. @set_time_limit(0);
  951. // Prepare an array of status information. Imitates the Batch API naming
  952. // for consistency. Passed to views_bulk_operations_execute_finished().
  953. $context = array();
  954. $context['results']['progress'] = 0;
  955. $context['results']['log'] = array();
  956. $entity_type = $operation->entityType;
  957. $entity_ids = array();
  958. foreach ($rows as $row_index => $row) {
  959. $entity_ids[] = $row['entity_id'];
  960. }
  961. $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  962. foreach ($entities as $id => $entity) {
  963. if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
  964. $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
  965. '%operation' => $operation->label(),
  966. '@type' => $entity_type,
  967. '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
  968. ));
  969. unset($entities[$id]);
  970. }
  971. }
  972. if (empty($entities)) {
  973. return;
  974. }
  975. // Pass the selected rows to the operation if needed.
  976. $operation_context = array();
  977. if ($operation->needsRows()) {
  978. $operation_context['rows'] = array();
  979. foreach ($rows as $row_index => $row) {
  980. $operation_context['rows'][$row_index] = $row['views_row'];
  981. }
  982. }
  983. $operation->execute($entities, $operation_context);
  984. $context['results']['log'][] = t('Performed aggregate %operation on @items.', array(
  985. '%operation' => $operation->label(),
  986. '@items' => format_plural(count($entities), '1 item', '@count items'),
  987. ));
  988. $context['results']['progress'] += count($entities);
  989. views_bulk_operations_execute_finished(TRUE, $context['results'], array());
  990. }
  991. /**
  992. * Helper function that runs after the execution process is complete.
  993. */
  994. function views_bulk_operations_execute_finished($success, $results, $operations) {
  995. if ($success) {
  996. if (count($results['log']) > 1) {
  997. $message = theme('item_list', array('items' => $results['log']));
  998. }
  999. else {
  1000. $message = reset($results['log']);
  1001. }
  1002. }
  1003. else {
  1004. // An error occurred.
  1005. // $operations contains the operations that remained unprocessed.
  1006. $error_operation = reset($operations);
  1007. $message = t('An error occurred while processing @operation with arguments: @arguments',
  1008. array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
  1009. }
  1010. _views_bulk_operations_log($message);
  1011. }
  1012. /**
  1013. * Helper function to verify access permission to operate on an entity.
  1014. */
  1015. function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
  1016. if (!entity_type_supports($entity_type, 'access')) {
  1017. return TRUE;
  1018. }
  1019. $access_ops = array(
  1020. VBO_ACCESS_OP_VIEW => 'view',
  1021. VBO_ACCESS_OP_UPDATE => 'update',
  1022. VBO_ACCESS_OP_CREATE => 'create',
  1023. VBO_ACCESS_OP_DELETE => 'delete',
  1024. );
  1025. foreach ($access_ops as $bit => $op) {
  1026. if ($operation->getAccessMask() & $bit) {
  1027. if (!entity_access($op, $entity_type, $entity, $account)) {
  1028. return FALSE;
  1029. }
  1030. }
  1031. }
  1032. return TRUE;
  1033. }
  1034. /**
  1035. * Loads multiple entities by their entity or revision ids, and returns them,
  1036. * keyed by the id used for loading.
  1037. */
  1038. function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
  1039. if (!$revision) {
  1040. $entities = entity_load($entity_type, $ids);
  1041. }
  1042. else {
  1043. // D7 can't load multiple entities by revision_id. Lovely.
  1044. $info = entity_get_info($entity_type);
  1045. $entities = array();
  1046. foreach ($ids as $revision_id) {
  1047. $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
  1048. $entities[$revision_id] = reset($loaded_entities);
  1049. }
  1050. }
  1051. return $entities;
  1052. }
  1053. /**
  1054. * Label function for entities.
  1055. * Core entities don't declare the "label" key, so entity_label() fails,
  1056. * and a fallback is needed. This function provides that fallback.
  1057. */
  1058. function _views_bulk_operations_entity_label($entity_type, $entity) {
  1059. $label = entity_label($entity_type, $entity);
  1060. if (!$label) {
  1061. $entity_info = entity_get_info($entity_type);
  1062. $id_key = $entity_info['entity keys']['id'];
  1063. // Many entity types (e.g. "user") have a name which fits the label perfectly.
  1064. if (isset($entity->name)) {
  1065. $label = $entity->name;
  1066. }
  1067. elseif (isset($entity->{$id_key})) {
  1068. // Fallback to the id key.
  1069. $label = $entity->{$id_key};
  1070. }
  1071. }
  1072. return $label;
  1073. }
  1074. /**
  1075. * Helper function to report an error.
  1076. */
  1077. function _views_bulk_operations_report_error($msg, $arg) {
  1078. watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
  1079. if (function_exists('drush_set_error')) {
  1080. drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
  1081. }
  1082. }
  1083. /**
  1084. * Display a message to the user through the relevant function.
  1085. */
  1086. function _views_bulk_operations_log($msg) {
  1087. // Is VBO being run through drush?
  1088. if (function_exists('drush_log')) {
  1089. drush_log(strip_tags($msg), 'ok');
  1090. }
  1091. else {
  1092. drupal_set_message($msg);
  1093. }
  1094. }