Projet

Général

Profil

Paste
Télécharger (45,3 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / views_bulk_operations / views_bulk_operations.module @ 7547bb19

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

    
56
  if (!$loaded) {
57
    foreach ($files as $file) {
58
      module_load_include('inc', 'views_bulk_operations', 'actions/' . $file);
59
    }
60
    $loaded = TRUE;
61
  }
62

    
63
  return $files;
64
}
65

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

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

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

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

    
126
  return $themes;
127
}
128

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

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

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

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

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

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

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

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

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

    
218
  // Create a unique hash of the options.
219
  $cid = md5(serialize($options));
220

    
221
  // See if there's a cached copy of the operation, including entity type and
222
  // options.
223
  if (!isset($operations[$operation_id][$entity_type][$cid])) {
224
    // Intentionally not using views_bulk_operations_get_operation_info() here
225
    // since it's an expensive function that loads all the operations on the
226
    // system, despite the fact that we might only need a few.
227
    $id_fragments = explode('::', $operation_id);
228
    $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
229
    $operation_info = $plugin['list callback']($operation_id);
230

    
231
    if ($operation_info) {
232
      $operations[$operation_id][$entity_type][$cid] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options);
233
    }
234
    else {
235
      $operations[$operation_id][$entity_type][$cid] = FALSE;
236
    }
237
  }
238

    
239
  return $operations[$operation_id][$entity_type][$cid];
240
}
241

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

    
260
  return $operations;
261
}
262

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

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

    
293
  return array(
294
    $select_all_placeholder => drupal_render($select_all),
295
  );
296
}
297

    
298
/**
299
 * Implements hook_form_alter().
300
 */
301
function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
302
  if (strpos($form_id, 'views_form_') === 0) {
303
    $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
304
  }
305
  // Not a VBO-enabled views form.
306
  if (empty($vbo)) {
307
    return;
308
  }
309

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

    
330
    $form = views_bulk_operations_form($form, $form_state, $vbo);
331
  }
332

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

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

    
352
  // Give other modules a chance to alter the form.
353
  drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
354
}
355

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

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

    
385
  if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
386
    if (!$enable_select_all_pages) {
387
      return '';
388
    }
389

    
390
    $wrapper_class = 'vbo-table-select-all-markup';
391
    $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
392
    $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
393
    $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
394
    $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));
395

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

    
414
    $form['select_all'] = array(
415
      '#type' => 'fieldset',
416
      '#attributes' => array('class' => array('vbo-fieldset-select-all')),
417
    );
418
    $form['select_all']['this_page'] = array(
419
      '#type' => 'checkbox',
420
      '#title' => t('Select all items on this page'),
421
      '#default_value' => '',
422
      '#attributes' => array('class' => array('vbo-select-this-page')),
423
    );
424

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

    
439
  $output = '<div class="' . $wrapper_class . '">';
440
  $output .= drupal_render($form);
441
  $output .= '</div>';
442

    
443
  return $output;
444
}
445

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

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

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

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

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

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

    
535
  return $form;
536
}
537

    
538
/**
539
 * Validation callback for the first step of the VBO form.
540
 */
541
function views_bulk_operations_form_validate($form, &$form_state) {
542
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
543

    
544
  if (!empty($form_state['triggering_element']['#operation_id'])) {
545
    $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
546
  }
547
  if (!$form_state['values']['operation']) {
548
    form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
549
  }
550

    
551
  $field_name = $vbo->options['id'];
552
  $selection = _views_bulk_operations_get_selection($vbo, $form_state);
553
  if (!$selection) {
554
    form_set_error($field_name, t('Please select at least one item.'));
555
  }
556
}
557

    
558
/**
559
 * Multistep form callback for the "configure" step.
560
 */
561
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
562
  $vbo = _views_bulk_operations_get_field($view);
563
  $operation = $form_state['operation'];
564
  drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
565

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

    
576
  $query = drupal_get_query_parameters($_GET, array('q'));
577
  $form['actions'] = array(
578
    '#type' => 'container',
579
    '#attributes' => array('class' => array('form-actions')),
580
    '#weight' => 999,
581
  );
582
  $form['actions']['submit'] = array(
583
    '#type' => 'submit',
584
    '#value' => t('Next'),
585
    '#validate' => array('views_bulk_operations_config_form_validate'),
586
    '#submit' => array('views_bulk_operations_form_submit'),
587
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
588
  );
589

    
590
  return $form;
591
}
592

    
593
/**
594
 * Validation callback for the "configure" step.
595
 * Gives the operation a chance to validate its config form.
596
 */
597
function views_bulk_operations_config_form_validate($form, &$form_state) {
598
  $operation = &$form_state['operation'];
599
  $operation->formValidate($form, $form_state);
600
}
601

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

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

    
627
  return $form;
628
}
629

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

    
650
  $count = format_plural(count($entities), 'item', '@count items');
651
  $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following %count:', array('%count' => $count))));
652
  return $output;
653
}
654

    
655
/**
656
 * Implements hook_preprocess_page().
657
 *
658
 * Hide action links on the configure and confirm pages.
659
 */
660
function views_bulk_operations_preprocess_page(&$variables) {
661
  if (isset($_POST['select_all'], $_POST['operation'])) {
662
    $variables['action_links'] = array();
663
  }
664
}
665

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

    
678
/**
679
 * Goes through the submitted values, and returns
680
 * an array of selected rows, in the form of
681
 * $row_index => $entity_id.
682
 */
683
function _views_bulk_operations_get_selection($vbo, $form_state) {
684
  $selection = array();
685
  $field_name = $vbo->options['id'];
686

    
687
  if (!empty($form_state['values'][$field_name])) {
688
    // If using "force single", the selection needs to be converted to an array.
689
    if (is_array($form_state['values'][$field_name])) {
690
      $selection = array_filter($form_state['values'][$field_name]);
691
    }
692
    else {
693
      $selection = array($form_state['values'][$field_name]);
694
    }
695
  }
696

    
697
  return $selection;
698
}
699

    
700
/**
701
 * Submit handler for all steps of the VBO multistep form.
702
 */
703
function views_bulk_operations_form_submit($form, &$form_state) {
704
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
705
  $entity_type = $vbo->get_entity_type();
706

    
707
  switch ($form_state['step']) {
708
    case 'views_form_views_form':
709
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
710
      $form_state['select_all_pages'] = $form_state['values']['select_all'];
711

    
712
      $options = $vbo->get_operation_options($form_state['values']['operation']);
713
      $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
714
      if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
715
        break; // Go directly to execution
716
      }
717
      $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
718
      $form_state['rebuild'] = TRUE;
719
      return;
720

    
721
    case 'views_bulk_operations_config_form':
722
      $form_state['step'] = 'views_bulk_operations_confirm_form';
723
      $operation = &$form_state['operation'];
724
      $operation->formSubmit($form, $form_state);
725

    
726
      if ($operation->getAdminOption('skip_confirmation')) {
727
        break; // Go directly to execution
728
      }
729
      $form_state['rebuild'] = TRUE;
730
      return;
731

    
732
    case 'views_bulk_operations_confirm_form':
733
      break;
734
  }
735

    
736
  // Execute the operation.
737
  views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);
738

    
739
  // Redirect.
740
  $query = drupal_get_query_parameters($_GET, array('q'));
741
  $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
742
}
743

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

    
775
  // Determine if the operation needs to be executed directly.
776
  $aggregate = $operation->aggregate();
777
  $skip_batching = $vbo->get_vbo_option('skip_batching');
778
  $force_single = $vbo->get_vbo_option('force_single');
779
  $execute_directly = ($aggregate || $skip_batching || $force_single);
780
  // Try to load all rows without a batch if needed.
781
  if ($execute_directly && $select_all_pages) {
782
    views_bulk_operations_direct_adjust($selection, $vbo);
783
  }
784

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

    
820
  if ($execute_directly) {
821
    // Execute the operation directly and stop here.
822
    views_bulk_operations_direct_process($operation, $rows, $options);
823
    return;
824
  }
825

    
826
  // Determine the correct queue to use.
827
  if ($operation->getAdminOption('postpone_processing')) {
828
    // Use the site queue processed on cron.
829
    $queue_name = 'views_bulk_operations';
830
  }
831
  else {
832
    // Use the active queue processed immediately by Batch API.
833
    $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
834
  }
835

    
836
  $batch = array(
837
    'operations' => array(),
838
    'finished' => 'views_bulk_operations_execute_finished',
839
    'progress_message' => '',
840
    'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
841
  );
842

    
843
  // All items on all pages should be selected, add a batch job to gather
844
  // and enqueue them.
845
  if ($select_all_pages && $vbo->view->query->pager->has_more_records()) {
846
    $total_rows = $vbo->view->total_rows;
847

    
848
    $batch['operations'][] = array(
849
      'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options),
850
    );
851
  }
852
  else {
853
    $total_rows = count($rows);
854

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

    
858
    // Provide a status message to the user, since this is the last step if
859
    // processing is postponed.
860
    if ($operation->getAdminOption('postpone_processing')) {
861
      drupal_set_message(t('Enqueued the selected operation (%operation).', array(
862
        '%operation' => $operation->label(),
863
      )));
864
    }
865
  }
866

    
867
  // Processing is not postponed, add a batch job to process the queue.
868
  if (!$operation->getAdminOption('postpone_processing')) {
869
    $batch['operations'][] = array(
870
      'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
871
    );
872
  }
873

    
874
  // If there are batch jobs to be processed, create the batch set.
875
  if (count($batch['operations'])) {
876
    batch_set($batch);
877
  }
878
}
879

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

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

    
910
  $vbo = _views_bulk_operations_get_field($view);
911

    
912
  // Call views_handler_field_entity::pre_render() to get the entities.
913
  $vbo->pre_render($view->result);
914

    
915
  $rows = array();
916
  foreach ($view->result as $row_index => $result) {
917
    // Set the row index.
918
    $view->row_index = $row_index;
919
    $rows[$row_index] = array(
920
      'entity_id' => $vbo->get_value($result, $vbo->real_field),
921
      'views_row' => array(),
922
      'position' => array(
923
        'current' => ++$context['sandbox']['progress'],
924
        'total' => $context['sandbox']['max'],
925
      ),
926
    );
927
    // Some operations require full selected rows.
928
    if ($operation->needsRows()) {
929
      $rows[$row_index]['views_row'] = $result;
930
    }
931
  }
932

    
933
  // Enqueue the gathered rows.
934
  views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
935

    
936
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
937
    // Provide an estimation of the completion level we've reached.
938
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
939
    $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
940
  }
941
  else {
942
    // Provide a status message to the user if this is the last batch job.
943
    if ($operation->getAdminOption('postpone_processing')) {
944
      $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
945
        '%operation' => $operation->label(),
946
      ));
947
    }
948
  }
949
}
950

    
951
/**
952
 * Divides the passed rows into groups and enqueues each group for processing
953
 *
954
 * @param $queue_name
955
 *   The name of the queue.
956
 * @param $rows
957
 *   The rows to be enqueued.
958
 * @param $operation
959
 *   The object representing the current operation.
960
 *   Passed along with each new queue item.
961
 * @param $options
962
 *   An array of options that affect execution (revision, entity_load_capacity).
963
 *   Passed along with each new queue item.
964
 */
965
function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
966
  global $user;
967

    
968
  $queue = DrupalQueue::get($queue_name, TRUE);
969
  $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
970

    
971
  foreach ($row_groups as $row_group) {
972
    $entity_ids = array();
973
    foreach ($row_group as $row) {
974
      $entity_ids[] = $row['entity_id'];
975
    }
976

    
977
    $job = array(
978
      'title' => t('Perform %operation on @type !entity_ids.', array(
979
        '%operation' => $operation->label(),
980
        '@type' => $operation->entityType,
981
        '!entity_ids' => implode(',', $entity_ids),
982
      )),
983
      'uid' => $user->uid,
984
      'arguments' => array($row_group, $operation, $options),
985
    );
986
    $queue->createItem($job);
987
  }
988
}
989

    
990
/**
991
 * Batch API callback: processes the active queue.
992
 *
993
 * @param $queue_name
994
 *   The name of the queue to process.
995
 * @param $operation
996
 *   The object representing the current operation.
997
 * @param $total_rows
998
 *   The total number of processable items (across all queue items), used
999
 *   to report progress.
1000
 *
1001
 * @see views_bulk_operations_queue_item_process()
1002
 */
1003
function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
1004
  static $queue;
1005

    
1006
  // It is still possible to hit the time limit.
1007
  drupal_set_time_limit(0);
1008

    
1009
  // Prepare the sandbox.
1010
  if (!isset($context['sandbox']['progress'])) {
1011
    $context['sandbox']['progress'] = 0;
1012
    $context['sandbox']['max'] = $total_rows;
1013
    $context['results']['log'] = array();
1014
  }
1015
  // Instantiate the queue.
1016
  if (!isset($queue)) {
1017
    $queue = DrupalQueue::get($queue_name, TRUE);
1018
  }
1019

    
1020
  // Process the queue as long as it has items for us.
1021
  $queue_item = $queue->claimItem(3600);
1022
  if ($queue_item) {
1023
    // Process the queue item, and update the progress count.
1024
    views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
1025
    $queue->deleteItem($queue_item);
1026

    
1027
    // Provide an estimation of the completion level we've reached.
1028
    $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
1029
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
1030
    $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
1031
  }
1032

    
1033
  if (!$queue_item || $context['finished'] === 1) {
1034
    // All done. Provide a status message to the user.
1035
    $context['results']['log'][] = t('Performed %operation on @items.', array(
1036
      '%operation' => $operation->label(),
1037
      '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
1038
    ));
1039
  }
1040
}
1041

    
1042
/**
1043
 * Processes the provided queue item.
1044
 *
1045
 * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
1046
 * to process the site queue, as well as by
1047
 * views_bulk_operations_active_queue_process() to process the active queue.
1048
 *
1049
 * @param $queue_item_arguments
1050
 *   The arguments of the queue item to process.
1051
 * @param $log
1052
 *   An injected array of log messages, to be modified by reference.
1053
 *   If NULL, the function defaults to using watchdog.
1054
 */
1055
function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
1056
  list($row_group, $operation, $options) = $queue_item_data['arguments'];
1057
  $account = user_load($queue_item_data['uid']);
1058
  $entity_type = $operation->entityType;
1059
  $entity_ids = array();
1060
  foreach ($row_group as $row_index => $row) {
1061
    $entity_ids[] = $row['entity_id'];
1062
  }
1063

    
1064
  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1065
  foreach ($row_group as $row_index => $row) {
1066
    $entity_id = $row['entity_id'];
1067
    // A matching entity couldn't be loaded. Skip this item.
1068
    if (!isset($entities[$entity_id])) {
1069
      continue;
1070
    }
1071

    
1072
    if ($options['revision']) {
1073
      // Don't reload revisions for now, they are not statically cached and
1074
      // usually don't run into the edge case described below.
1075
      $entity = $entities[$entity_id];
1076
    }
1077
    else {
1078
      // A previous action might have resulted in the entity being resaved
1079
      // (e.g. node synchronization from a prior node in this batch), so try
1080
      // to reload it. If no change occurred, the entity will be retrieved
1081
      // from the static cache, resulting in no performance penalty.
1082
      $entity = entity_load_single($entity_type, $entity_id);
1083
      if (empty($entity)) {
1084
        // The entity is no longer valid.
1085
        continue;
1086
      }
1087
    }
1088

    
1089
    // If the current entity can't be accessed, skip it and log a notice.
1090
    $skip_permission_check = $operation->getAdminOption('skip_permission_check');
1091
    if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
1092
      $message = 'Skipped %operation on @type %title due to insufficient permissions.';
1093
      $arguments = array(
1094
        '%operation' => $operation->label(),
1095
        '@type' => $entity_type,
1096
        '%title' => entity_label($entity_type, $entity),
1097
      );
1098

    
1099
      if ($log) {
1100
        $log[] = t($message, $arguments);
1101
      }
1102
      else {
1103
        watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
1104
      }
1105

    
1106
      continue;
1107
    }
1108

    
1109
    $operation_context = array(
1110
      'progress' => $row['position'],
1111
      'view_info' => $options['view_info'],
1112
    );
1113
    if ($operation->needsRows()) {
1114
      $operation_context['rows'] = array($row_index => $row['views_row']);
1115
    }
1116
    $operation->execute($entity, $operation_context);
1117

    
1118
    unset($row_group[$row_index]);
1119
  }
1120
}
1121

    
1122
/**
1123
 * Adjusts the selection for the direct execution method.
1124
 *
1125
 * Just like the direct method itself, this is legacy code, used only for
1126
 * aggregate actions.
1127
 */
1128
function views_bulk_operations_direct_adjust(&$selection, $vbo) {
1129
  // Adjust selection to select all rows across pages.
1130
  $view = views_get_view($vbo->view->name);
1131
  $view->set_exposed_input($vbo->view->get_exposed_input());
1132
  $view->set_arguments($vbo->view->args);
1133
  $view->set_display($vbo->view->current_display);
1134
  $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
1135
  $view->build();
1136
  // Unset every field except the VBO one (which holds the entity id).
1137
  // That way the performance hit becomes much smaller, because there is no
1138
  // chance of views_handler_field_field::post_execute() firing entity_load().
1139
  foreach ($view->field as $field_name => $field) {
1140
    if ($field_name != $vbo->options['id']) {
1141
      unset($view->field[$field_name]);
1142
    }
1143
    else {
1144
      // Get hold of the new VBO field.
1145
      $new_vbo = $view->field[$field_name];
1146
    }
1147
  }
1148

    
1149
  $view->execute($vbo->view->current_display);
1150

    
1151
  // Call views_handler_field_entity::pre_render() to get the entities.
1152
  $new_vbo->pre_render($view->result);
1153

    
1154
  $results = array();
1155
  foreach ($view->result as $row_index => $result) {
1156
    // Set the row index.
1157
    $view->row_index = $row_index;
1158
    $results[$row_index] = $new_vbo->get_value($result, $new_vbo->real_field);
1159
  }
1160
  $selection = $results;
1161
}
1162

    
1163
/**
1164
 * Processes the passed rows directly (without batching and queueing).
1165
 */
1166
function views_bulk_operations_direct_process($operation, $rows, $options) {
1167
  global $user;
1168

    
1169
  drupal_set_time_limit(0);
1170

    
1171
  // Prepare an array of status information. Imitates the Batch API naming
1172
  // for consistency. Passed to views_bulk_operations_execute_finished().
1173
  $context = array();
1174
  $context['results']['progress'] = 0;
1175
  $context['results']['log'] = array();
1176

    
1177
  if ($operation->aggregate()) {
1178
    // Load all entities.
1179
    $entity_type = $operation->entityType;
1180
    $entity_ids = array();
1181
    foreach ($rows as $row_index => $row) {
1182
      $entity_ids[] = $row['entity_id'];
1183
    }
1184
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1185

    
1186
    $skip_permission_check = $operation->getAdminOption('skip_permission_check');
1187
    // Filter out entities that can't be accessed.
1188
    foreach ($entities as $id => $entity) {
1189
      if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
1190
        $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
1191
          '%operation' => $operation->label(),
1192
          '@type' => $entity_type,
1193
          '%title' => entity_label($entity_type, $entity),
1194
        ));
1195
        unset($entities[$id]);
1196
      }
1197
    }
1198

    
1199
    // If there are any entities left, execute the operation on them.
1200
    if ($entities) {
1201
      $operation_context = array(
1202
        'view_info' => $options['view_info'],
1203
      );
1204
      // Pass the selected rows to the operation if needed.
1205
      if ($operation->needsRows()) {
1206
        $operation_context['rows'] = array();
1207
        foreach ($rows as $row_index => $row) {
1208
          $operation_context['rows'][$row_index] = $row['views_row'];
1209
        }
1210
      }
1211
      $operation->execute($entities, $operation_context);
1212
    }
1213
  }
1214
  else {
1215
    // Imitate a queue and process the entities one by one.
1216
    $queue_item_data = array(
1217
      'uid' => $user->uid,
1218
      'arguments' => array($rows, $operation, $options),
1219
    );
1220
    views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
1221
  }
1222

    
1223
  $context['results']['progress'] += count($rows);
1224
  $context['results']['log'][] = t('Performed %operation on @items.', array(
1225
    '%operation' => $operation->label(),
1226
    '@items' => format_plural(count($rows), '1 item', '@count items'),
1227
  ));
1228

    
1229
  views_bulk_operations_execute_finished(TRUE, $context['results'], array());
1230
}
1231

    
1232
/**
1233
 * Helper function that runs after the execution process is complete.
1234
 */
1235
function views_bulk_operations_execute_finished($success, $results, $operations) {
1236
  if ($success) {
1237
    if (count($results['log']) > 1) {
1238
      $message = theme('item_list', array('items' => $results['log']));
1239
    }
1240
    else {
1241
      $message = reset($results['log']);
1242
    }
1243
  }
1244
  else {
1245
    // An error occurred.
1246
    // $operations contains the operations that remained unprocessed.
1247
    $error_operation = reset($operations);
1248
    $message = t('An error occurred while processing @operation with arguments: @arguments',
1249
      array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
1250
  }
1251

    
1252
  _views_bulk_operations_log($message);
1253
}
1254

    
1255
/**
1256
 * Helper function to verify access permission to operate on an entity.
1257
 */
1258
function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
1259
  if (!entity_type_supports($entity_type, 'access')) {
1260
    return TRUE;
1261
  }
1262

    
1263
  $access_ops = array(
1264
    VBO_ACCESS_OP_VIEW => 'view',
1265
    VBO_ACCESS_OP_UPDATE => 'update',
1266
    VBO_ACCESS_OP_CREATE => 'create',
1267
    VBO_ACCESS_OP_DELETE => 'delete',
1268
  );
1269
  foreach ($access_ops as $bit => $op) {
1270
    if ($operation->getAccessMask() & $bit) {
1271
      if (!entity_access($op, $entity_type, $entity, $account)) {
1272
        return FALSE;
1273
      }
1274
    }
1275
  }
1276

    
1277
  return TRUE;
1278
}
1279

    
1280
/**
1281
 * Loads multiple entities by their entity or revision ids, and returns them,
1282
 * keyed by the id used for loading.
1283
 */
1284
function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
1285
  if (!$revision) {
1286
    $entities = entity_load($entity_type, $ids);
1287
  }
1288
  else {
1289
    // D7 can't load multiple entities by revision_id. Lovely.
1290
    $info = entity_get_info($entity_type);
1291
    $entities = array();
1292
    foreach ($ids as $revision_id) {
1293
      $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
1294
      $entities[$revision_id] = reset($loaded_entities);
1295
    }
1296
  }
1297

    
1298
  return $entities;
1299
}
1300

    
1301
/**
1302
 * Helper function to report an error.
1303
 */
1304
function _views_bulk_operations_report_error($msg, $arg) {
1305
  watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
1306
  if (function_exists('drush_set_error')) {
1307
    drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
1308
  }
1309
}
1310

    
1311
/**
1312
 * Display a message to the user through the relevant function.
1313
 */
1314
function _views_bulk_operations_log($msg) {
1315
  // Is VBO being run through drush?
1316
  if (function_exists('drush_log')) {
1317
    drush_log(strip_tags($msg), 'ok');
1318
  }
1319
  else {
1320
    drupal_set_message($msg);
1321
  }
1322
}