views_bulk_operations.module 45 KB

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