/[drupal]/drupal/includes/batch.inc
ViewVC logotype

Contents of /drupal/includes/batch.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


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

  ViewVC Help
Powered by ViewVC 1.1.2