Projet

Général

Profil

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

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

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
 *
17
 * Registers custom VBO actions as Drupal actions.
18
 */
19
function views_bulk_operations_action_info() {
20
  $actions = array();
21
  $files = views_bulk_operations_load_action_includes();
22
  foreach ($files as $filename) {
23
    $action_info_fn = 'views_bulk_operations_'. str_replace('.', '_', basename($filename, '.inc')).'_info';
24
    if (is_callable($action_info_fn)) {
25
      $action_info = call_user_func($action_info_fn);
26
      if (is_array($action_info)) {
27
        $actions += $action_info;
28
      }
29
    }
30
    else {
31
      watchdog('views bulk operations', 'views_bulk_operations_action_info() expects action filenames to have a matching valid callback function named: %function', array('%function' => $action_info_fn), WATCHDOG_WARNING);
32
    }
33
  }
34

    
35
  return $actions;
36
}
37

    
38
/**
39
 * Loads the VBO actions placed in their own include files (under actions/).
40
 *
41
 * @return
42
 *   An array of containing filenames of the included actions.
43
 */
44
function views_bulk_operations_load_action_includes() {
45
  static $loaded = FALSE;
46

    
47
  // The list of VBO actions is fairly static, so it's hardcoded for better
48
  // performance (hitting the filesystem with file_scan_directory(), and then
49
  // caching the result has its cost).
50
  $files = array(
51
    'archive.action',
52
    'argument_selector.action',
53
    'book.action',
54
    'change_owner.action',
55
    'delete.action',
56
    'modify.action',
57
    'script.action',
58
    'user_roles.action',
59
    'user_cancel.action',
60
  );
61

    
62
  if (!$loaded) {
63
    foreach ($files as $file) {
64
      module_load_include('inc', 'views_bulk_operations', 'actions/' . $file);
65
    }
66
    $loaded = TRUE;
67
  }
68

    
69
  return $files;
70
}
71

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

    
90
/**
91
 * Implements of hook_cron_queue_info().
92
 */
93
function views_bulk_operations_cron_queue_info() {
94
  return array(
95
    'views_bulk_operations' => array(
96
      'worker callback' => 'views_bulk_operations_queue_item_process',
97
      'time' => 30,
98
    ),
99
  );
100
}
101

    
102
/**
103
 * Implements hook_views_api().
104
 */
105
function views_bulk_operations_views_api() {
106
  return array(
107
    'api' => 3,
108
    'path' => drupal_get_path('module', 'views_bulk_operations') . '/views',
109
  );
110
}
111

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

    
132
  return $themes;
133
}
134

    
135
/**
136
 * Implements hook_ctools_plugin_type().
137
 */
138
function views_bulk_operations_ctools_plugin_type() {
139
  return array(
140
    'operation_types' => array(
141
      'classes' => array(
142
        'handler',
143
      ),
144
    ),
145
  );
146
}
147

    
148
/**
149
 * Implements hook_ctools_plugin_directory().
150
 */
151
function views_bulk_operations_ctools_plugin_directory($module, $plugin) {
152
  if ($module == 'views_bulk_operations') {
153
    return 'plugins/' . $plugin;
154
  }
155
}
156

    
157
/**
158
 * Fetch metadata for a specific operation type plugin.
159
 *
160
 * @param $operation_type
161
 *   Name of the plugin.
162
 *
163
 * @return
164
 *   An array with information about the requested operation type plugin.
165
 */
166
function views_bulk_operations_get_operation_type($operation_type) {
167
  ctools_include('plugins');
168
  return ctools_get_plugins('views_bulk_operations', 'operation_types', $operation_type);
169
}
170

    
171
/**
172
 * Fetch metadata for all operation type plugins.
173
 *
174
 * @return
175
 *   An array of arrays with information about all available operation types.
176
 */
177
function views_bulk_operations_get_operation_types() {
178
  ctools_include('plugins');
179
  return ctools_get_plugins('views_bulk_operations', 'operation_types');
180
}
181

    
182
/**
183
  * Gets the info array of an operation from the provider plugin.
184
  *
185
  * @param $operation_id
186
  *   The id of the operation for which the info shall be returned, or NULL
187
  *   to return an array with info about all operations.
188
  */
189
function views_bulk_operations_get_operation_info($operation_id = NULL) {
190
  $operations = &drupal_static(__FUNCTION__);
191

    
192
  if (!isset($operations)) {
193
    $operations = array();
194
    $plugins = views_bulk_operations_get_operation_types();
195
    foreach ($plugins as $plugin) {
196
      $operations += $plugin['list callback']();
197
    }
198

    
199
    uasort($operations, '_views_bulk_operations_sort_operations_by_label');
200
  }
201

    
202
  if (!empty($operation_id)) {
203
    return $operations[$operation_id];
204
  }
205
  else {
206
    return $operations;
207
  }
208
}
209

    
210
/**
211
 * Sort function used by uasort in views_bulk_operations_get_operation_info().
212
 *
213
 * A closure would be better suited for this, but closure support was added in
214
 * PHP 5.3 and D7 supports 5.2.
215
 */
216
function _views_bulk_operations_sort_operations_by_label($a, $b) {
217
  return strcasecmp($a['label'], $b['label']);
218
}
219

    
220
/**
221
 * Returns an operation instance.
222
 *
223
 * @param $operation_id
224
 *   The id of the operation to instantiate.
225
 *   For example: action::node_publish_action.
226
 * @param $entity_type
227
 *   The entity type on which the operation operates.
228
 * @param $options
229
 *   Options for this operation (label, operation settings, etc.)
230
 */
231
function views_bulk_operations_get_operation($operation_id, $entity_type, $options) {
232
  $operations = &drupal_static(__FUNCTION__);
233

    
234
  // Create a unique hash of the options.
235
  $cid = md5(serialize($options));
236

    
237
  // See if there's a cached copy of the operation, including entity type and
238
  // options.
239
  if (!isset($operations[$operation_id][$entity_type][$cid])) {
240
    // Intentionally not using views_bulk_operations_get_operation_info() here
241
    // since it's an expensive function that loads all the operations on the
242
    // system, despite the fact that we might only need a few.
243
    $id_fragments = explode('::', $operation_id);
244
    $plugin = views_bulk_operations_get_operation_type($id_fragments[0]);
245
    $operation_info = $plugin['list callback']($operation_id);
246

    
247
    if ($operation_info) {
248
      $operations[$operation_id][$entity_type][$cid] = new $plugin['handler']['class']($operation_id, $entity_type, $operation_info, $options);
249
    }
250
    else {
251
      $operations[$operation_id][$entity_type][$cid] = FALSE;
252
    }
253
  }
254

    
255
  return $operations[$operation_id][$entity_type][$cid];
256
}
257

    
258
/**
259
 * Get all operations that match the current entity type.
260
 *
261
 * @param $entity_type
262
 *   Entity type.
263
 * @param $options
264
 *   An array of options for all operations, in the form of
265
 *   $operation_id => $operation_options.
266
 */
267
function views_bulk_operations_get_applicable_operations($entity_type, $options) {
268
  $operations = array();
269
  foreach (views_bulk_operations_get_operation_info() as $operation_id => $operation_info) {
270
    if ($operation_info['type'] == $entity_type || $operation_info['type'] == 'entity' || $operation_info['type'] == 'system') {
271
      $options[$operation_id] = !empty($options[$operation_id]) ? $options[$operation_id] : array();
272
      $operations[$operation_id] = views_bulk_operations_get_operation($operation_id, $entity_type, $options[$operation_id]);
273
    }
274
  }
275

    
276
  return $operations;
277
}
278

    
279
/**
280
 * Gets the VBO field if it exists on the passed-in view.
281
 *
282
 * @return
283
 *  The field object if found. Otherwise, FALSE.
284
 */
285
function _views_bulk_operations_get_field($view) {
286
  foreach ($view->field as $field_name => $field) {
287
    if ($field instanceof views_bulk_operations_handler_field_operations) {
288
      // Add in the view object for convenience.
289
      $field->view = $view;
290
      return $field;
291
    }
292
  }
293
  return FALSE;
294
}
295

    
296
/**
297
 * Implements hook_views_form_substitutions().
298
 */
299
function views_bulk_operations_views_form_substitutions() {
300
  // Views check_plains the column label, so VBO needs to do the same
301
  // in order for the replace operation to succeed.
302
  $select_all_placeholder = check_plain('<!--views-bulk-operations-select-all-->');
303
  $select_all = array(
304
    '#type' => 'checkbox',
305
    '#default_value' => FALSE,
306
    '#attributes' => array('class' => array('vbo-table-select-all')),
307
  );
308

    
309
  return array(
310
    $select_all_placeholder => drupal_render($select_all),
311
  );
312
}
313

    
314
/**
315
 * Implements hook_form_alter().
316
 */
317
function views_bulk_operations_form_alter(&$form, &$form_state, $form_id) {
318
  if (strpos($form_id, 'views_form_') === 0) {
319
    $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
320
  }
321
  // Not a VBO-enabled views form.
322
  if (empty($vbo)) {
323
    return;
324
  }
325

    
326
  // Add basic VBO functionality.
327
  if ($form_state['step'] == 'views_form_views_form') {
328
    // The submit button added by Views Form API might be used by a non-VBO Views
329
    // Form handler. If there's no such handler on the view, hide the button.
330
    $has_other_views_form_handlers = FALSE;
331
    foreach ($vbo->view->field as $field) {
332
      if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
333
        if (!($field instanceof views_bulk_operations_handler_field_operations)) {
334
          $has_other_views_form_handlers = TRUE;
335
        }
336
      }
337
    }
338
    if (!$has_other_views_form_handlers) {
339
      $form['actions']['#access'] = FALSE;
340
    }
341
    // The VBO field is excluded from display, stop here.
342
    if (!empty($vbo->options['exclude'])) {
343
      return;
344
    }
345

    
346
    $form = views_bulk_operations_form($form, $form_state, $vbo);
347
  }
348

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

    
355
  if (empty($vbo->view->override_url)) {
356
    // If the VBO view is embedded using views_embed_view(), or in a block,
357
    // $view->get_url() doesn't point to the current page, which means that
358
    // the form doesn't get processed.
359
    if (!empty($vbo->view->preview) || $vbo->view->display_handler instanceof views_plugin_display_block) {
360
      $vbo->view->override_url = $_GET['q'];
361
      // We are changing the override_url too late, the form action was already
362
      // set by Views to the previous URL, so it needs to be overriden as well.
363
      $query = drupal_get_query_parameters($_GET, array('q'));
364
      $form['#action'] = url($_GET['q'], array('query' => $query));
365
    }
366
  }
367

    
368
  // Give other modules a chance to alter the form.
369
  drupal_alter('views_bulk_operations_form', $form, $form_state, $vbo);
370
}
371

    
372
/**
373
 * Implements hook_views_post_build().
374
 *
375
 * Hides the VBO field if no operations are available.
376
 * This causes the entire VBO form to be hidden.
377
 *
378
 * @see views_bulk_operations_form_alter().
379
 */
380
function views_bulk_operations_views_post_build(&$view) {
381
  $vbo = _views_bulk_operations_get_field($view);
382
  if ($vbo && count($vbo->get_selected_operations()) < 1) {
383
    $vbo->options['exclude'] = TRUE;
384
  }
385
}
386

    
387
/**
388
 * Returns the 'select all' div that gets inserted below the table header row
389
 * (for table style plugins with grouping disabled), or above the view results
390
 * (for non-table style plugins), providing a choice between selecting items
391
 * on the current page, and on all pages.
392
 *
393
 * The actual insertion is done by JS, matching the degradation behavior
394
 * of Drupal core (no JS - no select all).
395
 */
396
function theme_views_bulk_operations_select_all($variables) {
397
  $view = $variables['view'];
398
  $enable_select_all_pages = $variables['enable_select_all_pages'];
399
  $form = array();
400

    
401
  if ($view->style_plugin instanceof views_plugin_style_table && empty($view->style_plugin->options['grouping'])) {
402
    if (!$enable_select_all_pages) {
403
      return '';
404
    }
405

    
406
    $wrapper_class = 'vbo-table-select-all-markup';
407
    $this_page_count = format_plural(count($view->result), '1 row', '@count rows');
408
    $this_page = t('Selected <strong>!row_count</strong> in this page.', array('!row_count' => $this_page_count));
409
    $all_pages_count = format_plural($view->total_rows, '1 row', '@count rows');
410
    $all_pages = t('Selected <strong>!row_count</strong> in this view.', array('!row_count' => $all_pages_count));
411

    
412
    $form['select_all_pages'] = array(
413
      '#type' => 'button',
414
      '#attributes' => array('class' => array('vbo-table-select-all-pages')),
415
      '#value' => t('Select all !row_count in this view.', array('!row_count' => $all_pages_count)),
416
      '#prefix' => '<span class="vbo-table-this-page">' . $this_page . ' &nbsp;',
417
      '#suffix' => '</span>',
418
    );
419
    $form['select_this_page'] = array(
420
      '#type' => 'button',
421
      '#attributes' => array('class' => array('vbo-table-select-this-page')),
422
      '#value' => t('Select only !row_count in this page.', array('!row_count' => $this_page_count)),
423
      '#prefix' => '<span class="vbo-table-all-pages" style="display: none">' . $all_pages . ' &nbsp;',
424
      '#suffix' => '</span>',
425
    );
426
  }
427
  else {
428
    $wrapper_class = 'vbo-select-all-markup';
429

    
430
    $form['select_all'] = array(
431
      '#type' => 'fieldset',
432
      '#attributes' => array('class' => array('vbo-fieldset-select-all')),
433
    );
434
    $form['select_all']['this_page'] = array(
435
      '#type' => 'checkbox',
436
      '#title' => t('Select all items on this page'),
437
      '#default_value' => '',
438
      '#attributes' => array('class' => array('vbo-select-this-page')),
439
    );
440

    
441
    if ($enable_select_all_pages) {
442
      $form['select_all']['or'] = array(
443
        '#type' => 'markup',
444
        '#markup' => '<em>' . t('OR') . '</em>',
445
      );
446
      $form['select_all']['all_pages'] = array(
447
        '#type' => 'checkbox',
448
        '#title' => t('Select all items on all pages'),
449
        '#default_value' => '',
450
        '#attributes' => array('class' => array('vbo-select-all-pages')),
451
      );
452
    }
453
  }
454

    
455
  $output = '<div class="' . $wrapper_class . '">';
456
  $output .= drupal_render($form);
457
  $output .= '</div>';
458

    
459
  return $output;
460
}
461

    
462
/**
463
 * Extend the views_form multistep form with elements for executing an operation.
464
 */
465
function views_bulk_operations_form($form, &$form_state, $vbo) {
466
  $form['#attached']['js'][] = drupal_get_path('module', 'views_bulk_operations') . '/js/views_bulk_operations.js';
467
  $form['#attached']['js'][] = array(
468
    'data' => array('vbo' => array(
469
      'row_clickable' => $vbo->get_vbo_option('row_clickable'),
470
    )),
471
    'type' => 'setting',
472
  );
473

    
474
  $form['#attached']['css'][] = drupal_get_path('module', 'views_bulk_operations') . '/css/views_bulk_operations.css';
475
  // Wrap the form in a div with specific classes for JS targeting and theming.
476
  $class = 'vbo-views-form';
477
  if (empty($vbo->view->result)) {
478
    $class .= ' vbo-views-form-empty';
479
  }
480
  $form['#prefix'] = '<div class="' . $class . '">';
481
  $form['#suffix'] = '</div>';
482

    
483
  // Force browser to reload the page if Back is hit.
484
  if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('/msie/i', $_SERVER['HTTP_USER_AGENT'])) {
485
    drupal_add_http_header('Cache-Control', 'no-cache'); // works for IE6+
486
  }
487
  else {
488
    drupal_add_http_header('Cache-Control', 'no-store'); // works for Firefox and other browsers
489
  }
490

    
491
  // Set by JS to indicate that all rows on all pages are selected.
492
  $form['select_all'] = array(
493
    '#type' => 'hidden',
494
    '#attributes' => array('class' => 'select-all-rows'),
495
    '#default_value' => FALSE,
496
  );
497
  $form['select'] = array(
498
    '#type' => 'fieldset',
499
    '#title' => t('Operations'),
500
    '#collapsible' => FALSE,
501
    '#attributes' => array('class' => array('container-inline')),
502
  );
503
  if ($vbo->get_vbo_option('display_type') == 0) {
504
    $options = array(0 => t('- Choose an operation -'));
505
    foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
506
      $options[$operation_id] = $operation->label();
507
    }
508

    
509
    // Create dropdown and submit button.
510
    $form['select']['operation'] = array(
511
      '#type' => 'select',
512
      '#options' => $options,
513
    );
514
    $form['select']['submit'] = array(
515
      '#type' => 'submit',
516
      '#value' => t('Execute'),
517
      '#validate' => array('views_bulk_operations_form_validate'),
518
      '#submit' => array('views_bulk_operations_form_submit'),
519
    );
520
  }
521
  else {
522
    // Create buttons for operations.
523
    foreach ($vbo->get_selected_operations() as $operation_id => $operation) {
524
      $form['select'][$operation_id] = array(
525
        '#type' => 'submit',
526
        '#value' => $operation->label(),
527
        '#validate' => array('views_bulk_operations_form_validate'),
528
        '#submit' => array('views_bulk_operations_form_submit'),
529
        '#operation_id' => $operation_id,
530
      );
531
    }
532
  }
533

    
534
  // Adds the "select all" functionality if the view has results.
535
  // If the view is using a table style plugin, the markup gets moved to
536
  // a table row below the header.
537
  // If we are using radio buttons, we don't use select all at all.
538
  if (!empty($vbo->view->result) && !$vbo->get_vbo_option('force_single')) {
539
    $enable_select_all_pages = FALSE;
540
    // If the view is paginated, and "select all items on all pages" is
541
    // enabled, tell that to the theme function.
542
    if (isset($vbo->view->total_rows) && count($vbo->view->result) != $vbo->view->total_rows && $vbo->get_vbo_option('enable_select_all_pages')) {
543
      $enable_select_all_pages = TRUE;
544
    }
545
    $form['select_all_markup'] = array(
546
      '#type' => 'markup',
547
      '#markup' => theme('views_bulk_operations_select_all', array('view' => $vbo->view, 'enable_select_all_pages' => $enable_select_all_pages)),
548
    );
549
  }
550

    
551
  return $form;
552
}
553

    
554
/**
555
 * Validation callback for the first step of the VBO form.
556
 */
557
function views_bulk_operations_form_validate($form, &$form_state) {
558
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
559

    
560
  if (!empty($form_state['triggering_element']['#operation_id'])) {
561
    $form_state['values']['operation'] = $form_state['triggering_element']['#operation_id'];
562
  }
563
  if (!$form_state['values']['operation']) {
564
    form_set_error('operation', t('No operation selected. Please select an operation to perform.'));
565
  }
566

    
567
  $field_name = $vbo->options['id'];
568
  $selection = _views_bulk_operations_get_selection($vbo, $form_state);
569
  if (!$selection) {
570
    form_set_error($field_name, t('Please select at least one item.'));
571
  }
572
}
573

    
574
/**
575
 * Multistep form callback for the "configure" step.
576
 */
577
function views_bulk_operations_config_form($form, &$form_state, $view, $output) {
578
  $vbo = _views_bulk_operations_get_field($view);
579
  $operation = $form_state['operation'];
580
  drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
581

    
582
  $context = array(
583
    'entity_type' => $vbo->get_entity_type(),
584
    // Pass the View along.
585
    // Has no performance penalty since objects are passed by reference,
586
    // but needing the full views object in a core action is in most cases
587
    // a sign of a wrong implementation. Do it only if you have to.
588
    'view' => $view,
589
  );
590
  $form += $operation->form($form, $form_state, $context);
591

    
592
  $query = drupal_get_query_parameters($_GET, array('q'));
593
  $form['actions'] = array(
594
    '#type' => 'container',
595
    '#attributes' => array('class' => array('form-actions')),
596
    '#weight' => 999,
597
  );
598
  $form['actions']['submit'] = array(
599
    '#type' => 'submit',
600
    '#value' => t('Next'),
601
    '#validate' => array('views_bulk_operations_config_form_validate'),
602
    '#submit' => array('views_bulk_operations_form_submit'),
603
    '#suffix' => l(t('Cancel'), $vbo->view->get_url(), array('query' => $query)),
604
  );
605

    
606
  return $form;
607
}
608

    
609
/**
610
 * Validation callback for the "configure" step.
611
 * Gives the operation a chance to validate its config form.
612
 */
613
function views_bulk_operations_config_form_validate($form, &$form_state) {
614
  $operation = &$form_state['operation'];
615
  $operation->formValidate($form, $form_state);
616
}
617

    
618
/**
619
 * Multistep form callback for the "confirm" step.
620
 */
621
function views_bulk_operations_confirm_form($form, &$form_state, $view, $output) {
622
  $vbo = _views_bulk_operations_get_field($view);
623
  $operation = $form_state['operation'];
624
  $rows = $form_state['selection'];
625
  $query = drupal_get_query_parameters($_GET, array('q'));
626
  $title = t('Are you sure you want to perform %operation on the selected items?', array('%operation' => $operation->label()));
627
  $form = confirm_form($form,
628
    $title,
629
    array('path' => $view->get_url(), 'query' => $query),
630
    theme('views_bulk_operations_confirmation', array('rows' => $rows, 'vbo' => $vbo, 'operation' => $operation, 'select_all_pages' => $form_state['select_all_pages']))
631
  );
632
  // Add VBO's submit handler to the Confirm button added by config_form().
633
  $form['actions']['submit']['#submit'] = array('views_bulk_operations_form_submit');
634

    
635
  // We can't set the View title here as $view is just a copy of the original,
636
  // and our settings changes won't "stick" for the first page load of the
637
  // confirmation form. We also can't just call drupal_set_title() directly
638
  // because our title will be clobbered by the actual View title later. So
639
  // let's tuck the title away in the form for use later.
640
  // @see views_bulk_operations_preprocess_views_view()
641
  $form['#vbo_confirm_form_title'] = $title;
642

    
643
  return $form;
644
}
645

    
646
/**
647
 * Theme function to show the confirmation page before executing the operation.
648
 */
649
function theme_views_bulk_operations_confirmation($variables) {
650
  $select_all_pages = $variables['select_all_pages'];
651
  $vbo = $variables['vbo'];
652
  $entity_type = $vbo->get_entity_type();
653
  $rows = $variables['rows'];
654
  $items = array();
655
  // Load the entities from the current page, and show their titles.
656
  $entities = _views_bulk_operations_entity_load($entity_type, array_values($rows), $vbo->revision);
657
  foreach ($entities as $entity) {
658
    $items[] = check_plain(entity_label($entity_type, $entity));
659
  }
660
  // All rows on all pages have been selected, so show a count of additional items.
661
  if ($select_all_pages) {
662
    $more_count = $vbo->view->total_rows - count($vbo->view->result);
663
    $items[] = t('...and %count more.', array('%count' => $more_count));
664
  }
665

    
666
  $count = format_plural(count($entities), 'item', '@count items');
667
  $output = theme('item_list', array('items' => $items, 'title' => t('You selected the following %count:', array('%count' => $count))));
668
  return $output;
669
}
670

    
671
/**
672
 * Implements hook_preprocess_page().
673
 *
674
 * Hide action links on the configure and confirm pages.
675
 */
676
function views_bulk_operations_preprocess_page(&$variables) {
677
  if (isset($_POST['select_all'], $_POST['operation'])) {
678
    $variables['action_links'] = array();
679
  }
680
}
681

    
682
/**
683
 * Implements hook_preprocess_views_view().
684
 */
685
function views_bulk_operations_preprocess_views_view($variables) {
686
  // If we've stored a title for the confirmation form, retrieve it here and
687
  // retitle the View.
688
  // @see views_bulk_operations_confirm_form()
689
  if (array_key_exists('rows', $variables) && is_array($variables['rows']) && array_key_exists('#vbo_confirm_form_title', $variables['rows'])) {
690
    $variables['view']->set_title($variables['rows']['#vbo_confirm_form_title']);
691
  }
692
}
693

    
694
/**
695
 * Goes through the submitted values, and returns
696
 * an array of selected rows, in the form of
697
 * $row_index => $entity_id.
698
 */
699
function _views_bulk_operations_get_selection($vbo, $form_state) {
700
  $selection = array();
701
  $field_name = $vbo->options['id'];
702

    
703
  if (!empty($form_state['values'][$field_name])) {
704
    // If using "force single", the selection needs to be converted to an array.
705
    if (is_array($form_state['values'][$field_name])) {
706
      $selection = array_filter($form_state['values'][$field_name]);
707
    }
708
    else {
709
      $selection = array($form_state['values'][$field_name]);
710
    }
711
  }
712

    
713
  return $selection;
714
}
715

    
716
/**
717
 * Submit handler for all steps of the VBO multistep form.
718
 */
719
function views_bulk_operations_form_submit($form, &$form_state) {
720
  $vbo = _views_bulk_operations_get_field($form_state['build_info']['args'][0]);
721
  $entity_type = $vbo->get_entity_type();
722

    
723
  switch ($form_state['step']) {
724
    case 'views_form_views_form':
725
      $form_state['selection'] = _views_bulk_operations_get_selection($vbo, $form_state);
726
      $form_state['select_all_pages'] = $form_state['values']['select_all'];
727

    
728
      $options = $vbo->get_operation_options($form_state['values']['operation']);
729
      $form_state['operation'] = $operation = views_bulk_operations_get_operation($form_state['values']['operation'], $entity_type, $options);
730
      if (!$operation->configurable() && $operation->getAdminOption('skip_confirmation')) {
731
        break; // Go directly to execution
732
      }
733
      $form_state['step'] = $operation->configurable() ? 'views_bulk_operations_config_form' : 'views_bulk_operations_confirm_form';
734
      $form_state['rebuild'] = TRUE;
735
      return;
736

    
737
    case 'views_bulk_operations_config_form':
738
      $form_state['step'] = 'views_bulk_operations_confirm_form';
739
      $operation = &$form_state['operation'];
740
      $operation->formSubmit($form, $form_state);
741

    
742
      if ($operation->getAdminOption('skip_confirmation')) {
743
        break; // Go directly to execution
744
      }
745
      $form_state['rebuild'] = TRUE;
746
      return;
747

    
748
    case 'views_bulk_operations_confirm_form':
749
      break;
750
  }
751

    
752
  // Execute the operation.
753
  views_bulk_operations_execute($vbo, $form_state['operation'], $form_state['selection'], $form_state['select_all_pages']);
754

    
755
  // Redirect.
756
  $query = drupal_get_query_parameters($_GET, array('q'));
757
  $form_state['redirect'] = array('path' => $vbo->view->get_url(), array('query' => $query));
758
}
759

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

    
791
  // Determine if the operation needs to be executed directly.
792
  $aggregate = $operation->aggregate();
793
  $skip_batching = $vbo->get_vbo_option('skip_batching');
794
  $save_view = $vbo->get_vbo_option('save_view_object_when_batching');
795
  $force_single = $vbo->get_vbo_option('force_single');
796
  $execute_directly = ($aggregate || $skip_batching || $force_single);
797
  // Try to load all rows without a batch if needed.
798
  if ($execute_directly && $select_all_pages) {
799
    views_bulk_operations_direct_adjust($selection, $vbo);
800
  }
801

    
802
  // Options that affect execution.
803
  $options = array(
804
    'revision' => $vbo->revision,
805
    'entity_load_capacity' => $vbo->get_vbo_option('entity_load_capacity', 10),
806
    // The information needed to recreate the view, to avoid serializing the
807
    // whole object. Passed to the executed operation. Also used by
808
    // views_bulk_operations_adjust_selection().
809
    'view_info' => array(
810
      'name' => $vbo->view->name,
811
      'display' => $vbo->view->current_display,
812
      'arguments' => $vbo->view->args,
813
      'exposed_input' => $vbo->view->get_exposed_input(),
814
    ),
815
  );
816
  // If defined, save the whole view object.
817
  if ($save_view) {
818
    $options['view_info']['view'] = $vbo->view;
819
  }
820
  // Create an array of rows in the needed format.
821
  $rows = array();
822
  $current = 1;
823
  foreach ($selection as $row_index => $entity_id) {
824
    $rows[$row_index] = array(
825
      'entity_id' => $entity_id,
826
      'views_row' => array(),
827
      // Some operations rely on knowing the position of the current item
828
      // in the execution set (because of specific things that need to be done
829
      // at the beginning or the end of the set).
830
      'position' => array(
831
        'current' => $current++,
832
        'total' => count($selection),
833
      ),
834
    );
835
    // Some operations require full selected rows.
836
    if ($operation->needsRows()) {
837
      $rows[$row_index]['views_row'] = $vbo->view->result[$row_index];
838
    }
839
  }
840

    
841
  if ($execute_directly) {
842
    // Execute the operation directly and stop here.
843
    views_bulk_operations_direct_process($operation, $rows, $options);
844
    return;
845
  }
846

    
847
  // Determine the correct queue to use.
848
  if ($operation->getAdminOption('postpone_processing')) {
849
    // Use the site queue processed on cron.
850
    $queue_name = 'views_bulk_operations';
851
  }
852
  else {
853
    // Use the active queue processed immediately by Batch API.
854
    $queue_name = 'views_bulk_operations_active_queue_' . db_next_id();
855
  }
856

    
857
  $batch = array(
858
    'operations' => array(),
859
    'finished' => 'views_bulk_operations_execute_finished',
860
    'progress_message' => '',
861
    'title' => t('Performing %operation on the selected items...', array('%operation' => $operation->label())),
862
  );
863

    
864
  // All items on all pages should be selected, add a batch job to gather
865
  // and enqueue them.
866
  if ($select_all_pages && ($vbo->view->query->pager->has_more_records() || $vbo->view->query->pager->get_current_page() > 0)) {
867
    $total_rows = $vbo->view->total_rows;
868

    
869
    $batch['operations'][] = array(
870
      'views_bulk_operations_adjust_selection', array($queue_name, $operation, $options),
871
    );
872
  }
873
  else {
874
    $total_rows = count($rows);
875

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

    
879
    // Provide a status message to the user, since this is the last step if
880
    // processing is postponed.
881
    if ($operation->getAdminOption('postpone_processing')) {
882
      drupal_set_message(t('Enqueued the selected operation (%operation).', array(
883
        '%operation' => $operation->label(),
884
      )));
885
    }
886
  }
887

    
888
  // Processing is not postponed, add a batch job to process the queue.
889
  if (!$operation->getAdminOption('postpone_processing')) {
890
    $batch['operations'][] = array(
891
      'views_bulk_operations_active_queue_process', array($queue_name, $operation, $total_rows),
892
    );
893
  }
894

    
895
  // If there are batch jobs to be processed, create the batch set.
896
  if (count($batch['operations'])) {
897
    batch_set($batch);
898
  }
899
}
900

    
901
/**
902
 * Batch API callback: loads the view page by page and enqueues all items.
903
 *
904
 * @param $queue_name
905
 *   The name of the queue to which the items should be added.
906
 * @param $operation
907
 *   The operation object.
908
 * @param $options
909
 *   An array of options that affect execution (revision, entity_load_capacity,
910
 *   view_info). Passed along with each new queue item.
911
 */
912
function views_bulk_operations_adjust_selection($queue_name, $operation, $options, &$context) {
913
  if (!isset($context['sandbox']['progress'])) {
914
    $context['sandbox']['progress'] = 0;
915
    $context['sandbox']['max'] = 0;
916
  }
917

    
918
  $view_info = $options['view_info'];
919
  if (isset($view_info['view'])) {
920
    $view = $view_info['view'];
921
    // Because of the offset, we want our view to be re-build and re-executed.
922
    $view->built = FALSE;
923
    $view->executed = FALSE;
924
  }
925
  else {
926
    $view = views_get_view($view_info['name']);
927
    $view->set_exposed_input($view_info['exposed_input']);
928
    $view->set_arguments($view_info['arguments']);
929
    $view->set_display($view_info['display']);
930
  }
931
  $view->set_offset($context['sandbox']['progress']);
932
  $view->build();
933
  $view->execute($view_info['display']);
934
  // Note the total number of rows.
935
  if (empty($context['sandbox']['max'])) {
936
    $context['sandbox']['max'] = $view->total_rows;
937
  }
938

    
939
  $vbo = _views_bulk_operations_get_field($view);
940

    
941
  // Call views_handler_field_entity::pre_render() to get the entities.
942
  $vbo->pre_render($view->result);
943

    
944
  $rows = array();
945
  foreach ($view->result as $row_index => $result) {
946
    // Set the row index.
947
    $view->row_index = $row_index;
948
    $rows[$row_index] = array(
949
      'entity_id' => $vbo->get_value($result, $vbo->real_field),
950
      'views_row' => array(),
951
      'position' => array(
952
        'current' => ++$context['sandbox']['progress'],
953
        'total' => $context['sandbox']['max'],
954
      ),
955
    );
956
    // Some operations require full selected rows.
957
    if ($operation->needsRows()) {
958
      $rows[$row_index]['views_row'] = $result;
959
    }
960
  }
961

    
962
  // Enqueue the gathered rows.
963
  views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options);
964

    
965
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
966
    // Provide an estimation of the completion level we've reached.
967
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
968
    $context['message'] = t('Prepared @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
969
  }
970
  else {
971
    // Provide a status message to the user if this is the last batch job.
972
    if ($operation->getAdminOption('postpone_processing')) {
973
      $context['results']['log'][] = t('Enqueued the selected operation (%operation).', array(
974
        '%operation' => $operation->label(),
975
      ));
976
    }
977
  }
978
}
979

    
980
/**
981
 * Divides the passed rows into groups and enqueues each group for processing
982
 *
983
 * @param $queue_name
984
 *   The name of the queue.
985
 * @param $rows
986
 *   The rows to be enqueued.
987
 * @param $operation
988
 *   The object representing the current operation.
989
 *   Passed along with each new queue item.
990
 * @param $options
991
 *   An array of options that affect execution (revision, entity_load_capacity).
992
 *   Passed along with each new queue item.
993
 */
994
function views_bulk_operations_enqueue_rows($queue_name, $rows, $operation, $options) {
995
  global $user;
996

    
997
  $queue = DrupalQueue::get($queue_name, TRUE);
998
  $row_groups = array_chunk($rows, $options['entity_load_capacity'], TRUE);
999

    
1000
  foreach ($row_groups as $row_group) {
1001
    $entity_ids = array();
1002
    foreach ($row_group as $row) {
1003
      $entity_ids[] = $row['entity_id'];
1004
    }
1005

    
1006
    $job = array(
1007
      'title' => t('Perform %operation on @type !entity_ids.', array(
1008
        '%operation' => $operation->label(),
1009
        '@type' => $operation->entityType,
1010
        '!entity_ids' => implode(',', $entity_ids),
1011
      )),
1012
      'uid' => $user->uid,
1013
      'arguments' => array($row_group, $operation, $options),
1014
    );
1015
    $queue->createItem($job);
1016
  }
1017
}
1018

    
1019
/**
1020
 * Batch API callback: processes the active queue.
1021
 *
1022
 * @param $queue_name
1023
 *   The name of the queue to process.
1024
 * @param $operation
1025
 *   The object representing the current operation.
1026
 * @param $total_rows
1027
 *   The total number of processable items (across all queue items), used
1028
 *   to report progress.
1029
 *
1030
 * @see views_bulk_operations_queue_item_process()
1031
 */
1032
function views_bulk_operations_active_queue_process($queue_name, $operation, $total_rows, &$context) {
1033
  static $queue;
1034

    
1035
  // It is still possible to hit the time limit.
1036
  drupal_set_time_limit(0);
1037

    
1038
  // Prepare the sandbox.
1039
  if (!isset($context['sandbox']['progress'])) {
1040
    $context['sandbox']['progress'] = 0;
1041
    $context['sandbox']['max'] = $total_rows;
1042
    $context['results']['log'] = array();
1043
  }
1044
  // Instantiate the queue.
1045
  if (!isset($queue)) {
1046
    $queue = DrupalQueue::get($queue_name, TRUE);
1047
  }
1048

    
1049
  // Process the queue as long as it has items for us.
1050
  $queue_item = $queue->claimItem(3600);
1051
  if ($queue_item) {
1052
    // Process the queue item, and update the progress count.
1053
    views_bulk_operations_queue_item_process($queue_item->data, $context['results']['log']);
1054
    $queue->deleteItem($queue_item);
1055

    
1056
    // Provide an estimation of the completion level we've reached.
1057
    $context['sandbox']['progress'] += count($queue_item->data['arguments'][0]);
1058
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
1059
    $context['message'] = t('Processed @current out of @total', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
1060
  }
1061

    
1062
  if (!$queue_item || $context['finished'] === 1) {
1063
    // All done. Provide a status message to the user.
1064
    $context['results']['log'][] = t('Performed %operation on @items.', array(
1065
      '%operation' => $operation->label(),
1066
      '@items' => format_plural($context['sandbox']['progress'], '1 item', '@count items'),
1067
    ));
1068
  }
1069
}
1070

    
1071
/**
1072
 * Processes the provided queue item.
1073
 *
1074
 * Used as a worker callback defined by views_bulk_operations_cron_queue_info()
1075
 * to process the site queue, as well as by
1076
 * views_bulk_operations_active_queue_process() to process the active queue.
1077
 *
1078
 * @param $queue_item_arguments
1079
 *   The arguments of the queue item to process.
1080
 * @param $log
1081
 *   An injected array of log messages, to be modified by reference.
1082
 *   If NULL, the function defaults to using watchdog.
1083
 */
1084
function views_bulk_operations_queue_item_process($queue_item_data, &$log = NULL) {
1085
  list($row_group, $operation, $options) = $queue_item_data['arguments'];
1086
  $account = user_load($queue_item_data['uid']);
1087
  $entity_type = $operation->entityType;
1088
  $entity_ids = array();
1089
  foreach ($row_group as $row_index => $row) {
1090
    $entity_ids[] = $row['entity_id'];
1091
  }
1092

    
1093
  $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1094
  foreach ($row_group as $row_index => $row) {
1095
    $entity_id = $row['entity_id'];
1096
    // A matching entity couldn't be loaded. Skip this item.
1097
    if (!isset($entities[$entity_id])) {
1098
      continue;
1099
    }
1100

    
1101
    if ($options['revision']) {
1102
      // Don't reload revisions for now, they are not statically cached and
1103
      // usually don't run into the edge case described below.
1104
      $entity = $entities[$entity_id];
1105
    }
1106
    else {
1107
      // A previous action might have resulted in the entity being resaved
1108
      // (e.g. node synchronization from a prior node in this batch), so try
1109
      // to reload it. If no change occurred, the entity will be retrieved
1110
      // from the static cache, resulting in no performance penalty.
1111
      $entity = entity_load_single($entity_type, $entity_id);
1112
      if (empty($entity)) {
1113
        // The entity is no longer valid.
1114
        continue;
1115
      }
1116
    }
1117

    
1118
    // If the current entity can't be accessed, skip it and log a notice.
1119
    $skip_permission_check = $operation->getAdminOption('skip_permission_check');
1120
    if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $account)) {
1121
      $message = 'Skipped %operation on @type %title due to insufficient permissions.';
1122
      $arguments = array(
1123
        '%operation' => $operation->label(),
1124
        '@type' => $entity_type,
1125
        '%title' => entity_label($entity_type, $entity),
1126
      );
1127

    
1128
      if ($log) {
1129
        $log[] = t($message, $arguments);
1130
      }
1131
      else {
1132
        watchdog('views bulk operations', $message, $arguments, WATCHDOG_ALERT);
1133
      }
1134

    
1135
      continue;
1136
    }
1137

    
1138
    $operation_context = array(
1139
      'progress' => $row['position'],
1140
      'view_info' => $options['view_info'],
1141
    );
1142
    if ($operation->needsRows()) {
1143
      $operation_context['rows'] = array($row_index => $row['views_row']);
1144
    }
1145
    $operation->execute($entity, $operation_context);
1146

    
1147
    unset($row_group[$row_index]);
1148
  }
1149
}
1150

    
1151
/**
1152
 * Adjusts the selection for the direct execution method.
1153
 *
1154
 * Just like the direct method itself, this is legacy code, used only for
1155
 * aggregate actions.
1156
 */
1157
function views_bulk_operations_direct_adjust(&$selection, $vbo) {
1158
  // Adjust selection to select all rows across pages.
1159
  $view = views_get_view($vbo->view->name);
1160
  $view->set_exposed_input($vbo->view->get_exposed_input());
1161
  $view->set_arguments($vbo->view->args);
1162
  $view->set_display($vbo->view->current_display);
1163
  $view->display_handler->set_option('pager', array('type' => 'none', 'options' => array()));
1164
  $view->build();
1165
  // Unset every field except the VBO one (which holds the entity id).
1166
  // That way the performance hit becomes much smaller, because there is no
1167
  // chance of views_handler_field_field::post_execute() firing entity_load().
1168
  foreach ($view->field as $field_name => $field) {
1169
    if ($field_name != $vbo->options['id']) {
1170
      unset($view->field[$field_name]);
1171
    }
1172
    else {
1173
      // Get hold of the new VBO field.
1174
      $new_vbo = $view->field[$field_name];
1175
    }
1176
  }
1177

    
1178
  $view->execute($vbo->view->current_display);
1179

    
1180
  // Call views_handler_field_entity::pre_render() to get the entities.
1181
  $new_vbo->pre_render($view->result);
1182

    
1183
  $results = array();
1184
  foreach ($view->result as $row_index => $result) {
1185
    // Set the row index.
1186
    $view->row_index = $row_index;
1187
    $results[$row_index] = $new_vbo->get_value($result, $new_vbo->real_field);
1188
  }
1189
  $selection = $results;
1190
}
1191

    
1192
/**
1193
 * Processes the passed rows directly (without batching and queueing).
1194
 */
1195
function views_bulk_operations_direct_process($operation, $rows, $options) {
1196
  global $user;
1197

    
1198
  drupal_set_time_limit(0);
1199

    
1200
  // Prepare an array of status information. Imitates the Batch API naming
1201
  // for consistency. Passed to views_bulk_operations_execute_finished().
1202
  $context = array();
1203
  $context['results']['progress'] = 0;
1204
  $context['results']['log'] = array();
1205

    
1206
  if ($operation->aggregate()) {
1207
    // Load all entities.
1208
    $entity_type = $operation->entityType;
1209
    $entity_ids = array();
1210
    foreach ($rows as $row_index => $row) {
1211
      $entity_ids[] = $row['entity_id'];
1212
    }
1213
    $entities = _views_bulk_operations_entity_load($entity_type, $entity_ids, $options['revision']);
1214

    
1215
    $skip_permission_check = $operation->getAdminOption('skip_permission_check');
1216
    // Filter out entities that can't be accessed.
1217
    foreach ($entities as $id => $entity) {
1218
      if (!$skip_permission_check && !_views_bulk_operations_entity_access($operation, $entity_type, $entity, $user)) {
1219
        $context['results']['log'][] = t('Skipped %operation on @type %title due to insufficient permissions.', array(
1220
          '%operation' => $operation->label(),
1221
          '@type' => $entity_type,
1222
          '%title' => entity_label($entity_type, $entity),
1223
        ));
1224
        unset($entities[$id]);
1225
      }
1226
    }
1227

    
1228
    // If there are any entities left, execute the operation on them.
1229
    if ($entities) {
1230
      $operation_context = array(
1231
        'view_info' => $options['view_info'],
1232
      );
1233
      // Pass the selected rows to the operation if needed.
1234
      if ($operation->needsRows()) {
1235
        $operation_context['rows'] = array();
1236
        foreach ($rows as $row_index => $row) {
1237
          $operation_context['rows'][$row_index] = $row['views_row'];
1238
        }
1239
      }
1240
      $operation->execute($entities, $operation_context);
1241
    }
1242
  }
1243
  else {
1244
    // Imitate a queue and process the entities one by one.
1245
    $queue_item_data = array(
1246
      'uid' => $user->uid,
1247
      'arguments' => array($rows, $operation, $options),
1248
    );
1249
    views_bulk_operations_queue_item_process($queue_item_data, $context['results']['log']);
1250
  }
1251

    
1252
  $context['results']['progress'] += count($rows);
1253
  $context['results']['log'][] = t('Performed %operation on @items.', array(
1254
    '%operation' => $operation->label(),
1255
    '@items' => format_plural(count($rows), '1 item', '@count items'),
1256
  ));
1257

    
1258
  views_bulk_operations_execute_finished(TRUE, $context['results'], array());
1259
}
1260

    
1261
/**
1262
 * Helper function that runs after the execution process is complete.
1263
 */
1264
function views_bulk_operations_execute_finished($success, $results, $operations) {
1265
  if ($success) {
1266
    if (count($results['log']) > 1) {
1267
      $message = theme('item_list', array('items' => $results['log']));
1268
    }
1269
    else {
1270
      $message = reset($results['log']);
1271
    }
1272
  }
1273
  else {
1274
    // An error occurred.
1275
    // $operations contains the operations that remained unprocessed.
1276
    $error_operation = reset($operations);
1277
    $message = t('An error occurred while processing @operation with arguments: @arguments',
1278
      array('@operation' => $error_operation[0], '@arguments' => print_r($error_operation[0], TRUE)));
1279
  }
1280

    
1281
  _views_bulk_operations_log($message);
1282
}
1283

    
1284
/**
1285
 * Helper function to verify access permission to operate on an entity.
1286
 */
1287
function _views_bulk_operations_entity_access($operation, $entity_type, $entity, $account = NULL) {
1288
  if (!entity_type_supports($entity_type, 'access')) {
1289
    return TRUE;
1290
  }
1291

    
1292
  $access_ops = array(
1293
    VBO_ACCESS_OP_VIEW => 'view',
1294
    VBO_ACCESS_OP_UPDATE => 'update',
1295
    VBO_ACCESS_OP_CREATE => 'create',
1296
    VBO_ACCESS_OP_DELETE => 'delete',
1297
  );
1298
  foreach ($access_ops as $bit => $op) {
1299
    if ($operation->getAccessMask() & $bit) {
1300
      if (!entity_access($op, $entity_type, $entity, $account)) {
1301
        return FALSE;
1302
      }
1303
    }
1304
  }
1305

    
1306
  return TRUE;
1307
}
1308

    
1309
/**
1310
 * Loads multiple entities by their entity or revision ids, and returns them,
1311
 * keyed by the id used for loading.
1312
 */
1313
function _views_bulk_operations_entity_load($entity_type, $ids, $revision = FALSE) {
1314
  if (!$revision) {
1315
    $entities = entity_load($entity_type, $ids);
1316
  }
1317
  else {
1318
    // D7 can't load multiple entities by revision_id. Lovely.
1319
    $info = entity_get_info($entity_type);
1320
    $entities = array();
1321
    foreach ($ids as $revision_id) {
1322
      $loaded_entities = entity_load($entity_type, array(), array($info['entity keys']['revision'] => $revision_id));
1323
      $entities[$revision_id] = reset($loaded_entities);
1324
    }
1325
  }
1326

    
1327
  return $entities;
1328
}
1329

    
1330
/**
1331
 * Helper function to report an error.
1332
 */
1333
function _views_bulk_operations_report_error($msg, $arg) {
1334
  watchdog('views bulk operations', $msg, $arg, WATCHDOG_ERROR);
1335
  if (function_exists('drush_set_error')) {
1336
    drush_set_error('VIEWS_BULK_OPERATIONS_EXECUTION_ERROR', strip_tags(dt($msg, $arg)));
1337
  }
1338
}
1339

    
1340
/**
1341
 * Display a message to the user through the relevant function.
1342
 */
1343
function _views_bulk_operations_log($msg) {
1344
  // Is VBO being run through drush?
1345
  if (function_exists('drush_log')) {
1346
    drush_log(strip_tags($msg), 'ok');
1347
  }
1348
  else {
1349
    drupal_set_message($msg);
1350
  }
1351
}