Projet

Général

Profil

Paste
Télécharger (44,4 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / views_bulk_operations / views_bulk_operations.module @ 9df8b457

1
<?php
2

    
3
/**
4
 * @file
5
 * Allows operations to be performed on items selected in a view.
6
 */
7

    
8
// Access operations.
9
define('VBO_ACCESS_OP_VIEW',      0x01);
10
define('VBO_ACCESS_OP_UPDATE',    0x02);
11
define('VBO_ACCESS_OP_CREATE',    0x04);
12
define('VBO_ACCESS_OP_DELETE',    0x08);
13

    
14
/**
15
 * Implements hook_action_info().
16
 * Registers custom VBO actions as Drupal actions.
17
 */
18
function views_bulk_operations_action_info() {
19
  $actions = array();
20
  $files = views_bulk_operations_load_action_includes();
21
  foreach ($files as $filename) {
22
    $action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_info';
23
    $action_info = call_user_func($action_info_fn);
24
    if (is_array($action_info)) {
25
      $actions += $action_info;
26
    }
27
  }
28

    
29
  return $actions;
30
}
31

    
32
/**
33
 * Loads the VBO actions placed in their own include files (under actions/).
34
 *
35
 * @return
36
 *   An array of containing filenames of the included actions.
37
 */
38
function views_bulk_operations_load_action_includes() {
39
  static $loaded = FALSE;
40

    
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
    'delete.action',
49
    'modify.action',
50
    'script.action',
51
    'user_roles.action',
52
    'user_cancel.action',
53
  );
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

    
62
  return $files;
63
}
64

    
65
/**
66
 * Implements hook_cron().
67
 *
68
 * Deletes queue items belonging to VBO active queues (used by VBO's batches)
69
 * that are older than a day (since they can only be a result of VBO crashing
70
 * or the execution being interrupted in some other way). This is the interval
71
 * used to cleanup batches in system_cron(), so it can't be increased.
72
 *
73
 * Note: This code is specific to SystemQueue. Other queue implementations will
74
 * need to do their own garbage collection.
75
 */
76
function views_bulk_operations_cron() {
77
  db_delete('queue')
78
    ->condition('name', db_like('views_bulk_operations_active_queue_'), 'LIKE')
79
    ->condition('created', REQUEST_TIME - 86400, '<')
80
    ->execute();
81
}
82

    
83
/**
84
 * Implements of hook_cron_queue_info().
85
 */
86
function views_bulk_operations_cron_queue_info() {
87
  return array(
88
    'views_bulk_operations' => array(
89
      'worker callback' => 'views_bulk_operations_queue_item_process',
90
      'time' => 30,
91
    ),
92
  );
93
}
94

    
95
/**
96
 * Implements hook_views_api().
97
 */
98
function views_bulk_operations_views_api() {
99
  return array(
100
    'api' => 3,
101
    'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
102
  );
103
}
104

    
105
/**
106
 * Implements hook_theme().
107
 */
108
function views_bulk_operations_theme() {
109
  $themes = array(
110
    'views_bulk_operations_select_all' => array(
111
      'variables' => array('view' => NULL, 'enable_select_all_pages' => TRUE),
112
    ),
113
    'views_bulk_operations_confirmation' => array(
114
      'variables' => array('rows' => NULL, 'vbo' => NULL, 'operation' => NULL, 'select_all_pages' => FALSE),
115
    ),
116
  );
117
  $files = views_bulk_operations_load_action_includes();
118
  foreach ($files as $filename) {
119
    $action_theme_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_theme';
120
    if (function_exists($action_theme_fn)) {
121
      $themes += call_user_func($action_theme_fn);
122
    }
123
  }
124

    
125
  return $themes;
126
}
127

    
128
/**
129
 * Implements hook_ctools_plugin_type().
130
 */
131
function views_bulk_operations_ctools_plugin_type() {
132
  return array(
133
    'operation_types' => array(
134
      'classes' => array(
135
        'handler',
136
      ),
137
    ),
138
  );
139
}
140

    
141
/**
142
 * Implements hook_ctools_plugin_directory().
143
 */
144
function views_bulk_operations_ctools_plugin_directory($module, $plugin) {
145
  if ($module == 'views_bulk_operations') {
146
    return 'plugins/' . $plugin;
147
  }
148
}
149

    
150
/**
151
 * Fetch metadata for a specific operation type plugin.
152
 *
153
 * @param $operation_type
154
 *   Name of the plugin.
155
 *
156
 * @return
157
 *   An array with information about the requested operation type plugin.
158
 */
159
function views_bulk_operations_get_operation_type($operation_type) {
160
  ctools_include('plugins');
161
  return ctools_get_plugins('views_bulk_operations', 'operation_types', $operation_type);
162
}
163

    
164
/**
165
 * Fetch metadata for all operation type plugins.
166
 *
167
 * @return
168
 *   An array of arrays with information about all available operation types.
169
 */
170
function views_bulk_operations_get_operation_types() {
171
  ctools_include('plugins');
172
  return ctools_get_plugins('views_bulk_operations', 'operation_types');
173
}
174

    
175
/**
176
  * Gets the info array of an operation from the provider plugin.
177
  *
178
  * @param $operation_id
179
  *   The id of the operation for which the info shall be returned, or NULL
180
  *   to return an array with info about all operations.
181
  */
182
function views_bulk_operations_get_operation_info($operation_id = NULL) {
183
  $operations = &drupal_static(__FUNCTION__);
184

    
185
  if (!isset($operations)) {
186
    $operations = array();
187
    $plugins = views_bulk_operations_get_operation_types();
188
    foreach ($plugins as $plugin) {
189
      $operations += $plugin['list callback']();
190
    }
191

    
192
    uasort($operations, create_function('$a, $b', 'return strcasecmp($a["label"], $b["label"]);'));
193
  }
194

    
195
  if (!empty($operation_id)) {
196
    return $operations[$operation_id];
197
  }
198
  else {
199
    return $operations;
200
  }
201
}
202

    
203
/**
204
 * Returns an operation instance.
205
 *
206
 * @param $operation_id
207
 *   The id of the operation to instantiate.
208
 *   For example: action::node_publish_action.
209
 * @param $entity_type
210
 *   The entity type on which the operation operates.
211
 * @param $options
212
 *   Options for this operation (label, operation settings, etc.)
213
 */
214
function views_bulk_operations_get_operation($operation_id, $entity_type, $options) {
215
  $operations = &drupal_static(__FUNCTION__);
216

    
217
  if (!isset($operations[$operation_id])) {
218
    // Intentionally not using views_bulk_operations_get_operation_info() here
219
    // since it's an expensive function that loads all the operations on the
220
    // system, despite the fact that we might only need a few.
221
    $id_fragments = explode('::', $operation_id);
222
    $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
223
    $operation_info = $plugin['list callback']($operation_id);
224

    
225
    if ($operation_info) {
226
      $operations[$operation_id] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options);
227
    }
228
    else {
229
      $operations[$operation_id] = FALSE;
230
    }
231
  }
232

    
233
  return $operations[$operation_id];
234
}
235

    
236
/**
237
 * Get all operations that match the current entity type.
238
 *
239
 * @param $entity_type
240
 *   Entity type.
241
 * @param $options
242
 *   An array of options for all operations, in the form of
243
 *   $operation_id => $operation_options.
244
 */
245
function views_bulk_operations_get_applicable_operations($entity_type, $options) {
246
  $operations = array();
247
  foreach (views_bulk_operations_get_operation_info() as $operation_id => $operation_info) {
248
    if ($operation_info['type'] == $entity_type || $operation_info['type'] == 'entity' || $operation_info['type'] == 'system') {
249
      $options[$operation_id] = !empty($options[$operation_id]) ? $options[$operation_id] : array();
250
      $operations[$operation_id] = views_bulk_operations_get_operation($operation_id, $entity_type, $options[$operation_id]);
251
    }
252
  }
253

    
254
  return $operations;
255
}
256

    
257
/**
258
 * Gets the VBO field if it exists on the passed-in view.
259
 *
260
 * @return
261
 *  The field object if found. Otherwise, FALSE.
262
 */
263
function _views_bulk_operations_get_field($view) {
264
  foreach ($view->field as $field_name => $field) {
265
    if ($field instanceof views_bulk_operations_handler_field_operations) {
266
      // Add in the view object for convenience.
267
      $field->view = $view;
268
      return $field;
269
    }
270
  }
271
  return FALSE;
272
}
273

    
274
/**
275
 * Implements hook_views_form_substitutions().
276
 */
277
function views_bulk_operations_views_form_substitutions() {
278
  // Views check_plains the column label, so VBO needs to do the same
279
  // in order for the replace operation to succeed.
280
  $select_all_placeholder = check_plain('<!--views-bulk-operations-select-all-->');
281
  $select_all = array(
282
    '#type' => 'checkbox',
283
    '#default_value' => FALSE,
284
    '#attributes' => array('class' => array('vbo-table-select-all')),
285
  );
286

    
287
  return array(
288
    $select_all_placeholder => drupal_render($select_all),
289
  );
290
}
291

    
292
/**
293
 * Implements hook_form_alter().
294
 */
295
function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
296
  if (strpos($form_id, 'views_form_') === 0) {
297
    $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
298
  }
299
  // Not a VBO-enabled views form.
300
  if (empty($vbo)) {
301
    return;
302
  }
303

    
304
  // Add basic VBO functionality.
305
  if ($form_state['step'] == 'views_form_views_form') {
306
    // The submit button added by Views Form API might be used by a non-VBO Views
307
    // Form handler. If there's no such handler on the view, hide the button.
308
    $has_other_views_form_handlers = FALSE;
309
    foreach ($vbo->view->field as $field) {
310
      if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
311
        if (!($field instanceof views_bulk_operations_handler_field_operations)) {
312
          $has_other_views_form_handlers = TRUE;
313
        }
314
      }
315
    }
316
    if (!$has_other_views_form_handlers) {
317
      $form['actions']['#access'] = FALSE;
318
    }
319
    // The VBO field is excluded from display, stop here.
320
    if (!empty($vbo->options['exclude'])) {
321
      return;
322
    }
323

    
324
    $form = views_bulk_operations_form($form, $form_state, $vbo);
325
  }
326

    
327
  // Cache the built form to prevent it from being rebuilt prior to validation
328
  // and submission, which could lead to data being processed incorrectly,
329
  // because the views rows (and thus, the form elements as well) have changed
330
  // in the meantime. Matching views issue: http://drupal.org/node/1473276.
331
  $form_state['cache'] = TRUE;
332

    
333
  if (empty($vbo->view->override_url)) {
334
    // If the VBO view is embedded using views_embed_view(), or in a block,
335
    // $view->get_url() doesn't point to the current page, which means that
336
    // the form doesn't get processed.
337
    if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
338
      $vbo->view->override_url = $_GET['q'];
339
      // We are changing the override_url too late, the form action was already
340
      // set by Views to the previous URL, so it needs to be overriden as well.
341
      $query = drupal_get_query_parameters($_GET, array('q'));
342
      $form['#action'] = url($_GET['q'], array('query' => $query));
343
    }
344
  }
345

    
346
  // Give other modules a chance to alter the form.
347
  drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
348
}
349

    
350
/**
351
 * Implements hook_views_post_build().
352
 *
353
 * Hides the VBO field if no operations are available.
354
 * This causes the entire VBO form to be hidden.
355
 *
356
 * @see views_bulk_operations_form_alter().
357
 */
358
function views_bulk_operations_views_post_build(&$view) {
359
  $vbo = _views_bulk_operations_get_field($view);
360
  if ($vbo && count($vbo->get_selected_operations()) < 1) {
361
    $vbo->options['exclude'] = TRUE;
362
  }
363
}
364

    
365
/**
366
 * Returns the 'select all' div that gets inserted below the table header row
367
 * (for table style plugins with grouping disabled), or above the view results
368
 * (for non-table style plugins), providing a choice between selecting items
369
 * on the current page, and on all pages.
370
 *
371
 * The actual insertion is done by JS, matching the degradation behavior
372
 * of Drupal core (no JS - no select all).
373
 */
374
function theme_views_bulk_operations_select_all($variables) {
375
  $view = $variables['view'];
376
  $enable_select_all_pages = $variables['enable_select_all_pages'];
377
  $form = array();
378

    
379
  if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
380
    if (!$enable_select_all_pages) {
381
      return '';
382
    }
383

    
384
    $wrapper_class = 'vbo-table-select-all-markup';
385
    $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
386
    $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
387
    $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
388
    $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));
389

    
390
    $form['select_all_pages'] = array(
391
      '#type' => 'button',
392
      '#attributes' => array('class' => array('vbo-table-select-all-pages')),
393
      '#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
394
      '#prefix' => '<span class="vbo-table-this-page">' . $this_page . ' &nbsp;',
395
      '#suffix' => '</span>',
396
    );
397
    $form['select_this_page'] = array(
398
      '#type' => 'button',
399
      '#attributes' => array('class' => array('vbo-table-select-this-page')),
400
      '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
401
      '#prefix' => '<span class="vbo-table-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
402
      '#suffix' => '</span>',
403
    );
404
  }
405
  else {
406
    $wrapper_class = 'vbo-select-all-markup';
407

    
408
    $form['select_all'] = array(
409
      '#type' => 'fieldset',
410
      '#attributes' => array('class' => array('vbo-fieldset-select-all')),
411
    );
412
    $form['select_all']['this_page'] = array(
413
      '#type' => 'checkbox',
414
      '#title' => t('Select all items on this page'),
415
      '#default_value' => '',
416
      '#attributes' => array('class' => array('vbo-select-this-page')),
417
    );
418

    
419
    if ($enable_select_all_pages) {
420
      $form['select_all']['or'] = array(
421
        '#type' => 'markup',
422
        '#markup' => '<em>' . t('OR') . '</em>',
423
      );
424
      $form['select_all']['all_pages'] = array(
425
        '#type' => 'checkbox',
426
        '#title' => t('Select all items on all pages'),
427
        '#default_value' => '',
428
        '#attributes' => array('class' => array('vbo-select-all-pages')),
429
      );
430
    }
431
  }
432

    
433
  $output = '<div class="' . $wrapper_class . '">';
434
  $output .= drupal_render($form);
435
  $output .= '</div>';
436

    
437
  return $output;
438
}
439

    
440
/**
441
 * Extend the views_form multistep form with elements for executing an operation.
442
 */
443
function views_bulk_operations_form($form, &$form_state, $vbo) {
444
  $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
445
  $form['#attached']['js'][] = array(
446
    'data' => array('vbo' => array(
447
      'row_clickable' => $vbo->get_vbo_option('row_clickable'),
448
    )),
449
    'type' => 'setting',
450
  );
451

    
452
  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
453
  // Wrap the form in a div with specific classes for JS targeting and theming.
454
  $class = 'vbo-views-form';
455
  if (empty($vbo->view->result)) {
456
    $class .= ' vbo-views-form-empty';
457
  }
458
  $form['#prefix'] = '<div class="' . $class . '">';
459
  $form['#suffix'] = '</div>';
460

    
461
  // Force browser to reload the page if Back is hit.
462
  if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
463
    drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+
464
  }
465
  else {
466
    drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers
467
  }
468

    
469
  // Set by JS to indicate that all rows on all pages are selected.
470
  $form['select_all'] = array(
471
    '#type' => 'hidden',
472
    '#attributes' => array('class' => 'select-all-rows'),
473
    '#default_value' => FALSE,
474
  );
475
  $form['select'] = array(
476
    '#type' => 'fieldset',
477
    '#title' => t('Operations'),
478
    '#collapsible' => FALSE,
479
    '#attributes' => array('class' => array('container-inline')),
480
  );
481
  if ($vbo->get_vbo_option('display_type') == 0) {
482
    $options = array(0 => t('- Choose an operation -'));
483
    foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
484
      $options[$operation_id] = $operation->label();
485
    }
486

    
487
    // Create dropdown and submit button.
488
    $form['select']['operation'] = array(
489
      '#type' => 'select',
490
      '#options' => $options,
491
    );
492
    $form['select']['submit'] = array(
493
      '#type' => 'submit',
494
      '#value' => t('Execute'),
495
      '#validate' => array('views_bulk_operations_form_validate'),
496
      '#submit' => array('views_bulk_operations_form_submit'),
497
    );
498
  }
499
  else {
500
    // Create buttons for operations.
501
    foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
502
      $form['select'][$operation_id] = array(
503
        '#type' => 'submit',
504
        '#value' => $operation->label(),
505
        '#validate' => array('views_bulk_operations_form_validate'),
506
        '#submit' => array('views_bulk_operations_form_submit'),
507
        '#operation_id' => $operation_id,
508
      );
509
    }
510
  }
511

    
512
  // Adds the "select all" functionality if the view has results.
513
  // If the view is using a table style plugin, the markup gets moved to
514
  // a table row below the header.
515
  // If we are using radio buttons, we don't use select all at all.
516
  if (!empty($vbo->view->result) && !$vbo->get_vbo_option('force_single')) {
517
    $enable_select_all_pages = FALSE;
518
    // If the view is paginated, and "select all items on all pages" is
519
    // enabled, tell that to the theme function.
520
    if (count($vbo->view->result) != $vbo->view->total_rows && $vbo->get_vbo_option('enable_select_all_pages')) {
521
      $enable_select_all_pages = TRUE;
522
    }
523
    $form['select_all_markup'] = array(
524
      '#type' => 'markup',
525
      '#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view, 'enable_select_all_pages' => $enable_select_all_pages)),
526
    );
527
  }
528

    
529
  return $form;
530
}
531

    
532
/**
533
 * Validation callback for the first step of the VBO form.
534
 */
535
function views_bulk_operations_form_validate($form, &$form_state) {
536
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
537

    
538
  if (!empty($form_state['triggering_element']['#operation_id'])) {
539
    $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
540
  }
541
  if (!$form_state['values']['operation']) {
542
    form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
543
  }
544

    
545
  $field_name = $vbo->options['id'];
546
  $selection = _views_bulk_operations_get_selection($vbo, $form_state);
547
  if (!$selection) {
548
    form_set_error($field_name, t('Please select at least one item.'));
549
  }
550
}
551

    
552
/**
553
 * Multistep form callback for the "configure" step.
554
 */
555
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
556
  $vbo = _views_bulk_operations_get_field($view);
557
  $operation = $form_state['operation'];
558
  drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
559

    
560
  $context = array(
561
    'entity_type' => $vbo->get_entity_type(),
562
    // Pass the View along.
563
    // Has no performance penalty since objects are passed by reference,
564
    // but needing the full views object in a core action is in most cases
565
    // a sign of a wrong implementation. Do it only if you have to.
566
    'view' => $view,
567
  );
568
  $form += $operation->form($form, $form_state, $context);
569

    
570
  $query = drupal_get_query_parameters($_GET, array('q'));
571
  $form['actions'] = array(
572
    '#type' => 'container',
573
    '#attributes' => array('class' => array('form-actions')),
574
    '#weight' => 999,
575
  );
576
  $form['actions']['submit'] = array(
577
    '#type' => 'submit',
578
    '#value' => t('Next'),
579
    '#validate' => array('views_bulk_operations_config_form_validate'),
580
    '#submit' => array('views_bulk_operations_form_submit'),
581
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
582
  );
583

    
584
  return $form;
585
}
586

    
587
/**
588
 * Validation callback for the "configure" step.
589
 * Gives the operation a chance to validate its config form.
590
 */
591
function views_bulk_operations_config_form_validate($form, &$form_state) {
592
  $operation = &$form_state['operation'];
593
  $operation->formValidate($form, $form_state);
594
}
595

    
596
/**
597
 * Multistep form callback for the "confirm" step.
598
 */
599
function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) {
600
  $vbo = _views_bulk_operations_get_field($view);
601
  $operation = $form_state['operation'];
602
  $rows = $form_state['selection'];
603
  $query = drupal_get_query_parameters($_GET, array('q'));
604
  $title = t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation->label()));
605
  $form = confirm_form($form,
606
    $title,
607
    array('path' => $view->get_url(), 'query' => $query),
608
    theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo, 'operation' => $operation, 'select_all_pages' => $form_state['select_all_pages']))
609
  );
610
  // Add VBO's submit handler to the Confirm button added by config_form().
611
  $form['actions']['submit']['#submit'] = array('views_bulk_operations_form_submit');
612

    
613
  // We can't set the View title here as $view is just a copy of the original,
614
  // and our settings changes won't "stick" for the first page load of the
615
  // confirmation form. We also can't just call drupal_set_title() directly
616
  // because our title will be clobbered by the actual View title later. So
617
  // let's tuck the title away in the form for use later.
618
  // @see views_bulk_operations_preprocess_views_view()
619
  $form['#vbo_confirm_form_title'] = $title;
620

    
621
  return $form;
622
}
623

    
624
/**
625
 * Theme function to show the confirmation page before executing the operation.
626
 */
627
function theme_views_bulk_operations_confirmation($variables) {
628
  $select_all_pages = $variables['select_all_pages'];
629
  $vbo = $variables['vbo'];
630
  $entity_type = $vbo->get_entity_type();
631
  $rows = $variables['rows'];
632
  $items = array();
633
  // Load the entities from the current page, and show their titles.
634
  $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
635
  foreach ($entities as $entity) {
636
    $items[] = check_plain(entity_label($entity_type, $entity));
637
  }
638
  // All rows on all pages have been selected, so show a count of additional items.
639
  if ($select_all_pages) {
640
    $more_count = $vbo->view->total_rows - count($vbo->view->result);
641
    $items[] = t('...and <strong>!count</strong> more.', array('!count' => $more_count));
642
  }
643

    
644
  $count = format_plural(count($entities), 'item', '@count items');
645
  $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following <strong>!count</strong>:', array('!count' => $count))));
646
  return $output;
647
}
648

    
649
/**
650
 * Implements hook_preprocess_page().
651
 *
652
 * Hide action links on the configure and confirm pages.
653
 */
654
function views_bulk_operations_preprocess_page(&$variables) {
655
  if (isset($_POST['select_all'], $_POST['operation'])) {
656
    $variables['action_links'] = array();
657
  }
658
}
659

    
660
/**
661
 * Implements hook_preprocess_views_view().
662
 */
663
function views_bulk_operations_preprocess_views_view($variables) {
664
  // If we've stored a title for the confirmation form, retrieve it here and
665
  // retitle the View.
666
  // @see views_bulk_operations_confirm_form()
667
  if (array_key_exists('rows', $variables) && is_array($variables['rows']) && array_key_exists('#vbo_confirm_form_title', $variables['rows'])) {
668
    $variables['view']->set_title($variables['rows']['#vbo_confirm_form_title']);
669
  }
670
}
671

    
672
/**
673
 * Goes through the submitted values, and returns
674
 * an array of selected rows, in the form of
675
 * $row_index => $entity_id.
676
 */
677
function _views_bulk_operations_get_selection($vbo, $form_state) {
678
  $selection = array();
679
  $field_name = $vbo->options['id'];
680

    
681
  if (!empty($form_state['values'][$field_name])) {
682
    // If using "force single", the selection needs to be converted to an array.
683
    if (is_array($form_state['values'][$field_name])) {
684
      $selection = array_filter($form_state['values'][$field_name]);
685
    }
686
    else {
687
      $selection = array($form_state['values'][$field_name]);
688
    }
689
  }
690

    
691
  return $selection;
692
}
693

    
694
/**
695
 * Submit handler for all steps of the VBO multistep form.
696
 */
697
function views_bulk_operations_form_submit($form, &$form_state) {
698
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
699
  $entity_type = $vbo->get_entity_type();
700

    
701
  switch ($form_state['step']) {
702
    case 'views_form_views_form':
703
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
704
      $form_state['select_all_pages'] = $form_state['values']['select_all'];
705

    
706
      $options = $vbo->get_operation_options($form_state['values']['operation']);
707
      $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
708
      if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
709
        break; // Go directly to execution
710
      }
711
      $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
712
      $form_state['rebuild'] = TRUE;
713
      return;
714

    
715
    case 'views_bulk_operations_config_form':
716
      $form_state['step'] = 'views_bulk_operations_confirm_form';
717
      $operation = &$form_state['operation'];
718
      $operation->formSubmit($form, $form_state);
719

    
720
      if ($operation->getAdminOption('skip_confirmation')) {
721
        break; // Go directly to execution
722
      }
723
      $form_state['rebuild'] = TRUE;
724
      return;
725

    
726
    case 'views_bulk_operations_confirm_form':
727
      break;
728
  }
729

    
730
  // Execute the operation.
731
  views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);
732

    
733
  // Redirect.
734
  $query = drupal_get_query_parameters($_GET, array('q'));
735
  $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
736
}
737

    
738
/**
739
 * Entry point for executing the chosen operation upon selected rows.
740
 *
741
 * If the selected operation is an aggregate operation (requiring all selected
742
 * items to be passed at the same time), restricted to a single value, or has
743
 * the skip_batching option set, the operation is executed directly.
744
 * This means that there is no batching & queueing, the PHP execution
745
 * time limit is ignored (if allowed), all selected entities are loaded and
746
 * processed.
747
 *
748
 * Otherwise, the selected entity ids are divided into groups not larger than
749
 * $entity_load_capacity, and enqueued for processing.
750
 * If all items on all pages should be processed, a batch job runs that
751
 * collects and enqueues the items from all pages of the view, page by page.
752
 *
753
 * Based on the "Enqueue the operation instead of executing it directly"
754
 * VBO field setting, the newly filled queue is either processed at cron
755
 * time by the VBO worker function, or right away in a new batch job.
756
 *
757
 * @param $vbo
758
 *   The VBO field, containing a reference to the view in $vbo->view.
759
 * @param $operation
760
 *   The operation object.
761
 * @param $selection
762
 *   An array in the form of $row_index => $entity_id.
763
 * @param $select_all_pages
764
 *   Whether all items on all pages should be selected.
765
 */
766
function views_bulk_operations_execute($vbo, $operation, $selection, $select_all_pages = FALSE) {
767
  global $user;
768

    
769
  // Determine if the operation needs to be executed directly.
770
  $aggregate = $operation->aggregate();
771
  $skip_batching = $vbo->get_vbo_option('skip_batching');
772
  $force_single = $vbo->get_vbo_option('force_single');
773
  $execute_directly = ($aggregate || $skip_batching || $force_single);
774
  // Try to load all rows without a batch if needed.
775
  if ($execute_directly && $select_all_pages) {
776
    views_bulk_operations_direct_adjust($selection, $vbo);
777
  }
778

    
779
  // Options that affect execution.
780
  $options = array(
781
    'revision' => $vbo->revision,
782
    'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
783
    // The information needed to recreate the view, to avoid serializing the
784
    // whole object. Passed to the executed operation. Also used by
785
    // views_bulk_operations_adjust_selection().
786
    'view_info' => array(
787
      'name' => $vbo->view->name,
788
      'display' => $vbo->view->current_display,
789
      'arguments' => $vbo->view->args,
790
      'exposed_input' => $vbo->view->get_exposed_input(),
791
    ),
792
  );
793
  // Create an array of rows in the needed format.
794
  $rows = array();
795
  $current = 1;
796
  foreach ($selection as $row_index => $entity_id) {
797
    $rows[$row_index] = array(
798
      'entity_id' => $entity_id,
799
      'views_row' => array(),
800
      // Some operations rely on knowing the position of the current item
801
      // in the execution set (because of specific things that need to be done
802
      // at the beginning or the end of the set).
803
      'position' => array(
804
        'current' => $current++,
805
        'total' => count($selection),
806
      ),
807
    );
808
    // Some operations require full selected rows.
809
    if ($operation->needsRows()) {
810
      $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
811
    }
812
  }
813

    
814
  if ($execute_directly) {
815
    // Execute the operation directly and stop here.
816
    views_bulk_operations_direct_process($operation, $rows, $options);
817
    return;
818
  }
819

    
820
  // Determine the correct queue to use.
821
  if ($operation->getAdminOption('postpone_processing')) {
822
    // Use the site queue processed on cron.
823
    $queue_name = 'views_bulk_operations';
824
  }
825
  else {
826
    // Use the active queue processed immediately by Batch API.
827
    $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
828
  }
829

    
830
  $batch = array(
831
    'operations' => array(),
832
    'finished' => 'views_bulk_operations_execute_finished',
833
    'progress_message' => '',
834
    'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
835
  );
836

    
837
  // All items on all pages should be selected, add a batch job to gather
838
  // and enqueue them.
839
  if ($select_all_pages && $vbo->view->query->pager->has_more_records()) {
840
    $total_rows = $vbo->view->total_rows;
841

    
842
    $batch['operations'][] = array(
843
      'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options),
844
    );
845
  }
846
  else {
847
    $total_rows = count($rows);
848

    
849
    // We have all the items that we need, enqueue them right away.
850
    views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
851

    
852
    // Provide a status message to the user, since this is the last step if
853
    // processing is postponed.
854
    if ($operation->getAdminOption('postpone_processing')) {
855
      drupal_set_message(t('Enqueued the selected operation (%operation).', array(
856
        '%operation' => $operation->label(),
857
      )));
858
    }
859
  }
860

    
861
  // Processing is not postponed, add a batch job to process the queue.
862
  if (!$operation->getAdminOption('postpone_processing')) {
863
    $batch['operations'][] = array(
864
      'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
865
    );
866
  }
867

    
868
  // If there are batch jobs to be processed, create the batch set.
869
  if (count($batch['operations'])) {
870
    batch_set($batch);
871
  }
872
}
873

    
874
/**
875
 * Batch API callback: loads the view page by page and enqueues all items.
876
 *
877
 * @param $queue_name
878
 *   The name of the queue to which the items should be added.
879
 * @param $operation
880
 *   The operation object.
881
 * @param $options
882
 *   An array of options that affect execution (revision, entity_load_capacity,
883
 *   view_info). Passed along with each new queue item.
884
 */
885
function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) {
886
  if (!isset($context['sandbox']['progress'])) {
887
    $context['sandbox']['progress'] = 0;
888
    $context['sandbox']['max'] = 0;
889
  }
890

    
891
  $view_info = $options['view_info'];
892
  $view = views_get_view($view_info['name']);
893
  $view->set_exposed_input($view_info['exposed_input']);
894
  $view->set_arguments($view_info['arguments']);
895
  $view->set_display($view_info['display']);
896
  $view->set_offset($context['sandbox']['progress']);
897
  $view->build();
898
  $view->execute($view_info['display']);
899
  // Note the total number of rows.
900
  if (empty($context['sandbox']['max'])) {
901
    $context['sandbox']['max'] = $view->total_rows;
902
  }
903

    
904
  $vbo = _views_bulk_operations_get_field($view);
905
  $rows = array();
906
  foreach ($view->result as $row_index => $result) {
907
    $rows[$row_index] = array(
908
      'entity_id' => $vbo->get_value($result),
909
      'views_row' => array(),
910
      'position' => array(
911
        'current' => ++$context['sandbox']['progress'],
912
        'total' => $context['sandbox']['max'],
913
      ),
914
    );
915
    // Some operations require full selected rows.
916
    if ($operation->needsRows()) {
917
      $rows[$row_index]['views_row'] = $result;
918
    }
919
  }
920

    
921
  // Enqueue the gathered rows.
922
  views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
923

    
924
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
925
    // Provide an estimation of the completion level we've reached.
926
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
927
    $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
928
  }
929
  else {
930
    // Provide a status message to the user if this is the last batch job.
931
    if ($operation->getAdminOption('postpone_processing')) {
932
      $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
933
        '%operation' => $operation->label(),
934
      ));
935
    }
936
  }
937
}
938

    
939
/**
940
 * Divides the passed rows into groups and enqueues each group for processing
941
 *
942
 * @param $queue_name
943
 *   The name of the queue.
944
 * @param $rows
945
 *   The rows to be enqueued.
946
 * @param $operation
947
 *   The object representing the current operation.
948
 *   Passed along with each new queue item.
949
 * @param $options
950
 *   An array of options that affect execution (revision, entity_load_capacity).
951
 *   Passed along with each new queue item.
952
 */
953
function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
954
  global $user;
955

    
956
  $queue = DrupalQueue::get($queue_name, TRUE);
957
  $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
958

    
959
  foreach ($row_groups as $row_group) {
960
    $entity_ids = array();
961
    foreach ($row_group as $row) {
962
      $entity_ids[] = $row['entity_id'];
963
    }
964

    
965
    $job = array(
966
      'title' => t('Perform %operation on @type !entity_ids.', array(
967
        '%operation' => $operation->label(),
968
        '@type' => $operation->entityType,
969
        '!entity_ids' => implode(',', $entity_ids),
970
      )),
971
      'uid' => $user->uid,
972
      'arguments' => array($row_group, $operation, $options),
973
    );
974
    $queue->createItem($job);
975
  }
976
}
977

    
978
/**
979
 * Batch API callback: processes the active queue.
980
 *
981
 * @param $queue_name
982
 *   The name of the queue to process.
983
 * @param $operation
984
 *   The object representing the current operation.
985
 * @param $total_rows
986
 *   The total number of processable items (across all queue items), used
987
 *   to report progress.
988
 *
989
 * @see views_bulk_operations_queue_item_process()
990
 */
991
function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
992
  static $queue;
993

    
994
  // It is still possible to hit the time limit.
995
  drupal_set_time_limit(0);
996

    
997
  // Prepare the sandbox.
998
  if (!isset($context['sandbox']['progress'])) {
999
    $context['sandbox']['progress'] = 0;
1000
    $context['sandbox']['max'] = $total_rows;
1001
    $context['results']['log'] = array();
1002
  }
1003
  // Instantiate the queue.
1004
  if (!isset($queue)) {
1005
    $queue = DrupalQueue::get($queue_name, TRUE);
1006
  }
1007

    
1008
  // Process the queue as long as it has items for us.
1009
  $queue_item = $queue->claimItem(3600);
1010
  if ($queue_item) {
1011
    // Process the queue item, and update the progress count.
1012
    views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
1013
    $queue->deleteItem($queue_item);
1014

    
1015
    // Provide an estimation of the completion level we've reached.
1016
    $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
1017
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
1018
    $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
1019
  }
1020

    
1021
  if (!$queue_item || $context['finished'] === 1) {
1022
    // All done. Provide a status message to the user.
1023
    $context['results']['log'][] = t('Performed %operation on @items.', array(
1024
      '%operation' => $operation->label(),
1025
      '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
1026
    ));
1027
  }
1028
}
1029

    
1030
/**
1031
 * Processes the provided queue item.
1032
 *
1033
 * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
1034
 * to process the site queue, as well as by
1035
 * views_bulk_operations_active_queue_process() to process the active queue.
1036
 *
1037
 * @param $queue_item_arguments
1038
 *   The arguments of the queue item to process.
1039
 * @param $log
1040
 *   An injected array of log messages, to be modified by reference.
1041
 *   If NULL, the function defaults to using watchdog.
1042
 */
1043
function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
1044
  list($row_group, $operation, $options) = $queue_item_data['arguments'];
1045
  $account = user_load($queue_item_data['uid']);
1046
  $entity_type = $operation->entityType;
1047
  $entity_ids = array();
1048
  foreach ($row_group as $row_index => $row) {
1049
    $entity_ids[] = $row['entity_id'];
1050
  }
1051

    
1052
  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1053
  foreach ($row_group as $row_index => $row) {
1054
    $entity_id = $row['entity_id'];
1055
    // A matching entity couldn't be loaded. Skip this item.
1056
    if (!isset($entities[$entity_id])) {
1057
      continue;
1058
    }
1059

    
1060
    if ($options['revision']) {
1061
      // Don't reload revisions for now, they are not statically cached and
1062
      // usually don't run into the edge case described below.
1063
      $entity = $entities[$entity_id];
1064
    }
1065
    else {
1066
      // A previous action might have resulted in the entity being resaved
1067
      // (e.g. node synchronization from a prior node in this batch), so try
1068
      // to reload it. If no change occurred, the entity will be retrieved
1069
      // from the static cache, resulting in no performance penalty.
1070
      $entity = entity_load_single($entity_type, $entity_id);
1071
      if (empty($entity)) {
1072
        // The entity is no longer valid.
1073
        continue;
1074
      }
1075
    }
1076

    
1077
    // If the current entity can't be accessed, skip it and log a notice.
1078
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
1079
      $message = 'Skipped %operation on @type %title due to insufficient permissions.';
1080
      $arguments = array(
1081
        '%operation' => $operation->label(),
1082
        '@type' => $entity_type,
1083
        '%title' => entity_label($entity_type, $entity),
1084
      );
1085

    
1086
      if ($log) {
1087
        $log[] = t($message, $arguments);
1088
      }
1089
      else {
1090
        watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
1091
      }
1092

    
1093
      continue;
1094
    }
1095

    
1096
    $operation_context = array(
1097
      'progress' => $row['position'],
1098
      'view_info' => $options['view_info'],
1099
    );
1100
    if ($operation->needsRows()) {
1101
      $operation_context['rows'] = array($row_index => $row['views_row']);
1102
    }
1103
    $operation->execute($entity, $operation_context);
1104

    
1105
    unset($row_group[$row_index]);
1106
  }
1107
}
1108

    
1109
/**
1110
 * Adjusts the selection for the direct execution method.
1111
 *
1112
 * Just like the direct method itself, this is legacy code, used only for
1113
 * aggregate actions.
1114
 */
1115
function views_bulk_operations_direct_adjust(&$selection, $vbo) {
1116
  // Adjust selection to select all rows across pages.
1117
  $view = views_get_view($vbo->view->name);
1118
  $view->set_exposed_input($vbo->view->get_exposed_input());
1119
  $view->set_arguments($vbo->view->args);
1120
  $view->set_display($vbo->view->current_display);
1121
  $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
1122
  $view->build();
1123
  // Unset every field except the VBO one (which holds the entity id).
1124
  // That way the performance hit becomes much smaller, because there is no
1125
  // chance of views_handler_field_field::post_execute() firing entity_load().
1126
  foreach ($view->field as $field_name => $field) {
1127
    if ($field_name != $vbo->options['id']) {
1128
      unset($view->field[$field_name]);
1129
    }
1130
  }
1131

    
1132
  $view->execute($vbo->view->current_display);
1133
  $results = array();
1134
  foreach ($view->result as $row_index => $result) {
1135
    $results[$row_index] = $vbo->get_value($result);
1136
  }
1137
  $selection = $results;
1138
}
1139

    
1140
/**
1141
 * Processes the passed rows directly (without batching and queueing).
1142
 */
1143
function views_bulk_operations_direct_process($operation, $rows, $options) {
1144
  global $user;
1145

    
1146
  drupal_set_time_limit(0);
1147

    
1148
  // Prepare an array of status information. Imitates the Batch API naming
1149
  // for consistency. Passed to views_bulk_operations_execute_finished().
1150
  $context = array();
1151
  $context['results']['progress'] = 0;
1152
  $context['results']['log'] = array();
1153

    
1154
  if ($operation->aggregate()) {
1155
    // Load all entities.
1156
    $entity_type = $operation->entityType;
1157
    $entity_ids = array();
1158
    foreach ($rows as $row_index => $row) {
1159
      $entity_ids[] = $row['entity_id'];
1160
    }
1161
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1162

    
1163
    // Filter out entities that can't be accessed.
1164
    foreach ($entities as $id => $entity) {
1165
      if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
1166
        $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
1167
          '%operation' => $operation->label(),
1168
          '@type' => $entity_type,
1169
          '%title' => entity_label($entity_type, $entity),
1170
        ));
1171
        unset($entities[$id]);
1172
      }
1173
    }
1174

    
1175
    // If there are any entities left, execute the operation on them.
1176
    if ($entities) {
1177
      $operation_context = array(
1178
        'view_info' => $options['view_info'],
1179
      );
1180
      // Pass the selected rows to the operation if needed.
1181
      if ($operation->needsRows()) {
1182
        $operation_context['rows'] = array();
1183
        foreach ($rows as $row_index => $row) {
1184
          $operation_context['rows'][$row_index] = $row['views_row'];
1185
        }
1186
      }
1187
      $operation->execute($entities, $operation_context);
1188
    }
1189
  }
1190
  else {
1191
    // Imitate a queue and process the entities one by one.
1192
    $queue_item_data = array(
1193
      'uid' => $user->uid,
1194
      'arguments' => array($rows, $operation, $options),
1195
    );
1196
    views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
1197
  }
1198

    
1199
  $context['results']['progress'] += count($rows);
1200
  $context['results']['log'][] = t('Performed %operation on @items.', array(
1201
    '%operation' => $operation->label(),
1202
    '@items' => format_plural(count($rows), '1 item', '@count items'),
1203
  ));
1204

    
1205
  views_bulk_operations_execute_finished(TRUE, $context['results'], array());
1206
}
1207

    
1208
/**
1209
 * Helper function that runs after the execution process is complete.
1210
 */
1211
function views_bulk_operations_execute_finished($success, $results, $operations) {
1212
  if ($success) {
1213
    if (count($results['log']) > 1) {
1214
      $message = theme('item_list', array('items' => $results['log']));
1215
    }
1216
    else {
1217
      $message = reset($results['log']);
1218
    }
1219
  }
1220
  else {
1221
    // An error occurred.
1222
    // $operations contains the operations that remained unprocessed.
1223
    $error_operation = reset($operations);
1224
    $message = t('An error occurred while processing @operation with arguments: @arguments',
1225
      array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
1226
  }
1227

    
1228
  _views_bulk_operations_log($message);
1229
}
1230

    
1231
/**
1232
 * Helper function to verify access permission to operate on an entity.
1233
 */
1234
function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
1235
  if (!entity_type_supports($entity_type, 'access')) {
1236
    return TRUE;
1237
  }
1238

    
1239
  $access_ops = array(
1240
    VBO_ACCESS_OP_VIEW => 'view',
1241
    VBO_ACCESS_OP_UPDATE => 'update',
1242
    VBO_ACCESS_OP_CREATE => 'create',
1243
    VBO_ACCESS_OP_DELETE => 'delete',
1244
  );
1245
  foreach ($access_ops as $bit => $op) {
1246
    if ($operation->getAccessMask() & $bit) {
1247
      if (!entity_access($op, $entity_type, $entity, $account)) {
1248
        return FALSE;
1249
      }
1250
    }
1251
  }
1252

    
1253
  return TRUE;
1254
}
1255

    
1256
/**
1257
 * Loads multiple entities by their entity or revision ids, and returns them,
1258
 * keyed by the id used for loading.
1259
 */
1260
function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
1261
  if (!$revision) {
1262
    $entities = entity_load($entity_type, $ids);
1263
  }
1264
  else {
1265
    // D7 can't load multiple entities by revision_id. Lovely.
1266
    $info = entity_get_info($entity_type);
1267
    $entities = array();
1268
    foreach ($ids as $revision_id) {
1269
      $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
1270
      $entities[$revision_id] = reset($loaded_entities);
1271
    }
1272
  }
1273

    
1274
  return $entities;
1275
}
1276

    
1277
/**
1278
 * Helper function to report an error.
1279
 */
1280
function _views_bulk_operations_report_error($msg, $arg) {
1281
  watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
1282
  if (function_exists('drush_set_error')) {
1283
    drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
1284
  }
1285
}
1286

    
1287
/**
1288
 * Display a message to the user through the relevant function.
1289
 */
1290
function _views_bulk_operations_log($msg) {
1291
  // Is VBO being run through drush?
1292
  if (function_exists('drush_log')) {
1293
    drush_log(strip_tags($msg), 'ok');
1294
  }
1295
  else {
1296
    drupal_set_message($msg);
1297
  }
1298
}