summaryrefslogtreecommitdiff
path: root/includes/batch.inc
blob: 42d6159263c3f9ebee3cd74385a4d1b96f85e796 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
<?php
// $Id$


/**
 * @file
 * Batch processing API for processes to run in multiple HTTP requests.
 *
 * Please note that batches are usually invoked by form submissions, which is
 * why the core interaction functions of the batch processing API live in
 * form.inc.
 *
 * @see form.inc
 * @see batch_set()
 * @see batch_process()
 * @see batch_get()
 */

/**
 * State-based dispatcher for the batch processing page.
 *
 * @see _batch_shutdown()
 */
function _batch_page() {
  $batch = &batch_get();

  if (!isset($_REQUEST['id'])) {
    return FALSE;
  }

  // Retrieve the current state of batch from db.
  $batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
    ':bid' => $_REQUEST['id'],
    ':token' => drupal_get_token($_REQUEST['id']))
  )->fetchField();

  if (!$batch) {
    drupal_set_message(t('No active batch.'), 'error');
    drupal_goto();
  }

  $batch = unserialize($batch);

  // Register database update for the end of processing.
  register_shutdown_function('_batch_shutdown');

  // Add batch-specific CSS.
  foreach ($batch['sets'] as $batch_set) {
    foreach ($batch_set['css'] as $css) {
      drupal_add_css($css);
    }
  }

  $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
  $output = NULL;
  switch ($op) {
    case 'start':
      $output = _batch_start();
      break;

    case 'do':
      // JavaScript-based progress page callback.
      _batch_do();
      break;

    case 'do_nojs':
      // Non-JavaScript-based progress page.
      $output = _batch_progress_page_nojs();
      break;

    case 'finished':
      $output = _batch_finished();
      break;
  }

  return $output;
}

/**
 * Initialize the batch processing.
 *
 * JavaScript-enabled clients are identified by the 'has_js' cookie set in
 * drupal.js. If no JavaScript-enabled page has been visited during the current
 * user's browser session, the non-JavaScript version is returned.
 */
function _batch_start() {
  if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
    return _batch_progress_page_js();
  }
  else {
    return _batch_progress_page_nojs();
  }
}

/**
 * Output a batch processing page with JavaScript support.
 *
 * This initializes the batch and error messages. Note that in JavaScript-based
 * processing, the batch processing page is displayed only once and updated via
 * AHAH requests, so only the first batch set gets to define the page title.
 * Titles specified by subsequent batch sets are not displayed.
 *
 * @see batch_set()
 * @see _batch_do()
 */
function _batch_progress_page_js() {
  $batch = batch_get();

  $current_set = _batch_current_set();
  drupal_set_title($current_set['title'], PASS_THROUGH);

  $js_setting = array(
    'batch' => array(
      'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
      'initMessage' => $current_set['init_message'],
      'uri' => url($batch['url'], array('query' => array('id' => $batch['id']))),
    ),
  );
  drupal_add_js($js_setting, 'setting');
  drupal_add_js('misc/progress.js', array('cache' => FALSE));
  drupal_add_js('misc/batch.js', array('cache' => FALSE));

  return '<div id="progress"></div>';
}

/**
 * Do one pass of execution in JavaScript-mode and return progress to the browser.
 *
 * @see _batch_progress_page_js()
 * @see _batch_process()
 */
function _batch_do() {
  // HTTP POST required.
  if ($_SERVER['REQUEST_METHOD'] != 'POST') {
    drupal_set_message(t('HTTP POST is required.'), 'error');
    drupal_set_title(t('Error'));
    return '';
  }

  // Perform actual processing.
  list($percentage, $message) = _batch_process();

  drupal_json(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
}

/**
 * Output a batch processing page without JavaScript support.
 *
 * @see _batch_process()
 */
function _batch_progress_page_nojs() {
  $batch = &batch_get();

  $current_set = _batch_current_set();
  drupal_set_title($current_set['title'], PASS_THROUGH);

  $new_op = 'do_nojs';

  if (!isset($batch['running'])) {
    // This is the first page so we return some output immediately.
    $percentage       = 0;
    $message          = $current_set['init_message'];
    $batch['running'] = TRUE;
  }
  else {
    // This is one of the later requests; do some processing first.

    // Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
    // function), it will output whatever is in the output buffer, followed by
    // the error message.
    ob_start();
    $fallback = $current_set['error_message'] . '<br />' . $batch['error_message'];
    $fallback = theme('maintenance_page', $fallback, FALSE, FALSE);

    // We strip the end of the page using a marker in the template, so any
    // additional HTML output by PHP shows up inside the page rather than below
    // it. While this causes invalid HTML, the same would be true if we didn't,
    // as content is not allowed to appear after </html> anyway.
    list($fallback) = explode('<!--partial-->', $fallback);
    print $fallback;

    // Perform actual processing.
    list($percentage, $message) = _batch_process($batch);
    if ($percentage == 100) {
      $new_op = 'finished';
    }

    // PHP did not die; remove the fallback output.
    ob_end_clean();
  }

  $url = url($batch['url'], array('query' => array('id' => $batch['id'], 'op' => $new_op)));
  drupal_add_html_head('<meta http-equiv="Refresh" content="0; URL=' . $url . '">');

  return theme('progress_bar', $percentage, $message);
}

/**
 * Process sets in a batch.
 *
 * If the batch was marked for progressive execution (default), this executes as
 * many operations in batch sets until an execution time of 1 second has been
 * exceeded. It will continue with the next operation of the same batch set in
 * the next request.
 *
 * @return
 *   An array containing a completion value (in percent) and a status message.
 */
function _batch_process() {
  $batch       = &batch_get();
  $current_set = &_batch_current_set();
  // Indicate that this batch set needs to be initialized.
  $set_changed = TRUE;

  // If this batch was marked for progressive execution (e.g. forms submitted by
  // drupal_form_submit()), initialize a timer to determine whether we need to
  // proceed with the same batch phase when a processing time of 1 second has
  // been exceeded.
  if ($batch['progressive']) {
    timer_start('batch_processing');
  }

  while (!$current_set['success']) {
    // If this is the first time we iterate this batch set in the current
    // request, we check if it requires an additional file for functions
    // definitions.
    if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
      include_once DRUPAL_ROOT . '/' . $current_set['file'];
    }

    $task_message = '';
    // We assume a single pass operation and set the completion level to 1 by
    // default.
    $finished = 1;
    if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) {
      // Build the 'context' array, execute the function call, and retrieve the
      // user message.
      $batch_context = array(
        'sandbox'  => &$current_set['sandbox'],
        'results'  => &$current_set['results'],
        'finished' => &$finished,
        'message'  => &$task_message,
      );
      // Process the current operation.
      call_user_func_array($function, array_merge($args, array(&$batch_context)));
    }

    if ($finished == 1) {
      // Make sure this step is not counted twice when computing $current.
      $finished = 0;
      // Remove the processed operation and clear the sandbox.
      array_shift($current_set['operations']);
      $current_set['sandbox'] = array();
    }

    // When all operations in the current batch set are completed, browse
    // through the remaining sets until we find a set that contains operations.
    // Note that _batch_next_set() executes stored form submit handlers in
    // remaining batch sets, which can add new sets to the batch.
    $set_changed = FALSE;
    $old_set = $current_set;
    while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
      $current_set = &_batch_current_set();
      $set_changed = TRUE;
    }
    // At this point, either $current_set contains operations that need to be
    // processed or all sets have been completed.

    // If we are in progressive mode, break processing after 1 second.
    if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
      // Record elapsed wall clock time.
      $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
      break;
    }
  }

  if ($batch['progressive']) {
    // Gather progress information.

    // Reporting 100% progress will cause the whole batch to be considered
    // processed. If processing was paused right after moving to a new set,
    // we have to use the info from the new (unprocessed) set.
    if ($set_changed && isset($current_set['operations'])) {
      // Processing will continue with a fresh batch set.
      $remaining        = count($current_set['operations']);
      $total            = $current_set['total'];
      $progress_message = $current_set['init_message'];
      $task_message     = '';
    }
    else {
      // Processing will continue with the current batch set.
      $remaining        = count($old_set['operations']);
      $total            = $old_set['total'];
      $progress_message = $old_set['progress_message'];
    }

    $current = $total - $remaining + $finished;
    $percentage = _batch_api_percentage($total, $current);

    $elapsed    = $current_set['elapsed'];
    // Estimate remaining with percentage in floating format.
    $estimate   = $elapsed * ($total - $current) / $current;
    $values     = array(
      '@remaining'  => $remaining,
      '@total'      => $total,
      '@current'    => floor($current),
      '@percentage' => $percentage,
      '@elapsed'    => format_interval($elapsed / 1000),
      '@estimate'   => format_interval($estimate / 1000),
    );
    $message = strtr($progress_message, $values);
    if (!empty($message)) {
      $message .= '<br />';
    }
    if (!empty($task_message)) {
      $message .= $task_message;
    }

    return array($percentage, $message);
  }
  else {
    // If we are not in progressive mode, the entire batch has been processed.
    return _batch_finished();
  }
}

/**
 * Helper function for _batch_process(): returns the formatted percentage.
 *
 * @param $total
 *   The total number of operations.
 * @param $current
 *   The number of the current operation.
 * @return
 *   The properly formatted percentage, as a string. We output percentages
 *   using the correct number of decimal places so that we never print "100%"
 *   until we are finished, but we also never print more decimal places than
 *   are meaningful.
 */
function _batch_api_percentage($total, $current) {
  if (!$total || $total == $current) {
    // If $total doesn't evaluate as true or is equal to the current set, then
    // we're finished, and we can return "100".
    $percentage = "100";
  }
  else {
    // We add a new digit at 200, 2000, etc. (since, for example, 199/200
    // would round up to 100% if we didn't).
    $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
    $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
  }
  return $percentage;
}

/**
 * Return the batch set being currently processed.
 */
function &_batch_current_set() {
  $batch = &batch_get();
  return $batch['sets'][$batch['current_set']];
}

/**
 * Retrieve the next set in a batch.
 *
 * If there is a subsequent set in this batch, assign it as the new set to
 * process and execute its form submit handler (if defined), which may add
 * further sets to this batch.
 *
 * @return
 *   TRUE if a subsequent set was found in the batch.
 */
function _batch_next_set() {
  $batch = &batch_get();
  if (isset($batch['sets'][$batch['current_set'] + 1])) {
    $batch['current_set']++;
    $current_set = &_batch_current_set();
    if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
      // We use our stored copies of $form and $form_state to account for
      // possible alterations by previous form submit handlers.
      $function($batch['form'], $batch['form_state']);
    }
    return TRUE;
  }
}

/**
 * End the batch processing.
 *
 * Call the 'finished' callback of each batch set to allow custom handling of
 * the results and resolve page redirection.
 */
function _batch_finished() {
  $batch = &batch_get();

  // Execute the 'finished' callbacks for each batch set, if defined.
  foreach ($batch['sets'] as $key => $batch_set) {
    if (isset($batch_set['finished'])) {
      // Check if the set requires an additional file for function definitions.
      if (isset($batch_set['file']) && is_file($batch_set['file'])) {
        include_once DRUPAL_ROOT . '/' . $batch_set['file'];
      }
      if (function_exists($batch_set['finished'])) {
        // Format the elapsed time when batch complete.
        $batch_set['finished']($batch_set['success'], $batch_set['results'], $batch_set['operations'], format_interval($batch_set['elapsed'] / 1000));
      }
    }
  }

  // Clean up the batch table and unset the static $batch variable.
  if ($batch['progressive']) {
    db_delete('batch')
      ->condition('bid', $batch['id'])
      ->execute();
  }
  $_batch = $batch;
  $batch = NULL;

  // Clean-up the session.
  unset($_SESSION['batches'][$batch['id']]);
  if (empty($_SESSION['batches'])) {
    unset($_SESSION['batches']);
  }

  // Redirect if needed.
  if ($_batch['progressive']) {
    // Revert the 'destination' that was saved in batch_process().
    if (isset($_batch['destination'])) {
      $_REQUEST['destination'] = $_batch['destination'];
    }

    // Determine the target path to redirect to.
    if (isset($_batch['form_state']['redirect'])) {
      $redirect = $_batch['form_state']['redirect'];
    }
    elseif (isset($_batch['redirect'])) {
      $redirect = $_batch['redirect'];
    }
    else {
      $redirect = $_batch['source_page'];
    }

    // Use drupal_redirect_form() to handle the redirection logic.
    $form = isset($batch['form']) ? $batch['form'] : array();
    if (empty($_batch['form_state']['rebuild']) && empty($_batch['form_state']['storage'])) {
      drupal_redirect_form($form, $redirect);
    }

    // We get here if $form['#redirect'] was FALSE, or if the form is a
    // multi-step form. We save the final $form_state value to be retrieved
    // by drupal_get_form(), and redirect to the originating page.
    $_SESSION['batch_form_state'] = $_batch['form_state'];
    drupal_goto($_batch['source_page']);
  }
}

/**
 * Shutdown function; store the current batch data for the next request.
 */
function _batch_shutdown() {
  if ($batch = batch_get()) {
    db_update('batch')
      ->fields(array('batch' => serialize($batch)))
      ->condition('bid', $batch['id'])
      ->execute();
  }
}