Projet

Général

Profil

Paste
Télécharger (43,8 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / views_bulk_operations / views_bulk_operations.module @ 87dbc3bf

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
  $path = drupal_get_path('module', 'views_bulk_operations') . '/actions/';
45
  $files = array(
46
    'archive.action.inc',
47
    'argument_selector.action.inc',
48
    'book.action.inc',
49
    'delete.action.inc',
50
    'modify.action.inc',
51
    'script.action.inc',
52
    'user_roles.action.inc',
53
    'user_cancel.action.inc',
54
  );
55

    
56
  if (!$loaded) {
57
    foreach ($files as $file) {
58
      include_once $path . $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 - 864000, '<')
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
  if (!isset($operations[$operation_id])) {
219
    // Intentionally not using views_bulk_operations_get_operation_info() here
220
    // since it's an expensive function that loads all the operations on the
221
    // system, despite the fact that we might only need a few.
222
    $id_fragments = explode('::', $operation_id);
223
    $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
224
    $operation_info = $plugin['list callback']($operation_id);
225

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

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

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

    
255
  return $operations;
256
}
257

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
438
  return $output;
439
}
440

    
441
/**
442
 * Extend the views_form multistep form with elements for executing an operation.
443
 */
444
function views_bulk_operations_form($form, &$form_state, $vbo) {
445
  $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
446
  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
447
  // Wrap the form in a div with specific classes for JS targeting and theming.
448
  $class = 'vbo-views-form';
449
  if (empty($vbo->view->result)) {
450
    $class .= ' vbo-views-form-empty';
451
  }
452
  $form['#prefix'] = '<div class="' . $class . '">';
453
  $form['#suffix'] = '</div>';
454

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

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

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

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

    
523
  return $form;
524
}
525

    
526
/**
527
 * Validation callback for the first step of the VBO form.
528
 */
529
function views_bulk_operations_form_validate($form, &$form_state) {
530
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
531

    
532
  if (!empty($form_state['triggering_element']['#operation_id'])) {
533
    $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
534
  }
535
  if (!$form_state['values']['operation']) {
536
    form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
537
  }
538

    
539
  $field_name = $vbo->options['id'];
540
  $selection = _views_bulk_operations_get_selection($vbo, $form_state);
541
  if (!$selection) {
542
    form_set_error($field_name, t('Please select at least one item.'));
543
  }
544
}
545

    
546
/**
547
 * Multistep form callback for the "configure" step.
548
 */
549
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
550
  $vbo = _views_bulk_operations_get_field($view);
551
  $operation = $form_state['operation'];
552
  drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
553

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

    
564
  $query = drupal_get_query_parameters($_GET, array('q'));
565
  $form['actions'] = array(
566
    '#type' => 'container',
567
    '#attributes' => array('class' => array('form-actions')),
568
    '#weight' => 999,
569
  );
570
  $form['actions']['submit'] = array(
571
    '#type' => 'submit',
572
    '#value' => t('Next'),
573
    '#validate' => array('views_bulk_operations_config_form_validate'),
574
    '#submit' => array('views_bulk_operations_form_submit'),
575
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
576
  );
577

    
578
  return $form;
579
}
580

    
581
/**
582
 * Validation callback for the "configure" step.
583
 * Gives the operation a chance to validate its config form.
584
 */
585
function views_bulk_operations_config_form_validate($form, &$form_state) {
586
  $operation = &$form_state['operation'];
587
  $operation->formValidate($form, $form_state);
588
}
589

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

    
606
  return $form;
607
}
608

    
609
/**
610
 * Theme function to show the confirmation page before executing the operation.
611
 */
612
function theme_views_bulk_operations_confirmation($variables) {
613
  $select_all_pages = $variables['select_all_pages'];
614
  $vbo = $variables['vbo'];
615
  $entity_type = $vbo->get_entity_type();
616
  $rows = $variables['rows'];
617
  $items = array();
618
  // Load the entities from the current page, and show their titles.
619
  $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
620
  foreach ($entities as $entity) {
621
    $items[] = check_plain(_views_bulk_operations_entity_label($entity_type, $entity));
622
  }
623
  // All rows on all pages have been selected, so show a count of additional items.
624
  if ($select_all_pages) {
625
    $more_count = $vbo->view->total_rows - count($vbo->view->result);
626
    $items[] = t('...and <strong>!count</strong> more.', array('!count' => $more_count));
627
  }
628

    
629
  $count = format_plural(count($entities), 'item', '@count items');
630
  $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following <strong>!count</strong>:', array('!count' => $count))));
631
  return $output;
632
}
633

    
634
/**
635
 * Goes through the submitted values, and returns
636
 * an array of selected rows, in the form of
637
 * $row_index => $entity_id.
638
 */
639
function _views_bulk_operations_get_selection($vbo, $form_state) {
640
  $selection = array();
641
  $field_name = $vbo->options['id'];
642

    
643
  if (!empty($form_state['values'][$field_name])) {
644
    // If using "force single", the selection needs to be converted to an array.
645
    if (is_array($form_state['values'][$field_name])) {
646
      $selection = array_filter($form_state['values'][$field_name]);
647
    }
648
    else {
649
      $selection = array($form_state['values'][$field_name]);
650
    }
651
  }
652

    
653
  return $selection;
654
}
655

    
656
/**
657
 * Submit handler for all steps of the VBO multistep form.
658
 */
659
function views_bulk_operations_form_submit($form, &$form_state) {
660
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
661
  $entity_type = $vbo->get_entity_type();
662

    
663
  switch ($form_state['step']) {
664
    case 'views_form_views_form':
665
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
666
      $form_state['select_all_pages'] = $form_state['values']['select_all'];
667

    
668
      $options = $vbo->get_operation_options($form_state['values']['operation']);
669
      $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
670
      if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
671
        break; // Go directly to execution
672
      }
673
      $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
674
      $form_state['rebuild'] = TRUE;
675
      return;
676

    
677
    case 'views_bulk_operations_config_form':
678
      $form_state['step'] = 'views_bulk_operations_confirm_form';
679
      $operation = &$form_state['operation'];
680
      $operation->formSubmit($form, $form_state);
681

    
682
      if ($operation->getAdminOption('skip_confirmation')) {
683
        break; // Go directly to execution
684
      }
685
      $form_state['rebuild'] = TRUE;
686
      return;
687

    
688
    case 'views_bulk_operations_confirm_form':
689
      break;
690
  }
691

    
692
  // Execute the operation.
693
  views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);
694

    
695
  // Redirect.
696
  $query = drupal_get_query_parameters($_GET, array('q'));
697
  $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
698
}
699

    
700
/**
701
 * Entry point for executing the chosen operation upon selected rows.
702
 *
703
 * If the selected operation is an aggregate operation (requiring all selected
704
 * items to be passed at the same time), restricted to a single value, or has
705
 * the skip_batching option set, the operation is executed directly.
706
 * This means that there is no batching & queueing, the PHP execution
707
 * time limit is ignored (if allowed), all selected entities are loaded and
708
 * processed.
709
 *
710
 * Otherwise, the selected entity ids are divided into groups not larger than
711
 * $entity_load_capacity, and enqueued for processing.
712
 * If all items on all pages should be processed, a batch job runs that
713
 * collects and enqueues the items from all pages of the view, page by page.
714
 *
715
 * Based on the "Enqueue the operation instead of executing it directly"
716
 * VBO field setting, the newly filled queue is either processed at cron
717
 * time by the VBO worker function, or right away in a new batch job.
718
 *
719
 * @param $vbo
720
 *   The VBO field, containing a reference to the view in $vbo->view.
721
 * @param $operation
722
 *   The operation object.
723
 * @param $selection
724
 *   An array in the form of $row_index => $entity_id.
725
 * @param $select_all_pages
726
 *   Whether all items on all pages should be selected.
727
 */
728
function views_bulk_operations_execute($vbo, $operation, $selection, $select_all_pages = FALSE) {
729
  global $user;
730

    
731
  // Determine if the operation needs to be executed directly.
732
  $aggregate = $operation->aggregate();
733
  $skip_batching = $vbo->get_vbo_option('skip_batching');
734
  $force_single = $vbo->get_vbo_option('force_single');
735
  $execute_directly = ($aggregate || $skip_batching || $force_single);
736
  // Try to load all rows without a batch if needed.
737
  if ($execute_directly && $select_all_pages) {
738
    views_bulk_operations_direct_adjust($selection, $vbo);
739
  }
740

    
741
  // Options that affect execution.
742
  $options = array(
743
    'revision' => $vbo->revision,
744
    'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
745
    // The information needed to recreate the view, to avoid serializing the
746
    // whole object. Passed to the executed operation. Also used by
747
    // views_bulk_operations_adjust_selection().
748
    'view_info' => array(
749
      'name' => $vbo->view->name,
750
      'display' => $vbo->view->current_display,
751
      'arguments' => $vbo->view->args,
752
      'exposed_input' => $vbo->view->get_exposed_input(),
753
    ),
754
  );
755
  // Create an array of rows in the needed format.
756
  $rows = array();
757
  $current = 1;
758
  foreach ($selection as $row_index => $entity_id) {
759
    $rows[$row_index] = array(
760
      'entity_id' => $entity_id,
761
      'views_row' => array(),
762
      // Some operations rely on knowing the position of the current item
763
      // in the execution set (because of specific things that need to be done
764
      // at the beginning or the end of the set).
765
      'position' => array(
766
        'current' => $current++,
767
        'total' => count($selection),
768
      ),
769
    );
770
    // Some operations require full selected rows.
771
    if ($operation->needsRows()) {
772
      $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
773
    }
774
  }
775

    
776
  if ($execute_directly) {
777
    // Execute the operation directly and stop here.
778
    views_bulk_operations_direct_process($operation, $rows, $options);
779
    return;
780
  }
781

    
782
  // Determine the correct queue to use.
783
  if ($operation->getAdminOption('postpone_processing')) {
784
    // Use the site queue processed on cron.
785
    $queue_name = 'views_bulk_operations';
786
  }
787
  else {
788
    // Use the active queue processed immediately by Batch API.
789
    $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
790
  }
791

    
792
  $batch = array(
793
    'operations' => array(),
794
    'finished' => 'views_bulk_operations_execute_finished',
795
    'progress_message' => '',
796
    'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
797
  );
798

    
799
  // All items on all pages should be selected, add a batch job to gather
800
  // and enqueue them.
801
  if ($select_all_pages && $vbo->view->query->pager->has_more_records()) {
802
    $total_rows = $vbo->view->total_rows;
803

    
804
    $batch['operations'][] = array(
805
      'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options),
806
    );
807
  }
808
  else {
809
    $total_rows = count($rows);
810

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

    
814
    // Provide a status message to the user, since this is the last step if
815
    // processing is postponed.
816
    if ($operation->getAdminOption('postpone_processing')) {
817
      drupal_set_message(t('Enqueued the selected operation (%operation).', array(
818
        '%operation' => $operation->label(),
819
      )));
820
    }
821
  }
822

    
823
  // Processing is not postponed, add a batch job to process the queue.
824
  if (!$operation->getAdminOption('postpone_processing')) {
825
    $batch['operations'][] = array(
826
      'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
827
    );
828
  }
829

    
830
  // If there are batch jobs to be processed, create the batch set.
831
  if (count($batch['operations'])) {
832
    batch_set($batch);
833
  }
834
}
835

    
836
/**
837
 * Batch API callback: loads the view page by page and enqueues all items.
838
 *
839
 * @param $queue_name
840
 *   The name of the queue to which the items should be added.
841
 * @param $operation
842
 *   The operation object.
843
 * @param $options
844
 *   An array of options that affect execution (revision, entity_load_capacity,
845
 *   view_info). Passed along with each new queue item.
846
 */
847
function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) {
848
  if (!isset($context['sandbox']['progress'])) {
849
    $context['sandbox']['progress'] = 0;
850
    $context['sandbox']['max'] = 0;
851
  }
852

    
853
  $view_info = $options['view_info'];
854
  $view = views_get_view($view_info['name']);
855
  $view->set_exposed_input($view_info['exposed_input']);
856
  $view->set_arguments($view_info['arguments']);
857
  $view->set_display($view_info['display']);
858
  $view->set_offset($context['sandbox']['progress']);
859
  $view->build();
860
  $view->execute($view_info['display']);
861
  // Note the total number of rows.
862
  if (empty($context['sandbox']['max'])) {
863
    $context['sandbox']['max'] = $view->total_rows;
864
  }
865

    
866
  $vbo = _views_bulk_operations_get_field($view);
867
  $rows = array();
868
  foreach ($view->result as $row_index => $result) {
869
    $rows[$row_index] = array(
870
      'entity_id' => $vbo->get_value($result),
871
      'views_row' => array(),
872
      'position' => array(
873
        'current' => ++$context['sandbox']['progress'],
874
        'total' => $view->total_rows,
875
      ),
876
    );
877
    // Some operations require full selected rows.
878
    if ($operation->needsRows()) {
879
      $rows[$row_index]['views_row'] = $result;
880
    }
881
  }
882

    
883
  // Enqueue the gathered rows.
884
  views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
885

    
886
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
887
    // Provide an estimation of the completion level we've reached.
888
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
889
    $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
890
  }
891
  else {
892
    // Provide a status message to the user if this is the last batch job.
893
    if ($operation->getAdminOption('postpone_processing')) {
894
      $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
895
        '%operation' => $operation->label(),
896
      ));
897
    }
898
  }
899
}
900

    
901
/**
902
 * Divides the passed rows into groups and enqueues each group for processing
903
 *
904
 * @param $queue_name
905
 *   The name of the queue.
906
 * @param $rows
907
 *   The rows to be enqueued.
908
 * @param $operation
909
 *   The object representing the current operation.
910
 *   Passed along with each new queue item.
911
 * @param $options
912
 *   An array of options that affect execution (revision, entity_load_capacity).
913
 *   Passed along with each new queue item.
914
 */
915
function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
916
  global $user;
917

    
918
  $queue = DrupalQueue::get($queue_name, TRUE);
919
  $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
920

    
921
  foreach ($row_groups as $row_group) {
922
    $entity_ids = array();
923
    foreach ($row_group as $row) {
924
      $entity_ids[] = $row['entity_id'];
925
    }
926

    
927
    $job = array(
928
      'title' => t('Perform %operation on @type !entity_ids.', array(
929
        '%operation' => $operation->label(),
930
        '@type' => $operation->entityType,
931
        '!entity_ids' => implode(',', $entity_ids),
932
      )),
933
      'uid' => $user->uid,
934
      'arguments' => array($row_group, $operation, $options),
935
    );
936
    $queue->createItem($job);
937
  }
938
}
939

    
940
/**
941
 * Batch API callback: processes the active queue.
942
 *
943
 * @param $queue_name
944
 *   The name of the queue to process.
945
 * @param $operation
946
 *   The object representing the current operation.
947
 * @param $total_rows
948
 *   The total number of processable items (across all queue items), used
949
 *   to report progress.
950
 *
951
 * @see views_bulk_operations_queue_item_process()
952
 */
953
function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
954
  static $queue;
955

    
956
  // It is still possible to hit the time limit.
957
  drupal_set_time_limit(0);
958

    
959
  // Prepare the sandbox.
960
  if (!isset($context['sandbox']['progress'])) {
961
    $context['sandbox']['progress'] = 0;
962
    $context['sandbox']['max'] = $total_rows;
963
    $context['results']['log'] = array();
964
  }
965
  // Instantiate the queue.
966
  if (!isset($queue)) {
967
    $queue = DrupalQueue::get($queue_name, TRUE);
968
  }
969

    
970
  // Process the queue as long as it has items for us.
971
  $queue_item = $queue->claimItem(3600);
972
  if ($queue_item) {
973
    // Process the queue item, and update the progress count.
974
    views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
975
    $queue->deleteItem($queue_item);
976

    
977
    // Provide an estimation of the completion level we've reached.
978
    $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
979
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
980
    $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
981
  }
982

    
983
  if (!$queue_item || $context['finished'] === 1) {
984
    // All done. Provide a status message to the user.
985
    $context['results']['log'][] = t('Performed %operation on @items.', array(
986
      '%operation' => $operation->label(),
987
      '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
988
    ));
989
  }
990
}
991

    
992
/**
993
 * Processes the provided queue item.
994
 *
995
 * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
996
 * to process the site queue, as well as by
997
 * views_bulk_operations_active_queue_process() to process the active queue.
998
 *
999
 * @param $queue_item_arguments
1000
 *   The arguments of the queue item to process.
1001
 * @param $log
1002
 *   An injected array of log messages, to be modified by reference.
1003
 *   If NULL, the function defaults to using watchdog.
1004
 */
1005
function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
1006
  list($row_group, $operation, $options) = $queue_item_data['arguments'];
1007
  $account = user_load($queue_item_data['uid']);
1008
  $entity_type = $operation->entityType;
1009
  $entity_ids = array();
1010
  foreach ($row_group as $row_index => $row) {
1011
    $entity_ids[] = $row['entity_id'];
1012
  }
1013

    
1014
  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1015
  foreach ($row_group as $row_index => $row) {
1016
    $entity_id = $row['entity_id'];
1017
    // A matching entity couldn't be loaded. Skip this item.
1018
    if (!isset($entities[$entity_id])) {
1019
      continue;
1020
    }
1021

    
1022
    if ($options['revision']) {
1023
      // Don't reload revisions for now, they are not statically cached and
1024
      // usually don't run into the edge case described below.
1025
      $entity = $entities[$entity_id];
1026
    }
1027
    else {
1028
      // A previous action might have resulted in the entity being resaved
1029
      // (e.g. node synchronization from a prior node in this batch), so try
1030
      // to reload it. If no change occurred, the entity will be retrieved
1031
      // from the static cache, resulting in no performance penalty.
1032
      $entity = entity_load_single($entity_type, $entity_id);
1033
      if (empty($entity)) {
1034
        // The entity is no longer valid.
1035
        continue;
1036
      }
1037
    }
1038

    
1039
    // If the current entity can't be accessed, skip it and log a notice.
1040
    if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
1041
      $message = 'Skipped %operation on @type %title due to insufficient permissions.';
1042
      $arguments = array(
1043
        '%operation' => $operation->label(),
1044
        '@type' => $entity_type,
1045
        '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
1046
      );
1047

    
1048
      if ($log) {
1049
        $log[] = t($message, $arguments);
1050
      }
1051
      else {
1052
        watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
1053
      }
1054

    
1055
      continue;
1056
    }
1057

    
1058
    $operation_context = array(
1059
      'progress' => $row['position'],
1060
      'view_info' => $options['view_info'],
1061
    );
1062
    if ($operation->needsRows()) {
1063
      $operation_context['rows'] = array($row_index => $row['views_row']);
1064
    }
1065
    $operation->execute($entity, $operation_context);
1066

    
1067
    unset($row_group[$row_index]);
1068
  }
1069
}
1070

    
1071
/**
1072
 * Adjusts the selection for the direct execution method.
1073
 *
1074
 * Just like the direct method itself, this is legacy code, used only for
1075
 * aggregate actions.
1076
 */
1077
function views_bulk_operations_direct_adjust(&$selection, $vbo) {
1078
  // Adjust selection to select all rows across pages.
1079
  $view = views_get_view($vbo->view->name);
1080
  $view->set_exposed_input($vbo->view->get_exposed_input());
1081
  $view->set_arguments($vbo->view->args);
1082
  $view->set_display($vbo->view->current_display);
1083
  $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
1084
  $view->build();
1085
  // Unset every field except the VBO one (which holds the entity id).
1086
  // That way the performance hit becomes much smaller, because there is no
1087
  // chance of views_handler_field_field::post_execute() firing entity_load().
1088
  foreach ($view->field as $field_name => $field) {
1089
    if ($field_name != $vbo->options['id']) {
1090
      unset($view->field[$field_name]);
1091
    }
1092
  }
1093

    
1094
  $view->execute($vbo->view->current_display);
1095
  $results = array();
1096
  foreach ($view->result as $row_index => $result) {
1097
    $results[$row_index] = $vbo->get_value($result);
1098
  }
1099
  $selection = $results;
1100
}
1101

    
1102
/**
1103
 * Processes the passed rows directly (without batching and queueing).
1104
 */
1105
function views_bulk_operations_direct_process($operation, $rows, $options) {
1106
  global $user;
1107

    
1108
  drupal_set_time_limit(0);
1109

    
1110
  // Prepare an array of status information. Imitates the Batch API naming
1111
  // for consistency. Passed to views_bulk_operations_execute_finished().
1112
  $context = array();
1113
  $context['results']['progress'] = 0;
1114
  $context['results']['log'] = array();
1115

    
1116
  if ($operation->aggregate()) {
1117
    // Load all entities.
1118
    $entity_type = $operation->entityType;
1119
    $entity_ids = array();
1120
    foreach ($rows as $row_index => $row) {
1121
      $entity_ids[] = $row['entity_id'];
1122
    }
1123
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1124

    
1125
    // Filter out entities that can't be accessed.
1126
    foreach ($entities as $id => $entity) {
1127
      if (!_views_bulk_operations_entity_access($operation, $entity_type, $entity)) {
1128
        $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
1129
          '%operation' => $operation->label(),
1130
          '@type' => $entity_type,
1131
          '%title' => _views_bulk_operations_entity_label($entity_type, $entity),
1132
        ));
1133
        unset($entities[$id]);
1134
      }
1135
    }
1136

    
1137
    // If there are any entities left, execute the operation on them.
1138
    if ($entities) {
1139
      $operation_context = array(
1140
        'view_info' => $options['view_info'],
1141
      );
1142
      // Pass the selected rows to the operation if needed.
1143
      if ($operation->needsRows()) {
1144
        $operation_context['rows'] = array();
1145
        foreach ($rows as $row_index => $row) {
1146
          $operation_context['rows'][$row_index] = $row['views_row'];
1147
        }
1148
      }
1149
      $operation->execute($entities, $operation_context);
1150
    }
1151
  }
1152
  else {
1153
    // Imitate a queue and process the entities one by one.
1154
    $queue_item_data = array(
1155
      'uid' => $user->uid,
1156
      'arguments' => array($rows, $operation, $options),
1157
    );
1158
    views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
1159
  }
1160

    
1161
  $context['results']['progress'] += count($rows);
1162
  $context['results']['log'][] = t('Performed %operation on @items.', array(
1163
    '%operation' => $operation->label(),
1164
    '@items' => format_plural(count($rows), '1 item', '@count items'),
1165
  ));
1166

    
1167
  views_bulk_operations_execute_finished(TRUE, $context['results'], array());
1168
}
1169

    
1170
/**
1171
 * Helper function that runs after the execution process is complete.
1172
 */
1173
function views_bulk_operations_execute_finished($success, $results, $operations) {
1174
  if ($success) {
1175
    if (count($results['log']) > 1) {
1176
      $message = theme('item_list', array('items' => $results['log']));
1177
    }
1178
    else {
1179
      $message = reset($results['log']);
1180
    }
1181
  }
1182
  else {
1183
    // An error occurred.
1184
    // $operations contains the operations that remained unprocessed.
1185
    $error_operation = reset($operations);
1186
    $message = t('An error occurred while processing @operation with arguments: @arguments',
1187
      array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
1188
  }
1189

    
1190
  _views_bulk_operations_log($message);
1191
}
1192

    
1193
/**
1194
 * Helper function to verify access permission to operate on an entity.
1195
 */
1196
function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
1197
  if (!entity_type_supports($entity_type, 'access')) {
1198
    return TRUE;
1199
  }
1200

    
1201
  $access_ops = array(
1202
    VBO_ACCESS_OP_VIEW => 'view',
1203
    VBO_ACCESS_OP_UPDATE => 'update',
1204
    VBO_ACCESS_OP_CREATE => 'create',
1205
    VBO_ACCESS_OP_DELETE => 'delete',
1206
  );
1207
  foreach ($access_ops as $bit => $op) {
1208
    if ($operation->getAccessMask() & $bit) {
1209
      if (!entity_access($op, $entity_type, $entity, $account)) {
1210
        return FALSE;
1211
      }
1212
    }
1213
  }
1214

    
1215
  return TRUE;
1216
}
1217

    
1218
/**
1219
 * Loads multiple entities by their entity or revision ids, and returns them,
1220
 * keyed by the id used for loading.
1221
 */
1222
function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
1223
  if (!$revision) {
1224
    $entities = entity_load($entity_type, $ids);
1225
  }
1226
  else {
1227
    // D7 can't load multiple entities by revision_id. Lovely.
1228
    $info = entity_get_info($entity_type);
1229
    $entities = array();
1230
    foreach ($ids as $revision_id) {
1231
      $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
1232
      $entities[$revision_id] = reset($loaded_entities);
1233
    }
1234
  }
1235

    
1236
  return $entities;
1237
}
1238

    
1239
/**
1240
 * Label function for entities.
1241
 * Core entities don't declare the "label" key, so entity_label() fails,
1242
 * and a fallback is needed. This function provides that fallback.
1243
 */
1244
function _views_bulk_operations_entity_label($entity_type, $entity) {
1245
  $label = entity_label($entity_type, $entity);
1246
  if (!$label) {
1247
    $entity_info = entity_get_info($entity_type);
1248
    $id_key = $entity_info['entity keys']['id'];
1249
    // Many entity types (e.g. "user") have a name which fits the label perfectly.
1250
    if (isset($entity->name)) {
1251
      $label = $entity->name;
1252
    }
1253
    elseif (isset($entity->{$id_key})) {
1254
      // Fallback to the id key.
1255
      $label = $entity->{$id_key};
1256
    }
1257
  }
1258

    
1259
  return $label;
1260
}
1261

    
1262
/**
1263
 * Helper function to report an error.
1264
 */
1265
function _views_bulk_operations_report_error($msg, $arg) {
1266
  watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
1267
  if (function_exists('drush_set_error')) {
1268
    drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
1269
  }
1270
}
1271

    
1272
/**
1273
 * Display a message to the user through the relevant function.
1274
 */
1275
function _views_bulk_operations_log($msg) {
1276
  // Is VBO being run through drush?
1277
  if (function_exists('drush_log')) {
1278
    drush_log(strip_tags($msg), 'ok');
1279
  }
1280
  else {
1281
    drupal_set_message($msg);
1282
  }
1283
}