Simpletest Coverage - includes/batch.inc

1 <?php
2 // $Id: batch.inc,v 1.36 2009/06/02 06:58:15 dries Exp $
3
4
5 /**
6 * @file
7 * Batch processing API for processes to run in multiple HTTP requests.
8 *
9 * Please note that batches are usually invoked by form submissions, which is
10 * why the core interaction functions of the batch processing API live in
11 * form.inc.
12 *
13 * @see form.inc
14 * @see batch_set()
15 * @see batch_process()
16 * @see batch_get()
17 */
18
19 /**
20 * State-based dispatcher for the batch processing page.
21 *
22 * @see _batch_shutdown()
23 */
24 function _batch_page() {
25 $batch = &batch_get();
26
27 if (!isset($_REQUEST['id'])) {
28 return FALSE;
29 }
30
31 // Retrieve the current state of batch from db.
32 $batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
33 ':bid' => $_REQUEST['id'],
34 ':token' => drupal_get_token($_REQUEST['id']))
35 )->fetchField();
36
37 if (!$batch) {
38 drupal_set_message(t('No active batch.'), 'error');
39 drupal_goto();
40 }
41
42 $batch = unserialize($batch);
43
44 // Register database update for the end of processing.
45 register_shutdown_function('_batch_shutdown');
46
47 // Add batch-specific CSS.
48 foreach ($batch['sets'] as $batch_set) {
49 foreach ($batch_set['css'] as $css) {
50 drupal_add_css($css);
51 }
52 }
53
54 $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
55 $output = NULL;
56 switch ($op) {
57 case 'start':
58 $output = _batch_start();
59 break;
60
61 case 'do':
62 // JavaScript-based progress page callback.
63 _batch_do();
64 break;
65
66 case 'do_nojs':
67 // Non-JavaScript-based progress page.
68 $output = _batch_progress_page_nojs();
69 break;
70
71 case 'finished':
72 $output = _batch_finished();
73 break;
74 }
75
76 return $output;
77 }
78
79 /**
80 * Initialize the batch processing.
81 *
82 * JavaScript-enabled clients are identified by the 'has_js' cookie set in
83 * drupal.js. If no JavaScript-enabled page has been visited during the current
84 * user's browser session, the non-JavaScript version is returned.
85 */
86 function _batch_start() {
87 if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
88 return _batch_progress_page_js();
89 }
90 else {
91 return _batch_progress_page_nojs();
92 }
93 }
94
95 /**
96 * Output a batch processing page with JavaScript support.
97 *
98 * This initializes the batch and error messages. Note that in JavaScript-based
99 * processing, the batch processing page is displayed only once and updated via
100 * AHAH requests, so only the first batch set gets to define the page title.
101 * Titles specified by subsequent batch sets are not displayed.
102 *
103 * @see batch_set()
104 * @see _batch_do()
105 */
106 function _batch_progress_page_js() {
107 $batch = batch_get();
108
109 $current_set = _batch_current_set();
110 drupal_set_title($current_set['title'], PASS_THROUGH);
111
112 $js_setting = array(
113 'batch' => array(
114 'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
115 'initMessage' => $current_set['init_message'],
116 'uri' => url($batch['url'], array('query' => array('id' => $batch['id']))),
117 ),
118 );
119 drupal_add_js($js_setting, 'setting');
120 drupal_add_js('misc/progress.js', array('cache' => FALSE));
121 drupal_add_js('misc/batch.js', array('cache' => FALSE));
122
123 return '<div id="progress"></div>';
124 }
125
126 /**
127 * Do one pass of execution in JavaScript-mode and return progress to the browser.
128 *
129 * @see _batch_progress_page_js()
130 * @see _batch_process()
131 */
132 function _batch_do() {
133 // HTTP POST required.
134 if ($_SERVER['REQUEST_METHOD'] != 'POST') {
135 drupal_set_message(t('HTTP POST is required.'), 'error');
136 drupal_set_title(t('Error'));
137 return '';
138 }
139
140 // Perform actual processing.
141 list($percentage, $message) = _batch_process();
142
143 drupal_json(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
144 }
145
146 /**
147 * Output a batch processing page without JavaScript support.
148 *
149 * @see _batch_process()
150 */
151 function _batch_progress_page_nojs() {
152 $batch = &batch_get();
153
154 $current_set = _batch_current_set();
155 drupal_set_title($current_set['title'], PASS_THROUGH);
156
157 $new_op = 'do_nojs';
158
159 if (!isset($batch['running'])) {
160 // This is the first page so we return some output immediately.
161 $percentage = 0;
162 $message = $current_set['init_message'];
163 $batch['running'] = TRUE;
164 }
165 else {
166 // This is one of the later requests; do some processing first.
167
168 // Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
169 // function), it will output whatever is in the output buffer, followed by
170 // the error message.
171 ob_start();
172 $fallback = $current_set['error_message'] . '<br />' . $batch['error_message'];
173 $fallback = theme('maintenance_page', $fallback, FALSE, FALSE);
174
175 // We strip the end of the page using a marker in the template, so any
176 // additional HTML output by PHP shows up inside the page rather than below
177 // it. While this causes invalid HTML, the same would be true if we didn't,
178 // as content is not allowed to appear after </html> anyway.
179 list($fallback) = explode('<!--partial-->', $fallback);
180 print $fallback;
181
182 // Perform actual processing.
183 list($percentage, $message) = _batch_process($batch);
184 if ($percentage == 100) {
185 $new_op = 'finished';
186 }
187
188 // PHP did not die; remove the fallback output.
189 ob_end_clean();
190 }
191
192 $url = url($batch['url'], array('query' => array('id' => $batch['id'], 'op' => $new_op)));
193 drupal_add_html_head('<meta http-equiv="Refresh" content="0; URL=' . $url . '">');
194
195 return theme('progress_bar', $percentage, $message);
196 }
197
198 /**
199 * Process sets in a batch.
200 *
201 * If the batch was marked for progressive execution (default), this executes as
202 * many operations in batch sets until an execution time of 1 second has been
203 * exceeded. It will continue with the next operation of the same batch set in
204 * the next request.
205 *
206 * @return
207 * An array containing a completion value (in percent) and a status message.
208 */
209 function _batch_process() {
210 $batch = &batch_get();
211 $current_set = &_batch_current_set();
212 // Indicate that this batch set needs to be initialized.
213 $set_changed = TRUE;
214
215 // If this batch was marked for progressive execution (e.g. forms submitted by
216 // drupal_form_submit()), initialize a timer to determine whether we need to
217 // proceed with the same batch phase when a processing time of 1 second has
218 // been exceeded.
219 if ($batch['progressive']) {
220 timer_start('batch_processing');
221 }
222
223 while (!$current_set['success']) {
224 // If this is the first time we iterate this batch set in the current
225 // request, we check if it requires an additional file for functions
226 // definitions.
227 if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
228 include_once DRUPAL_ROOT . '/' . $current_set['file'];
229 }
230
231 $task_message = '';
232 // We assume a single pass operation and set the completion level to 1 by
233 // default.
234 $finished = 1;
235 if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) {
236 // Build the 'context' array, execute the function call, and retrieve the
237 // user message.
238 $batch_context = array(
239 'sandbox' => &$current_set['sandbox'],
240 'results' => &$current_set['results'],
241 'finished' => &$finished,
242 'message' => &$task_message,
243 );
244 // Process the current operation.
245 call_user_func_array($function, array_merge($args, array(&$batch_context)));
246 }
247
248 if ($finished == 1) {
249 // Make sure this step is not counted twice when computing $current.
250 $finished = 0;
251 // Remove the processed operation and clear the sandbox.
252 array_shift($current_set['operations']);
253 $current_set['sandbox'] = array();
254 }
255
256 // When all operations in the current batch set are completed, browse
257 // through the remaining sets until we find a set that contains operations.
258 // Note that _batch_next_set() executes stored form submit handlers in
259 // remaining batch sets, which can add new sets to the batch.
260 $set_changed = FALSE;
261 $old_set = $current_set;
262 while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
263 $current_set = &_batch_current_set();
264 $set_changed = TRUE;
265 }
266 // At this point, either $current_set contains operations that need to be
267 // processed or all sets have been completed.
268
269 // If we are in progressive mode, break processing after 1 second.
270 if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
271 // Record elapsed wall clock time.
272 $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
273 break;
274 }
275 }
276
277 if ($batch['progressive']) {
278 // Gather progress information.
279
280 // Reporting 100% progress will cause the whole batch to be considered
281 // processed. If processing was paused right after moving to a new set,
282 // we have to use the info from the new (unprocessed) set.
283 if ($set_changed && isset($current_set['operations'])) {
284 // Processing will continue with a fresh batch set.
285 $remaining = count($current_set['operations']);
286 $total = $current_set['total'];
287 $progress_message = $current_set['init_message'];
288 $task_message = '';
289 }
290 else {
291 // Processing will continue with the current batch set.
292 $remaining = count($old_set['operations']);
293 $total = $old_set['total'];
294 $progress_message = $old_set['progress_message'];
295 }
296
297 $current = $total - $remaining + $finished;
298 $percentage = _batch_api_percentage($total, $current);
299
300 $elapsed = $current_set['elapsed'];
301 // Estimate remaining with percentage in floating format.
302 $estimate = $elapsed * ($total - $current) / $current;
303 $values = array(
304 '@remaining' => $remaining,
305 '@total' => $total,
306 '@current' => floor($current),
307 '@percentage' => $percentage,
308 '@elapsed' => format_interval($elapsed / 1000),
309 '@estimate' => format_interval($estimate / 1000),
310 );
311 $message = strtr($progress_message, $values);
312 if (!empty($message)) {
313 $message .= '<br />';
314 }
315 if (!empty($task_message)) {
316 $message .= $task_message;
317 }
318
319 return array($percentage, $message);
320 }
321 else {
322 // If we are not in progressive mode, the entire batch has been processed.
323 return _batch_finished();
324 }
325 }
326
327 /**
328 * Helper function for _batch_process(): returns the formatted percentage.
329 *
330 * @param $total
331 * The total number of operations.
332 * @param $current
333 * The number of the current operation.
334 * @return
335 * The properly formatted percentage, as a string. We output percentages
336 * using the correct number of decimal places so that we never print "100%"
337 * until we are finished, but we also never print more decimal places than
338 * are meaningful.
339 */
340 function _batch_api_percentage($total, $current) {
341 if (!$total || $total == $current) {
342 // If $total doesn't evaluate as true or is equal to the current set, then
343 // we're finished, and we can return "100".
344 $percentage = "100";
345 }
346 else {
347 // We add a new digit at 200, 2000, etc. (since, for example, 199/200
348 // would round up to 100% if we didn't).
349 $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
350 $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
351 }
352 return $percentage;
353 }
354
355 /**
356 * Return the batch set being currently processed.
357 */
358 function &_batch_current_set() {
359 $batch = &batch_get();
360 return $batch['sets'][$batch['current_set']];
361 }
362
363 /**
364 * Retrieve the next set in a batch.
365 *
366 * If there is a subsequent set in this batch, assign it as the new set to
367 * process and execute its form submit handler (if defined), which may add
368 * further sets to this batch.
369 *
370 * @return
371 * TRUE if a subsequent set was found in the batch.
372 */
373 function _batch_next_set() {
374 $batch = &batch_get();
375 if (isset($batch['sets'][$batch['current_set'] + 1])) {
376 $batch['current_set']++;
377 $current_set = &_batch_current_set();
378 if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
379 // We use our stored copies of $form and $form_state to account for
380 // possible alterations by previous form submit handlers.
381 $function($batch['form'], $batch['form_state']);
382 }
383 return TRUE;
384 }
385 }
386
387 /**
388 * End the batch processing.
389 *
390 * Call the 'finished' callback of each batch set to allow custom handling of
391 * the results and resolve page redirection.
392 */
393 function _batch_finished() {
394 $batch = &batch_get();
395
396 // Execute the 'finished' callbacks for each batch set, if defined.
397 foreach ($batch['sets'] as $key => $batch_set) {
398 if (isset($batch_set['finished'])) {
399 // Check if the set requires an additional file for function definitions.
400 if (isset($batch_set['file']) && is_file($batch_set['file'])) {
401 include_once DRUPAL_ROOT . '/' . $batch_set['file'];
402 }
403 if (function_exists($batch_set['finished'])) {
404 // Format the elapsed time when batch complete.
405 $batch_set['finished']($batch_set['success'], $batch_set['results'], $batch_set['operations'], format_interval($batch_set['elapsed'] / 1000));
406 }
407 }
408 }
409
410 // Clean up the batch table and unset the static $batch variable.
411 if ($batch['progressive']) {
412 db_delete('batch')
413 ->condition('bid', $batch['id'])
414 ->execute();
415 }
416 $_batch = $batch;
417 $batch = NULL;
418
419 // Clean-up the session.
420 unset($_SESSION['batches'][$batch['id']]);
421 if (empty($_SESSION['batches'])) {
422 unset($_SESSION['batches']);
423 }
424
425 // Redirect if needed.
426 if ($_batch['progressive']) {
427 // Revert the 'destination' that was saved in batch_process().
428 if (isset($_batch['destination'])) {
429 $_REQUEST['destination'] = $_batch['destination'];
430 }
431
432 // Determine the target path to redirect to.
433 if (isset($_batch['form_state']['redirect'])) {
434 $redirect = $_batch['form_state']['redirect'];
435 }
436 elseif (isset($_batch['redirect'])) {
437 $redirect = $_batch['redirect'];
438 }
439 else {
440 $redirect = $_batch['source_page'];
441 }
442
443 // Use drupal_redirect_form() to handle the redirection logic.
444 $form = isset($batch['form']) ? $batch['form'] : array();
445 if (empty($_batch['form_state']['rebuild']) && empty($_batch['form_state']['storage'])) {
446 drupal_redirect_form($form, $redirect);
447 }
448
449 // We get here if $form['#redirect'] was FALSE, or if the form is a
450 // multi-step form. We save the final $form_state value to be retrieved
451 // by drupal_get_form(), and redirect to the originating page.
452 $_SESSION['batch_form_state'] = $_batch['form_state'];
453 drupal_goto($_batch['source_page']);
454 }
455 }
456
457 /**
458 * Shutdown function; store the current batch data for the next request.
459 */
460 function _batch_shutdown() {
461 if ($batch = batch_get()) {
462 db_update('batch')
463 ->fields(array('batch' => serialize($batch)))
464 ->condition('bid', $batch['id'])
465 ->execute();
466 }
467 }
468
469

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.