cms/drupal/includes/batch.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * @file
       
     5  * Batch processing API for processes to run in multiple HTTP requests.
       
     6  *
       
     7  * Note that batches are usually invoked by form submissions, which is
       
     8  * why the core interaction functions of the batch processing API live in
       
     9  * form.inc.
       
    10  *
       
    11  * @see form.inc
       
    12  * @see batch_set()
       
    13  * @see batch_process()
       
    14  * @see batch_get()
       
    15  */
       
    16 
       
    17 /**
       
    18  * Loads a batch from the database.
       
    19  *
       
    20  * @param $id
       
    21  *   The ID of the batch to load. When a progressive batch is being processed,
       
    22  *   the relevant ID is found in $_REQUEST['id'].
       
    23  *
       
    24  * @return
       
    25  *   An array representing the batch, or FALSE if no batch was found.
       
    26  */
       
    27 function batch_load($id) {
       
    28   $batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
       
    29     ':bid' => $id,
       
    30     ':token' => drupal_get_token($id),
       
    31   ))->fetchField();
       
    32   if ($batch) {
       
    33     return unserialize($batch);
       
    34   }
       
    35   return FALSE;
       
    36 }
       
    37 
       
    38 /**
       
    39  * Renders the batch processing page based on the current state of the batch.
       
    40  *
       
    41  * @see _batch_shutdown()
       
    42  */
       
    43 function _batch_page() {
       
    44   $batch = &batch_get();
       
    45 
       
    46   if (!isset($_REQUEST['id'])) {
       
    47     return FALSE;
       
    48   }
       
    49 
       
    50   // Retrieve the current state of the batch.
       
    51   if (!$batch) {
       
    52     $batch = batch_load($_REQUEST['id']);
       
    53     if (!$batch) {
       
    54       drupal_set_message(t('No active batch.'), 'error');
       
    55       drupal_goto();
       
    56     }
       
    57   }
       
    58 
       
    59   // Register database update for the end of processing.
       
    60   drupal_register_shutdown_function('_batch_shutdown');
       
    61 
       
    62   // Add batch-specific CSS.
       
    63   foreach ($batch['sets'] as $batch_set) {
       
    64     if (isset($batch_set['css'])) {
       
    65       foreach ($batch_set['css'] as $css) {
       
    66         drupal_add_css($css);
       
    67       }
       
    68     }
       
    69   }
       
    70 
       
    71   $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
       
    72   $output = NULL;
       
    73   switch ($op) {
       
    74     case 'start':
       
    75       $output = _batch_start();
       
    76       break;
       
    77 
       
    78     case 'do':
       
    79       // JavaScript-based progress page callback.
       
    80       _batch_do();
       
    81       break;
       
    82 
       
    83     case 'do_nojs':
       
    84       // Non-JavaScript-based progress page.
       
    85       $output = _batch_progress_page_nojs();
       
    86       break;
       
    87 
       
    88     case 'finished':
       
    89       $output = _batch_finished();
       
    90       break;
       
    91   }
       
    92 
       
    93   return $output;
       
    94 }
       
    95 
       
    96 /**
       
    97  * Initializes the batch processing.
       
    98  *
       
    99  * JavaScript-enabled clients are identified by the 'has_js' cookie set in
       
   100  * drupal.js. If no JavaScript-enabled page has been visited during the current
       
   101  * user's browser session, the non-JavaScript version is returned.
       
   102  */
       
   103 function _batch_start() {
       
   104   if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
       
   105     return _batch_progress_page_js();
       
   106   }
       
   107   else {
       
   108     return _batch_progress_page_nojs();
       
   109   }
       
   110 }
       
   111 
       
   112 /**
       
   113  * Outputs a batch processing page with JavaScript support.
       
   114  *
       
   115  * This initializes the batch and error messages. Note that in JavaScript-based
       
   116  * processing, the batch processing page is displayed only once and updated via
       
   117  * AHAH requests, so only the first batch set gets to define the page title.
       
   118  * Titles specified by subsequent batch sets are not displayed.
       
   119  *
       
   120  * @see batch_set()
       
   121  * @see _batch_do()
       
   122  */
       
   123 function _batch_progress_page_js() {
       
   124   $batch = batch_get();
       
   125 
       
   126   $current_set = _batch_current_set();
       
   127   drupal_set_title($current_set['title'], PASS_THROUGH);
       
   128 
       
   129   // Merge required query parameters for batch processing into those provided by
       
   130   // batch_set() or hook_batch_alter().
       
   131   $batch['url_options']['query']['id'] = $batch['id'];
       
   132 
       
   133   $js_setting = array(
       
   134     'batch' => array(
       
   135       'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
       
   136       'initMessage' => $current_set['init_message'],
       
   137       'uri' => url($batch['url'], $batch['url_options']),
       
   138     ),
       
   139   );
       
   140   drupal_add_js($js_setting, 'setting');
       
   141   drupal_add_library('system', 'drupal.batch');
       
   142 
       
   143   return '<div id="progress"></div>';
       
   144 }
       
   145 
       
   146 /**
       
   147  * Does one execution pass with JavaScript and returns 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  * Outputs 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  * Processes 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   if (empty($current_set['start'])) {
       
   256     $current_set['start'] = microtime(TRUE);
       
   257   }
       
   258 
       
   259   $queue = _batch_queue($current_set);
       
   260 
       
   261   while (!$current_set['success']) {
       
   262     // If this is the first time we iterate this batch set in the current
       
   263     // request, we check if it requires an additional file for functions
       
   264     // definitions.
       
   265     if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
       
   266       include_once DRUPAL_ROOT . '/' . $current_set['file'];
       
   267     }
       
   268 
       
   269     $task_message = '';
       
   270     // Assume a single pass operation and set the completion level to 1 by
       
   271     // default.
       
   272     $finished = 1;
       
   273 
       
   274     if ($item = $queue->claimItem()) {
       
   275       list($function, $args) = $item->data;
       
   276 
       
   277       // Build the 'context' array and execute the function call.
       
   278       $batch_context = array(
       
   279         'sandbox'  => &$current_set['sandbox'],
       
   280         'results'  => &$current_set['results'],
       
   281         'finished' => &$finished,
       
   282         'message'  => &$task_message,
       
   283       );
       
   284       call_user_func_array($function, array_merge($args, array(&$batch_context)));
       
   285 
       
   286       if ($finished >= 1) {
       
   287         // Make sure this step is not counted twice when computing $current.
       
   288         $finished = 0;
       
   289         // Remove the processed operation and clear the sandbox.
       
   290         $queue->deleteItem($item);
       
   291         $current_set['count']--;
       
   292         $current_set['sandbox'] = array();
       
   293       }
       
   294     }
       
   295 
       
   296     // When all operations in the current batch set are completed, browse
       
   297     // through the remaining sets, marking them 'successfully processed'
       
   298     // along the way, until we find a set that contains operations.
       
   299     // _batch_next_set() executes form submit handlers stored in 'control'
       
   300     // sets (see form_execute_handlers()), which can in turn add new sets to
       
   301     // the batch.
       
   302     $set_changed = FALSE;
       
   303     $old_set = $current_set;
       
   304     while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
       
   305       $current_set = &_batch_current_set();
       
   306       $current_set['start'] = microtime(TRUE);
       
   307       $set_changed = TRUE;
       
   308     }
       
   309 
       
   310     // At this point, either $current_set contains operations that need to be
       
   311     // processed or all sets have been completed.
       
   312     $queue = _batch_queue($current_set);
       
   313 
       
   314     // If we are in progressive mode, break processing after 1 second.
       
   315     if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
       
   316       // Record elapsed wall clock time.
       
   317       $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
       
   318       break;
       
   319     }
       
   320   }
       
   321 
       
   322   if ($batch['progressive']) {
       
   323     // Gather progress information.
       
   324 
       
   325     // Reporting 100% progress will cause the whole batch to be considered
       
   326     // processed. If processing was paused right after moving to a new set,
       
   327     // we have to use the info from the new (unprocessed) set.
       
   328     if ($set_changed && isset($current_set['queue'])) {
       
   329       // Processing will continue with a fresh batch set.
       
   330       $remaining        = $current_set['count'];
       
   331       $total            = $current_set['total'];
       
   332       $progress_message = $current_set['init_message'];
       
   333       $task_message     = '';
       
   334     }
       
   335     else {
       
   336       // Processing will continue with the current batch set.
       
   337       $remaining        = $old_set['count'];
       
   338       $total            = $old_set['total'];
       
   339       $progress_message = $old_set['progress_message'];
       
   340     }
       
   341 
       
   342     // Total progress is the number of operations that have fully run plus the
       
   343     // completion level of the current operation.
       
   344     $current    = $total - $remaining + $finished;
       
   345     $percentage = _batch_api_percentage($total, $current);
       
   346     $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
       
   347     $values     = array(
       
   348       '@remaining'  => $remaining,
       
   349       '@total'      => $total,
       
   350       '@current'    => floor($current),
       
   351       '@percentage' => $percentage,
       
   352       '@elapsed'    => format_interval($elapsed / 1000),
       
   353       // If possible, estimate remaining processing time.
       
   354       '@estimate'   => ($current > 0) ? format_interval(($elapsed * ($total - $current) / $current) / 1000) : '-',
       
   355     );
       
   356     $message = strtr($progress_message, $values);
       
   357     if (!empty($message)) {
       
   358       $message .= '<br />';
       
   359     }
       
   360     if (!empty($task_message)) {
       
   361       $message .= $task_message;
       
   362     }
       
   363 
       
   364     return array($percentage, $message);
       
   365   }
       
   366   else {
       
   367     // If we are not in progressive mode, the entire batch has been processed.
       
   368     return _batch_finished();
       
   369   }
       
   370 }
       
   371 
       
   372 /**
       
   373  * Formats the percent completion for a batch set.
       
   374  *
       
   375  * @param $total
       
   376  *   The total number of operations.
       
   377  * @param $current
       
   378  *   The number of the current operation. This may be a floating point number
       
   379  *   rather than an integer in the case of a multi-step operation that is not
       
   380  *   yet complete; in that case, the fractional part of $current represents the
       
   381  *   fraction of the operation that has been completed.
       
   382  *
       
   383  * @return
       
   384  *   The properly formatted percentage, as a string. We output percentages
       
   385  *   using the correct number of decimal places so that we never print "100%"
       
   386  *   until we are finished, but we also never print more decimal places than
       
   387  *   are meaningful.
       
   388  *
       
   389  * @see _batch_process()
       
   390  */
       
   391 function _batch_api_percentage($total, $current) {
       
   392   if (!$total || $total == $current) {
       
   393     // If $total doesn't evaluate as true or is equal to the current set, then
       
   394     // we're finished, and we can return "100".
       
   395     $percentage = "100";
       
   396   }
       
   397   else {
       
   398     // We add a new digit at 200, 2000, etc. (since, for example, 199/200
       
   399     // would round up to 100% if we didn't).
       
   400     $decimal_places = max(0, floor(log10($total / 2.0)) - 1);
       
   401     do {
       
   402       // Calculate the percentage to the specified number of decimal places.
       
   403       $percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
       
   404       // When $current is an integer, the above calculation will always be
       
   405       // correct. However, if $current is a floating point number (in the case
       
   406       // of a multi-step batch operation that is not yet complete), $percentage
       
   407       // may be erroneously rounded up to 100%. To prevent that, we add one
       
   408       // more decimal place and try again.
       
   409       $decimal_places++;
       
   410     } while ($percentage == '100');
       
   411   }
       
   412   return $percentage;
       
   413 }
       
   414 
       
   415 /**
       
   416  * Returns the batch set being currently processed.
       
   417  */
       
   418 function &_batch_current_set() {
       
   419   $batch = &batch_get();
       
   420   return $batch['sets'][$batch['current_set']];
       
   421 }
       
   422 
       
   423 /**
       
   424  * Retrieves the next set in a batch.
       
   425  *
       
   426  * If there is a subsequent set in this batch, assign it as the new set to
       
   427  * process and execute its form submit handler (if defined), which may add
       
   428  * further sets to this batch.
       
   429  *
       
   430  * @return
       
   431  *   TRUE if a subsequent set was found in the batch.
       
   432  */
       
   433 function _batch_next_set() {
       
   434   $batch = &batch_get();
       
   435   if (isset($batch['sets'][$batch['current_set'] + 1])) {
       
   436     $batch['current_set']++;
       
   437     $current_set = &_batch_current_set();
       
   438     if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
       
   439       // We use our stored copies of $form and $form_state to account for
       
   440       // possible alterations by previous form submit handlers.
       
   441       $function($batch['form_state']['complete form'], $batch['form_state']);
       
   442     }
       
   443     return TRUE;
       
   444   }
       
   445 }
       
   446 
       
   447 /**
       
   448  * Ends the batch processing.
       
   449  *
       
   450  * Call the 'finished' callback of each batch set to allow custom handling of
       
   451  * the results and resolve page redirection.
       
   452  */
       
   453 function _batch_finished() {
       
   454   $batch = &batch_get();
       
   455 
       
   456   // Execute the 'finished' callbacks for each batch set, if defined.
       
   457   foreach ($batch['sets'] as $batch_set) {
       
   458     if (isset($batch_set['finished'])) {
       
   459       // Check if the set requires an additional file for function definitions.
       
   460       if (isset($batch_set['file']) && is_file($batch_set['file'])) {
       
   461         include_once DRUPAL_ROOT . '/' . $batch_set['file'];
       
   462       }
       
   463       if (is_callable($batch_set['finished'])) {
       
   464         $queue = _batch_queue($batch_set);
       
   465         $operations = $queue->getAllItems();
       
   466         call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
       
   467       }
       
   468     }
       
   469   }
       
   470 
       
   471   // Clean up the batch table and unset the static $batch variable.
       
   472   if ($batch['progressive']) {
       
   473     db_delete('batch')
       
   474       ->condition('bid', $batch['id'])
       
   475       ->execute();
       
   476     foreach ($batch['sets'] as $batch_set) {
       
   477       if ($queue = _batch_queue($batch_set)) {
       
   478         $queue->deleteQueue();
       
   479       }
       
   480     }
       
   481   }
       
   482   $_batch = $batch;
       
   483   $batch = NULL;
       
   484 
       
   485   // Clean-up the session. Not needed for CLI updates.
       
   486   if (isset($_SESSION)) {
       
   487     unset($_SESSION['batches'][$batch['id']]);
       
   488     if (empty($_SESSION['batches'])) {
       
   489       unset($_SESSION['batches']);
       
   490     }
       
   491   }
       
   492 
       
   493   // Redirect if needed.
       
   494   if ($_batch['progressive']) {
       
   495     // Revert the 'destination' that was saved in batch_process().
       
   496     if (isset($_batch['destination'])) {
       
   497       $_GET['destination'] = $_batch['destination'];
       
   498     }
       
   499 
       
   500     // Determine the target path to redirect to.
       
   501     if (!isset($_batch['form_state']['redirect'])) {
       
   502       if (isset($_batch['redirect'])) {
       
   503         $_batch['form_state']['redirect'] = $_batch['redirect'];
       
   504       }
       
   505       else {
       
   506         $_batch['form_state']['redirect'] = $_batch['source_url'];
       
   507       }
       
   508     }
       
   509 
       
   510     // Use drupal_redirect_form() to handle the redirection logic.
       
   511     drupal_redirect_form($_batch['form_state']);
       
   512 
       
   513     // If no redirection happened, redirect to the originating page. In case the
       
   514     // form needs to be rebuilt, save the final $form_state for
       
   515     // drupal_build_form().
       
   516     if (!empty($_batch['form_state']['rebuild'])) {
       
   517       $_SESSION['batch_form_state'] = $_batch['form_state'];
       
   518     }
       
   519     $function = $_batch['redirect_callback'];
       
   520     if (function_exists($function)) {
       
   521       $function($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
       
   522     }
       
   523   }
       
   524 }
       
   525 
       
   526 /**
       
   527  * Shutdown function: Stores the current batch data for the next request.
       
   528  *
       
   529  * @see _batch_page()
       
   530  * @see drupal_register_shutdown_function()
       
   531  */
       
   532 function _batch_shutdown() {
       
   533   if ($batch = batch_get()) {
       
   534     db_update('batch')
       
   535       ->fields(array('batch' => serialize($batch)))
       
   536       ->condition('bid', $batch['id'])
       
   537       ->execute();
       
   538   }
       
   539 }