views_bulk_operations.module 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338
  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. $force_single = $vbo->get_vbo_option('force_single');
  713. $execute_directly = ($aggregate || $skip_batching || $force_single);
  714. // Try to load all rows without a batch if needed.
  715. if ($execute_directly && $select_all_pages) {
  716. views_bulk_operations_direct_adjust($selection, $vbo);
  717. }
  718. // Options that affect execution.
  719. $options = array(
  720. 'revision' => $vbo->revision,
  721. 'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
  722. // The information needed to recreate the view, to avoid serializing the
  723. // whole object. Passed to the executed operation. Also used by
  724. // views_bulk_operations_adjust_selection().
  725. 'view_info' => array(
  726. 'name' => $vbo->view->name,
  727. 'display' => $vbo->view->current_display,
  728. 'arguments' => $vbo->view->args,
  729. 'exposed_input' => $vbo->view->get_exposed_input(),
  730. ),
  731. );
  732. // Create an array of rows in the needed format.
  733. $rows = array();
  734. $current = 1;
  735. foreach ($selection as $row_index => $entity_id) {
  736. $rows[$row_index] = array(
  737. 'entity_id' => $entity_id,
  738. 'views_row' => array(),
  739. // Some operations rely on knowing the position of the current item
  740. // in the execution set (because of specific things that need to be done
  741. // at the beginning or the end of the set).
  742. 'position' => array(
  743. 'current' => $current++,
  744. 'total' => count($selection),
  745. ),
  746. );
  747. // Some operations require full selected rows.
  748. if ($operation->needsRows()) {
  749. $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
  750. }
  751. }
  752. if ($execute_directly) {
  753. // Execute the operation directly and stop here.
  754. views_bulk_operations_direct_process($operation, $rows, $options);
  755. return;
  756. }
  757. // Determine the correct queue to use.
  758. if ($operation->getAdminOption('postpone_processing')) {
  759. // Use the site queue processed on cron.
  760. $queue_name = 'views_bulk_operations';
  761. }
  762. else {
  763. // Use the active queue processed immediately by Batch API.
  764. $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
  765. }
  766. $batch = array(
  767. 'operations' => array(),
  768. 'finished' => 'views_bulk_operations_execute_finished',
  769. 'progress_message' => '',
  770. 'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
  771. );
  772. // All items on all pages should be selected, add a batch job to gather
  773. // and enqueue them.
  774. if ($select_all_pages && ($vbo->view->query->pager->has_more_records() || $vbo->view->query->pager->get_current_page() > 0)) {
  775. $total_rows = $vbo->view->total_rows;
  776. $batch['operations'][] = array(
  777. 'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options),
  778. );
  779. }
  780. else {
  781. $total_rows = count($rows);
  782. // We have all the items that we need, enqueue them right away.
  783. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
  784. // Provide a status message to the user, since this is the last step if
  785. // processing is postponed.
  786. if ($operation->getAdminOption('postpone_processing')) {
  787. drupal_set_message(t('Enqueued the selected operation (%operation).', array(
  788. '%operation' => $operation->label(),
  789. )));
  790. }
  791. }
  792. // Processing is not postponed, add a batch job to process the queue.
  793. if (!$operation->getAdminOption('postpone_processing')) {
  794. $batch['operations'][] = array(
  795. 'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
  796. );
  797. }
  798. // If there are batch jobs to be processed, create the batch set.
  799. if (count($batch['operations'])) {
  800. batch_set($batch);
  801. }
  802. }
  803. /**
  804. * Batch API callback: loads the view page by page and enqueues all items.
  805. *
  806. * @param $queue_name
  807. * The name of the queue to which the items should be added.
  808. * @param $operation
  809. * The operation object.
  810. * @param $options
  811. * An array of options that affect execution (revision, entity_load_capacity,
  812. * view_info). Passed along with each new queue item.
  813. */
  814. function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) {
  815. if (!isset($context['sandbox']['progress'])) {
  816. $context['sandbox']['progress'] = 0;
  817. $context['sandbox']['max'] = 0;
  818. }
  819. $view_info = $options['view_info'];
  820. $view = views_get_view($view_info['name']);
  821. $view->set_exposed_input($view_info['exposed_input']);
  822. $view->set_arguments($view_info['arguments']);
  823. $view->set_display($view_info['display']);
  824. $view->set_offset($context['sandbox']['progress']);
  825. $view->build();
  826. $view->execute($view_info['display']);
  827. // Note the total number of rows.
  828. if (empty($context['sandbox']['max'])) {
  829. $context['sandbox']['max'] = $view->total_rows;
  830. }
  831. $vbo = _views_bulk_operations_get_field($view);
  832. // Call views_handler_field_entity::pre_render() to get the entities.
  833. $vbo->pre_render($view->result);
  834. $rows = array();
  835. foreach ($view->result as $row_index => $result) {
  836. // Set the row index.
  837. $view->row_index = $row_index;
  838. $rows[$row_index] = array(
  839. 'entity_id' => $vbo->get_value($result, $vbo->real_field),
  840. 'views_row' => array(),
  841. 'position' => array(
  842. 'current' => ++$context['sandbox']['progress'],
  843. 'total' => $context['sandbox']['max'],
  844. ),
  845. );
  846. // Some operations require full selected rows.
  847. if ($operation->needsRows()) {
  848. $rows[$row_index]['views_row'] = $result;
  849. }
  850. }
  851. // Enqueue the gathered rows.
  852. views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
  853. if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
  854. // Provide an estimation of the completion level we've reached.
  855. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  856. $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
  857. }
  858. else {
  859. // Provide a status message to the user if this is the last batch job.
  860. if ($operation->getAdminOption('postpone_processing')) {
  861. $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
  862. '%operation' => $operation->label(),
  863. ));
  864. }
  865. }
  866. }
  867. /**
  868. * Divides the passed rows into groups and enqueues each group for processing
  869. *
  870. * @param $queue_name
  871. * The name of the queue.
  872. * @param $rows
  873. * The rows to be enqueued.
  874. * @param $operation
  875. * The object representing the current operation.
  876. * Passed along with each new queue item.
  877. * @param $options
  878. * An array of options that affect execution (revision, entity_load_capacity).
  879. * Passed along with each new queue item.
  880. */
  881. function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
  882. global $user;
  883. $queue = DrupalQueue::get($queue_name, TRUE);
  884. $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
  885. foreach ($row_groups as $row_group) {
  886. $entity_ids = array();
  887. foreach ($row_group as $row) {
  888. $entity_ids[] = $row['entity_id'];
  889. }
  890. $job = array(
  891. 'title' => t('Perform %operation on @type !entity_ids.', array(
  892. '%operation' => $operation->label(),
  893. '@type' => $operation->entityType,
  894. '!entity_ids' => implode(',', $entity_ids),
  895. )),
  896. 'uid' => $user->uid,
  897. 'arguments' => array($row_group, $operation, $options),
  898. );
  899. $queue->createItem($job);
  900. }
  901. }
  902. /**
  903. * Batch API callback: processes the active queue.
  904. *
  905. * @param $queue_name
  906. * The name of the queue to process.
  907. * @param $operation
  908. * The object representing the current operation.
  909. * @param $total_rows
  910. * The total number of processable items (across all queue items), used
  911. * to report progress.
  912. *
  913. * @see views_bulk_operations_queue_item_process()
  914. */
  915. function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
  916. static $queue;
  917. // It is still possible to hit the time limit.
  918. drupal_set_time_limit(0);
  919. // Prepare the sandbox.
  920. if (!isset($context['sandbox']['progress'])) {
  921. $context['sandbox']['progress'] = 0;
  922. $context['sandbox']['max'] = $total_rows;
  923. $context['results']['log'] = array();
  924. }
  925. // Instantiate the queue.
  926. if (!isset($queue)) {
  927. $queue = DrupalQueue::get($queue_name, TRUE);
  928. }
  929. // Process the queue as long as it has items for us.
  930. $queue_item = $queue->claimItem(3600);
  931. if ($queue_item) {
  932. // Process the queue item, and update the progress count.
  933. views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
  934. $queue->deleteItem($queue_item);
  935. // Provide an estimation of the completion level we've reached.
  936. $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
  937. $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  938. $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
  939. }
  940. if (!$queue_item || $context['finished'] === 1) {
  941. // All done. Provide a status message to the user.
  942. $context['results']['log'][] = t('Performed %operation on @items.', array(
  943. '%operation' => $operation->label(),
  944. '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
  945. ));
  946. }
  947. }
  948. /**
  949. * Processes the provided queue item.
  950. *
  951. * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
  952. * to process the site queue, as well as by
  953. * views_bulk_operations_active_queue_process() to process the active queue.
  954. *
  955. * @param $queue_item_arguments
  956. * The arguments of the queue item to process.
  957. * @param $log
  958. * An injected array of log messages, to be modified by reference.
  959. * If NULL, the function defaults to using watchdog.
  960. */
  961. function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
  962. list($row_group, $operation, $options) = $queue_item_data['arguments'];
  963. $account = user_load($queue_item_data['uid']);
  964. $entity_type = $operation->entityType;
  965. $entity_ids = array();
  966. foreach ($row_group as $row_index => $row) {
  967. $entity_ids[] = $row['entity_id'];
  968. }
  969. $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  970. foreach ($row_group as $row_index => $row) {
  971. $entity_id = $row['entity_id'];
  972. // A matching entity couldn't be loaded. Skip this item.
  973. if (!isset($entities[$entity_id])) {
  974. continue;
  975. }
  976. if ($options['revision']) {
  977. // Don't reload revisions for now, they are not statically cached and
  978. // usually don't run into the edge case described below.
  979. $entity = $entities[$entity_id];
  980. }
  981. else {
  982. // A previous action might have resulted in the entity being resaved
  983. // (e.g. node synchronization from a prior node in this batch), so try
  984. // to reload it. If no change occurred, the entity will be retrieved
  985. // from the static cache, resulting in no performance penalty.
  986. $entity = entity_load_single($entity_type, $entity_id);
  987. if (empty($entity)) {
  988. // The entity is no longer valid.
  989. continue;
  990. }
  991. }
  992. // If the current entity can't be accessed, skip it and log a notice.
  993. $skip_permission_check = $operation->getAdminOption('skip_permission_check');
  994. if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
  995. $message = 'Skipped %operation on @type %title due to insufficient permissions.';
  996. $arguments = array(
  997. '%operation' => $operation->label(),
  998. '@type' => $entity_type,
  999. '%title' => entity_label($entity_type, $entity),
  1000. );
  1001. if ($log) {
  1002. $log[] = t($message, $arguments);
  1003. }
  1004. else {
  1005. watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
  1006. }
  1007. continue;
  1008. }
  1009. $operation_context = array(
  1010. 'progress' => $row['position'],
  1011. 'view_info' => $options['view_info'],
  1012. );
  1013. if ($operation->needsRows()) {
  1014. $operation_context['rows'] = array($row_index => $row['views_row']);
  1015. }
  1016. $operation->execute($entity, $operation_context);
  1017. unset($row_group[$row_index]);
  1018. }
  1019. }
  1020. /**
  1021. * Adjusts the selection for the direct execution method.
  1022. *
  1023. * Just like the direct method itself, this is legacy code, used only for
  1024. * aggregate actions.
  1025. */
  1026. function views_bulk_operations_direct_adjust(&$selection, $vbo) {
  1027. // Adjust selection to select all rows across pages.
  1028. $view = views_get_view($vbo->view->name);
  1029. $view->set_exposed_input($vbo->view->get_exposed_input());
  1030. $view->set_arguments($vbo->view->args);
  1031. $view->set_display($vbo->view->current_display);
  1032. $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
  1033. $view->build();
  1034. // Unset every field except the VBO one (which holds the entity id).
  1035. // That way the performance hit becomes much smaller, because there is no
  1036. // chance of views_handler_field_field::post_execute() firing entity_load().
  1037. foreach ($view->field as $field_name => $field) {
  1038. if ($field_name != $vbo->options['id']) {
  1039. unset($view->field[$field_name]);
  1040. }
  1041. else {
  1042. // Get hold of the new VBO field.
  1043. $new_vbo = $view->field[$field_name];
  1044. }
  1045. }
  1046. $view->execute($vbo->view->current_display);
  1047. // Call views_handler_field_entity::pre_render() to get the entities.
  1048. $new_vbo->pre_render($view->result);
  1049. $results = array();
  1050. foreach ($view->result as $row_index => $result) {
  1051. // Set the row index.
  1052. $view->row_index = $row_index;
  1053. $results[$row_index] = $new_vbo->get_value($result, $new_vbo->real_field);
  1054. }
  1055. $selection = $results;
  1056. }
  1057. /**
  1058. * Processes the passed rows directly (without batching and queueing).
  1059. */
  1060. function views_bulk_operations_direct_process($operation, $rows, $options) {
  1061. global $user;
  1062. drupal_set_time_limit(0);
  1063. // Prepare an array of status information. Imitates the Batch API naming
  1064. // for consistency. Passed to views_bulk_operations_execute_finished().
  1065. $context = array();
  1066. $context['results']['progress'] = 0;
  1067. $context['results']['log'] = array();
  1068. if ($operation->aggregate()) {
  1069. // Load all entities.
  1070. $entity_type = $operation->entityType;
  1071. $entity_ids = array();
  1072. foreach ($rows as $row_index => $row) {
  1073. $entity_ids[] = $row['entity_id'];
  1074. }
  1075. $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
  1076. $skip_permission_check = $operation->getAdminOption('skip_permission_check');
  1077. // Filter out entities that can't be accessed.
  1078. foreach ($entities as $id => $entity) {
  1079. if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $user)) {
  1080. $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
  1081. '%operation' => $operation->label(),
  1082. '@type' => $entity_type,
  1083. '%title' => entity_label($entity_type, $entity),
  1084. ));
  1085. unset($entities[$id]);
  1086. }
  1087. }
  1088. // If there are any entities left, execute the operation on them.
  1089. if ($entities) {
  1090. $operation_context = array(
  1091. 'view_info' => $options['view_info'],
  1092. );
  1093. // Pass the selected rows to the operation if needed.
  1094. if ($operation->needsRows()) {
  1095. $operation_context['rows'] = array();
  1096. foreach ($rows as $row_index => $row) {
  1097. $operation_context['rows'][$row_index] = $row['views_row'];
  1098. }
  1099. }
  1100. $operation->execute($entities, $operation_context);
  1101. }
  1102. }
  1103. else {
  1104. // Imitate a queue and process the entities one by one.
  1105. $queue_item_data = array(
  1106. 'uid' => $user->uid,
  1107. 'arguments' => array($rows, $operation, $options),
  1108. );
  1109. views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
  1110. }
  1111. $context['results']['progress'] += count($rows);
  1112. $context['results']['log'][] = t('Performed %operation on @items.', array(
  1113. '%operation' => $operation->label(),
  1114. '@items' => format_plural(count($rows), '1 item', '@count items'),
  1115. ));
  1116. views_bulk_operations_execute_finished(TRUE, $context['results'], array());
  1117. }
  1118. /**
  1119. * Helper function that runs after the execution process is complete.
  1120. */
  1121. function views_bulk_operations_execute_finished($success, $results, $operations) {
  1122. if ($success) {
  1123. if (count($results['log']) > 1) {
  1124. $message = theme('item_list', array('items' => $results['log']));
  1125. }
  1126. else {
  1127. $message = reset($results['log']);
  1128. }
  1129. }
  1130. else {
  1131. // An error occurred.
  1132. // $operations contains the operations that remained unprocessed.
  1133. $error_operation = reset($operations);
  1134. $message = t('An error occurred while processing @operation with arguments: @arguments',
  1135. array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
  1136. }
  1137. _views_bulk_operations_log($message);
  1138. }
  1139. /**
  1140. * Helper function to verify access permission to operate on an entity.
  1141. */
  1142. function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
  1143. if (!entity_type_supports($entity_type, 'access')) {
  1144. return TRUE;
  1145. }
  1146. $access_ops = array(
  1147. VBO_ACCESS_OP_VIEW => 'view',
  1148. VBO_ACCESS_OP_UPDATE => 'update',
  1149. VBO_ACCESS_OP_CREATE => 'create',
  1150. VBO_ACCESS_OP_DELETE => 'delete',
  1151. );
  1152. foreach ($access_ops as $bit => $op) {
  1153. if ($operation->getAccessMask() & $bit) {
  1154. if (!entity_access($op, $entity_type, $entity, $account)) {
  1155. return FALSE;
  1156. }
  1157. }
  1158. }
  1159. return TRUE;
  1160. }
  1161. /**
  1162. * Loads multiple entities by their entity or revision ids, and returns them,
  1163. * keyed by the id used for loading.
  1164. */
  1165. function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
  1166. if (!$revision) {
  1167. $entities = entity_load($entity_type, $ids);
  1168. }
  1169. else {
  1170. // D7 can't load multiple entities by revision_id. Lovely.
  1171. $info = entity_get_info($entity_type);
  1172. $entities = array();
  1173. foreach ($ids as $revision_id) {
  1174. $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
  1175. $entities[$revision_id] = reset($loaded_entities);
  1176. }
  1177. }
  1178. return $entities;
  1179. }
  1180. /**
  1181. * Helper function to report an error.
  1182. */
  1183. function _views_bulk_operations_report_error($msg, $arg) {
  1184. watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
  1185. if (function_exists('drush_set_error')) {
  1186. drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
  1187. }
  1188. }
  1189. /**
  1190. * Display a message to the user through the relevant function.
  1191. */
  1192. function _views_bulk_operations_log($msg) {
  1193. // Is VBO being run through drush?
  1194. if (function_exists('drush_log')) {
  1195. drush_log(strip_tags($msg), 'ok');
  1196. }
  1197. else {
  1198. drupal_set_message($msg);
  1199. }
  1200. }