views_bulk_operations.module 46 KB

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