diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e3718cb477d458b571c551b8005e482f47f30971..c2815d1ef5a11b6c5ae62f06a1ab7e062d9f26c3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -28,6 +28,7 @@ Drupal 6.0, xxxx-xx-xx (development version) * Added .info files to themes and made it easier to specify regions and features. * Added theme registry: modules can directly provide .tpl.php files for their themes without having to create theme_ functions. * Used the Garland theme for the installation and maintenance pages. +- Refactored update.php to a generic batch API to be able to run time consuming operations in multiple subsequent HTTP requests Drupal 5.0, 2007-01-15 ---------------------- diff --git a/includes/batch.inc b/includes/batch.inc new file mode 100644 index 0000000000000000000000000000000000000000..60c0e370b6aae255436a059c413c4c004009cf9a --- /dev/null +++ b/includes/batch.inc @@ -0,0 +1,297 @@ +<?php + +/** + * @file Batch processing API for processes to run in multiple HTTP requests. + */ + +/** + * State based dispatcher for batches. + */ +function _batch_page() { + global $user; + + $batch =& batch_get(); + + if (isset($_REQUEST['id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d AND sid = %d", $_REQUEST['id'], $user->sid))) { + $batch = unserialize($data); + } + else { + return FALSE; + } + + // Register database update for end of processing. + register_shutdown_function('_batch_shutdown'); + + $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; + switch ($op) { + case 'start': + $output = _batch_start(); + break; + + case 'do': + $output = _batch_do(); + break; + + case 'do_nojs': + $output = _batch_progress_page_nojs(); + break; + + case 'finished': + $output = _batch_finished(); + break; + } + + return $output; +} + +/** + * Initiate the batch processing + */ +function _batch_start() { + // Choose between the JS and non-JS version. + // JS-enabled users are identified through the 'has_js' cookie set in drupal.js. + // If the user did not visit any JS enabled page during his browser session, + // he gets the non-JS version... + if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) { + return _batch_progress_page_js(); + } + else { + return _batch_progress_page_nojs(); + } +} + +/** + * Batch processing page with JavaScript support. + */ +function _batch_progress_page_js() { + $batch = batch_get(); + $current_set = _batch_current_set(); + + drupal_set_title($current_set['title']); + drupal_add_js('misc/progress.js', 'core', 'header'); + + $url = url($batch['url'], array('query' => array('id' => $batch['id']))); + $js_setting = array( + 'batch' => array( + 'errorMessage' => $current_set['error_message'] .'<br/>'. $batch['error_message'], + 'initMessage' => $current_set['init_message'], + 'uri' => $url, + ), + ); + drupal_add_js($js_setting, 'setting'); + drupal_add_js('misc/batch.js', 'core', 'header', FALSE, TRUE); + + $output = '<div id="progress"></div>'; + return $output; +} + +/** + * Do one pass of execution and inform back the browser about progression. + */ +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 ''; + } + + list($percentage, $message) = _batch_process(); + + drupal_set_header('Content-Type: text/plain; charset=utf-8'); + print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message)); + exit(); +} + +/** + * Batch processing page without JavaScript support. + */ +function _batch_progress_page_nojs() { + $batch =& batch_get(); + $current_set = _batch_current_set(); + + drupal_set_title($current_set['title']); + + $new_op = 'do_nojs'; + + if (!isset($batch['running'])) { + // This is the first page so 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. non-existant + // 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); + + // 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; + + list($percentage, $message) = _batch_process($batch); + if ($percentage == 100) { + $new_op = 'finished'; + } + + // Processing successful; remove fallback. + ob_end_clean(); + } + + $url = url($batch['url'], array('query' => array('id' => $batch['id'], 'op' => $new_op))); + drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL='. $url .'">'); + $output = theme('progress_bar', $percentage, $message); + return $output; +} + +/** + * Advance batch processing for 1 second (or process the whole batch if it + * was not set for progressive execution). + */ +function _batch_process() { + $batch =& batch_get(); + $current_set =& _batch_current_set(); + + while (!$current_set['success']) { + $task_message = NULL; + $finished = 1; + if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) { + // Build the 'batch 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' => ''); + call_user_func_array($function, array_merge($args, array(&$batch_context))); + $task_message = $batch_context['message']; + } + if ($finished == 1) { + // Make sure this step isn't counted double. + $finished = 0; + // Remove the operation, and clear the sandbox to reduce the stored data. + array_shift($current_set['operations']); + $current_set['sandbox'] = array(); + + // If the batch set is completed, browse through the remaining sets + // until we find one that acually has operations. + while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) { + $current_set =& _batch_current_set(); + } + } + // Progressive mode : stop after 1 second + if ($batch['progressive'] && timer_read('page') > 1000) { + break; + } + } + + if ($batch['progressive']) { + $remaining = count($current_set['operations']); + $total = $current_set['total']; + $current = $total - $remaining + $finished; + $percentage = $total ? floor($current / $total * 100) : 100; + $values = array( + '@remaining' => $remaining, + '@total' => $total, + '@current' => floor($current), + '@percentage' => $percentage, + ); + $progress_message = strtr($current_set['progress_message'], $values); + + $message = $progress_message .'<br/>'; + $message.= $task_message ? $task_message : ' '; + + return array($percentage, $message); + } + else { + return _batch_finished(); + } + +} + +/** + * Retrieve the batch set being currently processed. + */ +function &_batch_current_set() { + $batch =& batch_get(); + return $batch['sets'][$batch['current_set']]; +} + +/** + * Move execution to the next batch set if any, executing the stored + * form _submit callbacks along the way (possibly inserting additional batch sets) + */ +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']) && (list($function, $args) = $current_set['form submit']) && function_exists($function)) { + // We have to keep our own copy of $form_values, to account + // for possible alteration by the submit callback. + if (isset($batch['form_values'])) { + $args[1] = $batch['form_values']; + } + $redirect = call_user_func_array($function, $args); + // Store the form_values only if needed, to limit the + // amount of data we store in the batch. + if (isset($batch['sets'][$batch['current_set']+1])) { + $batch['form_values'] = $args[1]; + } + if (isset($redirect)) { + $batch['redirect'] = $redirect; + } + } + return TRUE; + } +} + +/** + * End the batch : + * Call the 'finished' callbacks to allow custom handling of results, + * and resolve page redirection. + */ +function _batch_finished() { + $batch =& batch_get(); + + // Execute the 'finished' callbacks. + foreach($batch['sets'] as $key => $batch_set) { + if (isset($batch_set['finished']) && function_exists($batch_set['finished'])) { + $batch_set['finished']($batch_set['success'], $batch_set['results'], $batch_set['operations']); + } + } + + // Cleanup the batch table and unset the global $batch variable. + db_query("DELETE FROM {batch} WHERE bid = %d", $batch['id']); + $_batch = $batch; + $batch = NULL; + + // Redirect if needed. + if ($_batch['progressive']) { + if (isset($_batch['destination'])) { + $_REQUEST['destination'] = $_batch['destination']; + } + $redirect = isset($_batch['redirect']) ? $_batch['redirect'] : $_batch['source_page']; + $form_redirect = isset($_batch['form_redirect']) ? $_batch['form_redirect'] : NULL; + // Let drupal_redirect_form handle redirection logic, using a bare pseudo form + // to limit the amount of data we store in the batch. + drupal_redirect_form(array('#redirect' => $form_redirect), $redirect); + + // If we get here, $form['redirect']['#redirect'] was FALSE, and we are most + // probably dealing with a multistep form - not supported at the moment. + // Redirect to the originating page - first step of the form. + drupal_goto($_batch['source_page']); + } +} + +/** + * Store tha batch data for next request, or clear the table if the batch is finished. + */ +function _batch_shutdown() { + if ($batch = batch_get()) { + db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($batch), $batch['id']); + } +} diff --git a/includes/common.inc b/includes/common.inc index 354eb4f7f2ae0fd79c4875c43855bd59cfa244b4..ec2f064325099b57a484cb82bf3ea5b9a4aa7423 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2327,11 +2327,11 @@ function drupal_common_themes() { 'arguments' => array('text' => NULL) ), 'page' => array( - 'arguments' => array('content' => NULL, 'show_blocks' => TRUE), + 'arguments' => array('content' => NULL, 'show_blocks' => TRUE, 'show_messages' => TRUE), 'file' => 'page', ), 'maintenance_page' => array( - 'arguments' => array('content' => NULL, 'messages' => TRUE), + 'arguments' => array('content' => NULL, 'show_messages' => TRUE), ), 'install_page' => array( 'arguments' => array('content' => NULL), diff --git a/includes/form.inc b/includes/form.inc index dc843d2868c57e42efda7e00d56ca55363bcf227..5de5ded31e1908bb4ed02a13d98724ad6dd526db 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -255,6 +255,12 @@ function drupal_process_form($form_id, &$form) { // In that case we accept a submission without button values. if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) { $redirect = drupal_submit_form($form_id, $form); + if ($batch =& batch_get()) { + $batch['progressive'] = !$form['#programmed']; + batch_process(); + // Progressive batch processing redirects to the progress page. + // Execution continues only if programmed form. + } if (!$form['#programmed']) { drupal_redirect_form($form, $redirect); } @@ -420,7 +426,19 @@ function drupal_submit_form($form_id, $form) { $args = array_merge($default_args, (array) $args); // Since we can only redirect to one page, only the last redirect // will work. - $redirect = call_user_func_array($function, $args); + if ($batch =& batch_get()) { + // Some previous _submit callback has set a batch. + // We store the call in a special 'control' batch set for execution + // at the correct time during the batch processing workflow. + $batch['sets'][] = array('form submit' => array($function, $args)); + } + else { + $redirect = call_user_func_array($function, $args); + if ($batch =& batch_get()) { + // The _submit callback has opened a batch: store the needed form info. + $batch['form_redirect'] = isset($form['#redirect']) ? $form['#redirect'] : NULL; + } + } $submitted = TRUE; if (isset($redirect)) { $goto = $redirect; @@ -1491,14 +1509,14 @@ function theme_markup($element) { } /** -* Format a password field. -* -* @param $element -* An associative array containing the properties of the element. -* Properties used: title, value, description, size, maxlength, required, attributes -* @return -* A themed HTML string representing the form. -*/ + * Format a password field. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, description, size, maxlength, required, attributes + * @return + * A themed HTML string representing the form. + */ function theme_password($element) { $size = $element['#size'] ? ' size="'. $element['#size'] .'" ' : ''; $maxlength = $element['#maxlength'] ? ' maxlength="'. $element['#maxlength'] .'" ' : ''; @@ -1625,3 +1643,252 @@ function form_clean_id($id = NULL) { /** * @} End of "defgroup form". */ + +/** + * @defgroup batch Batch operations + * @{ + * Functions allowing forms processing to be spread out over several page + * requests, thus ensuring that the processing does not get interrupted + * because of a PHP timeout, while allowing the user to receive feedback + * on the progress of the ongoing operations. + * + * The API is primarily designed to integrate nicely with the Form API + * workflow, but can also be used by non-FAPI scripts (like update.php) + * or even simple page callbacks (which should probably be used sparingly). + * + * Example: + * @code + * $batch = array( + * 'title' => t('Exporting'), + * 'operations' => array( + * array('my_function_1', array($account->uid, 'story')), + * array('my_function_2', array()), + * ), + * 'finished' => 'my_finished_callback', + * ); + * batch_set($batch); + * // only needed if not inside a form _submit callback : + * batch_process(); + * @endcode + * + * Sample batch operations: + * @code + * // Simple and artificial: load a node of a given type for a given user + * function my_function_1($uid, $type, &$context) { + * // The $context array gathers batch context information about the execution (read), + * // as well as 'return values' for the current operation (write) + * // The following keys are provided : + * // 'results' (read / write): The array of results gathered so far by + * // the batch processing, for the curent operation to append its own. + * // 'message' (write): A text message displayed in the progress page. + * // The following keys allow for multi-step operations : + * // 'sandbox' (read / write): An array that can be freely used to + * // store persistent data between iterations. It is recommended to + * // use this instead of $_SESSION, which is unsafe if the user + * // continues browsing in a separate window while the batch is processing. + * // 'finished' (write): A float number between 0 and 1 informing + * // the processing engine of the completion level for the operation. + * // 1 means the operation is finished and processing can continue + * // to the next operation. This value always be 1 if not specified + * // by the batch operation (a single-step operation), so that will + * // be considered as finished. + * $node = node_load(array('uid' => $uid, 'type' => $type)); + * $context['results'][] = $node->nid .' : '. $node->title; + * $context['message'] = $node->title; + * } + * + * // More advanced example: mutli-step operation - load all nodes, five by five + * function my_function_2(&$context) { + * if (empty($context['sandbox'])) { + * $context['sandbox']['progress'] = 0; + * $context['sandbox']['current_node'] = 0; + * $context['sandbox']['max'] = db_result(db_query('SELECT COUNT(DISTINCT nid) FROM {node}')); + * } + * $limit = 5; + * $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit); + * while ($row = db_fetch_array($result)) { + * $node = node_load($row['nid'], NULL, TRUE); + * $context['results'][] = $node->nid .' : '. $node->title; + * $context['sandbox']['progress']++; + * $context['sandbox']['current_node'] = $node->nid; + * $context['message'] = $node->title; + * } + * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + * } + * } + * @endcode + * + * Sample 'finished' callback: + * @code + * function batch_test_finished($success, $results, $operations) { + * if ($success) { + * $message = format_plural(count($results), 'One node processed.', '@count nodes processed.'); + * } + * else { + * $message = t('Finished with an error.'); + * } + * drupal_set_message($message); + * // Provinding data for the redirected page is done through $_SESSION. + * foreach ($results as $result) { + * $items[] = t('Loaded node %title.', array('%title' => $result)); + * } + * $_SESSION['my_batch_results'] = $items; + * } + * @endcode + */ + +/** + * Open a new batch. + * + * @param $batch + * An array defining the batch. The following keys can be used: + * 'operations': an array of function calls to be performed. + * Example: + * @code + * array( + * array('my_function_1', array($arg1)), + * array('my_function_2', array($arg2_1, $arg2_2)), + * ) + * @endcode + * All the other values below are optional. + * batch_init() provides default values for the messages. + * 'title': title for the progress page. + * Defaults to t('Processing'). + * 'init_message': message displayed while the processing is initialized. + * Defaults to t('Initializing.'). + * 'progress_message': message displayed while processing the batch. + * Available placeholders are @current, @remaining, @total and @percent. + * Defaults to t('Remaining @remaining of @total.'). + * 'error_message': message displayed if an error occurred while processing the batch. + * Defaults to t('An error has occurred.'). + * 'finished': the name of a function to be executed after the batch has completed. + * This should be used to perform any result massaging that may be needed, + * and possibly save data in $_SESSION for display after final page redirection. + * + * Operations are added as new batch sets. Batch sets are used to ensure + * clean code independency, ensuring that several batches submitted by + * different parts of the code (core / contrib modules) can be processed + * correctly while not interfering or having to cope with each other. Each + * batch set gets to specify his own UI messages, operates on it's own set + * of operations and results, and triggers it's own 'finished' callback. + * Batch sets are processed sequentially, with the progress bar starting + * fresh for every new set. + */ +function batch_set($batch_definition) { + if ($batch_definition) { + $batch =& batch_get(); + // Initialize the batch + if (empty($batch)) { + $batch = array( + 'id' => db_next_id('{batch}_bid'), + 'sets' => array(), + ); + } + + $init = array( + 'sandbox' => array(), + 'results' => array(), + 'success' => FALSE, + ); + // Use get_t() to allow batches at install time. + $t = get_t(); + $defaults = array( + 'title' => $t('Processing'), + 'init_message' => $t('Initializing.'), + 'progress_message' => $t('Remaining @remaining of @total.'), + 'error_message' => $t('An error has occurred.'), + ); + $batch_set = $init + $batch_definition + $defaults; + + // Tweak init_message to avoid the bottom of the page flickering down after init phase. + $batch_set['init_message'] .= '<br/> '; + $batch_set['total'] = count($batch_set['operations']); + + // If the batch is being processed (meaning we are executing a stored submit callback), + // insert the new set after the current one. + if (isset($batch['current_set'])) { + // array_insert does not exist... + $slice1 = array_slice($batch['sets'], 0, $batch['current_set'] + 1); + $slice2 = array_slice($batch['sets'], $batch['current_set'] + 1); + $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); + } + else { + $batch['sets'][] = $batch_set; + } + } +} + +/** + * Process the batch. + * + * Unless the batch has been markes with 'progressive' = FALSE, the function + * isses a drupal_goto and thus ends page execution. + * + * This function is not needed in form submit callbacks; Form API takes care + * of batches issued during form submission. + * + * @param $redirect + * (optional) Path to redirect to when the batch has finished processing. + * @param $url + * (optional - should ony be used for separate scripts like update.php) + * URL of the batch processing page. + */ +function batch_process($redirect = NULL, $url = NULL) { + global $form_values, $user; + $batch =& batch_get(); + + // batch_process should not be called inside form _submit callbacks, or while a + // batch is already running. Neutralize the call if it is the case. + if (isset($batch['current_set']) || (isset($form_values) && !isset($batch['progressive']))) { + return; + } + + if (isset($batch)) { + // Add process information + $t = get_t(); + $url = isset($url) ? $url : 'batch'; + $process_info = array( + 'current_set' => 0, + 'progressive' => TRUE, + 'url' => isset($url) ? $url : 'batch', + 'source_page' => $_GET['q'], + 'redirect' => $redirect, + 'error_message' => $t('Please continue to <a href="!error_url">the error page</a>', array('!error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'error'))))), + ); + $batch += $process_info; + + if ($batch['progressive']) { + // Save and unset the destination if any. drupal_goto looks for redirection + // in $_REQUEST['destination'] and $_REQUEST['edit']['destination']. + if (isset($_REQUEST['destination'])) { + $batch['destination'] = $_REQUEST['destination']; + unset($_REQUEST['destination']); + } + elseif (isset($_REQUEST['edit']['destination'])) { + $batch['destination'] = $_REQUEST['edit']['destination']; + unset($_REQUEST['edit']['destination']); + } + db_query("INSERT INTO {batch} (bid, sid, timestamp, batch) VALUES (%d, %d, %d, '%s')", $batch['id'], $user->sid, time(), serialize($batch)); + drupal_goto($batch['url'], 'op=start&id='. $batch['id']); + } + else { + // Non-progressive execution: bypass the whole progressbar workflow + // and execute the batch in one pass. + require_once './includes/batch.inc'; + _batch_process(); + } + } +} + +/** + * Retrive the current batch. + */ +function &batch_get() { + static $batch = array(); + return $batch; +} + +/** + * @} End of "defgroup batch". + */ diff --git a/includes/theme.inc b/includes/theme.inc index 84e3406dd454304787c202aa14c72a452e48f5f5..593c2819ef1c3dee714bf1f00affe49872fa58c8 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -686,10 +686,11 @@ function theme_placeholder($text) { * * @param $content * The page content to show. - * @param $messages + * @param $show_messages * Whether to output status and error messages. + * FALSE can be useful to postpone the messages to a subsequent page. */ -function theme_maintenance_page($content, $messages = TRUE) { +function theme_maintenance_page($content, $show_messages = TRUE) { // Set required headers. drupal_set_header('Content-Type: text/html; charset=utf-8'); drupal_set_html_head('<style type="text/css" media="all">@import "'. base_path() .'misc/maintenance.css";</style>'); @@ -710,7 +711,7 @@ function theme_maintenance_page($content, $messages = TRUE) { 'logo' => base_path() .'themes/garland/minnelli/logo.png', 'site_title' => t('Drupal'), 'title' => drupal_get_title(), - 'messages' => theme('status_messages'), + 'messages' => $show_messages ? theme('status_messages') : '', 'content' => $content, ); @@ -1288,9 +1289,9 @@ function theme_username($object) { function theme_progress_bar($percent, $message) { $output = '<div id="progress" class="progress">'; - $output .= '<div class="percentage">'. $percent .'%</div>'; - $output .= '<div class="status">'. $message .'</div>'; $output .= '<div class="bar"><div class="filled" style="width: '. $percent .'%"></div></div>'; + $output .= '<div class="percentage">'. $percent .'%</div>'; + $output .= '<div class="message">'. $message .'</div>'; $output .= '</div>'; return $output; @@ -1399,7 +1400,7 @@ function template_preprocess_page(&$variables) { $variables['help'] = theme('help'); $variables['language'] = $GLOBALS['language']; $variables['logo'] = theme_get_setting('logo'); - $variables['messages'] = theme('status_messages'); + $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; $variables['mission'] = isset($mission) ? $mission : ''; $variables['primary_links'] = menu_primary_links(); $variables['search_box'] = (theme_get_setting('toggle_search') ? drupal_get_form('search_theme_form') : ''); diff --git a/misc/batch.js b/misc/batch.js new file mode 100644 index 0000000000000000000000000000000000000000..43117e2431476bc9e4f927fb6349afebdd99e268 --- /dev/null +++ b/misc/batch.js @@ -0,0 +1,31 @@ +if (Drupal.jsEnabled) { + $(document).ready(function() { + $('#progress').each(function () { + var holder = this; + var uri = Drupal.settings.batch.uri; + var initMessage = Drupal.settings.batch.initMessage; + var errorMessage = Drupal.settings.batch.errorMessage; + + // Success: redirect to the summary. + var updateCallback = function (progress, status, pb) { + if (progress == 100) { + pb.stopMonitoring(); + window.location = uri+'&op=finished'; + } + } + + var errorCallback = function (pb) { + var div = document.createElement('p'); + div.className = 'error'; + $(div).html(errorMessage); + $(holder).prepend(div); + $('#wait').hide(); + } + + var progress = new Drupal.progressBar('updateprogress', updateCallback, "POST", errorCallback); + progress.setProgress(-1, initMessage); + $(holder).append(progress.element); + progress.startMonitoring(uri+'&op=do', 10); + }); + }); +} diff --git a/misc/drupal.js b/misc/drupal.js index 04176089dd887fa59dc2880581bbe01e23a9ed69..c4fa5d8ab18694bea47187d742039e08f2f3b977 100644 --- a/misc/drupal.js +++ b/misc/drupal.js @@ -222,5 +222,8 @@ Drupal.getSelection = function (element) { // Global Killswitch on the <html> element if (Drupal.jsEnabled) { + // Global Killswitch on the <html> element document.documentElement.className = 'js'; + // 'js enabled' cookie + document.cookie = 'has_js=1'; } diff --git a/misc/progress.js b/misc/progress.js index 3db804f2f60e6ad746cddb4ae5966e76c3d40ed6..cf5c120179e8788a3805d97bbca3de642aef8d3f 100644 --- a/misc/progress.js +++ b/misc/progress.js @@ -20,9 +20,9 @@ Drupal.progressBar = function (id, updateCallback, method, errorCallback) { this.element = document.createElement('div'); this.element.id = id; this.element.className = 'progress'; - $(this.element).html('<div class="percentage"></div>'+ - '<div class="message"> </div>'+ - '<div class="bar"><div class="filled"></div></div>'); + $(this.element).html('<div class="bar"><div class="filled"></div></div>'+ + '<div class="percentage"></div>'+ + '<div class="message"> </div>'); } /** diff --git a/modules/system/page.tpl.php b/modules/system/page.tpl.php index fce977e5fb36203e5380a051543ad1df8eaf0d13..5bf9e423b02e09bbb7fa6206239cd45de3934760 100644 --- a/modules/system/page.tpl.php +++ b/modules/system/page.tpl.php @@ -41,7 +41,7 @@ <h1 class="title"><?php print $title ?></h1> <div class="tabs"><?php print $tabs ?></div> <?php print $help ?> - <?php print $messages ?> + <?php if ($show_messages) { print $messages; }?> <?php print $content; ?> <?php print $feed_icons; ?> </div> diff --git a/modules/system/system.install b/modules/system/system.install index f409de7fa38f96930f18a263e06ec125bca62261..7cf9b19797df57e82dc04780331b20e958a7e519 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -190,6 +190,15 @@ function system_install() { UNIQUE KEY authname (authname) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {batch} ( + bid int(11) NOT NULL, + sid varchar(64) NOT NULL, + timestamp int(11) NOT NULL, + batch longtext, + PRIMARY KEY (bid), + KEY sid (sid) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {blocks} ( module varchar(64) DEFAULT '' NOT NULL, delta varchar(32) NOT NULL default '0', @@ -666,6 +675,15 @@ function system_install() { UNIQUE (authname) )"); + db_query("CREATE TABLE {batch} ( + bid int NOT NULL default '0', + sid varchar(64) NOT NULL default '', + timestamp int NOT NULL default '0', + batch text, + PRIMARY KEY (bid), + )"); + db_query("CREATE INDEX {batch}_sid_idx ON {batch} (sid)"); + db_query("CREATE TABLE {blocks} ( module varchar(64) DEFAULT '' NOT NULL, delta varchar(32) NOT NULL default '0', diff --git a/modules/system/system.module b/modules/system/system.module index fab91e2d6c7a9d3e96984b70d53d58e3bf2af69a..a18439e07a2230e8add71e53530aa40885e24668 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -327,6 +327,12 @@ function system_menu() { 'page callback' => 'system_sql', 'type' => MENU_CALLBACK, ); + // Default page for batch operations + $items['batch'] = array( + 'page callback' => 'system_batch_page', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -2459,5 +2465,22 @@ function theme_system_admin_by_module($menu_items) { function system_cron() { // Cleanup the flood db_query('DELETE FROM {flood} WHERE timestamp < %d', time() - 3600); + // Cleanup the batch table + db_query('DELETE FROM {batch} WHERE timestamp < %d', time() - 864000); } +/** + * Default page callback for batches. + */ +function system_batch_page() { + require_once './includes/batch.inc'; + $output = _batch_page(); + if ($output === FALSE) { + drupal_access_denied(); + } + else { + // Force a page without blocks or messages to + // display a list of collected messages later. + print theme('page', $output, FALSE, FALSE); + } +} diff --git a/themes/bluemarine/page.tpl.php b/themes/bluemarine/page.tpl.php index fce977e5fb36203e5380a051543ad1df8eaf0d13..2f101ddf17facb8f62ec54a6a119f573e35a8386 100644 --- a/themes/bluemarine/page.tpl.php +++ b/themes/bluemarine/page.tpl.php @@ -41,7 +41,7 @@ <h1 class="title"><?php print $title ?></h1> <div class="tabs"><?php print $tabs ?></div> <?php print $help ?> - <?php print $messages ?> + <?php if ($show_messages) { print $messages; } ?> <?php print $content; ?> <?php print $feed_icons; ?> </div> diff --git a/themes/chameleon/chameleon.theme b/themes/chameleon/chameleon.theme index 7d3134b86cbb899db13ba9c412ab31a4ba5dca59..1c87653d3380f55741ea89dad84b4951c20f03d1 100644 --- a/themes/chameleon/chameleon.theme +++ b/themes/chameleon/chameleon.theme @@ -23,7 +23,7 @@ function chameleon_theme($existing) { return $templates; } -function chameleon_page($content, $show_blocks = TRUE) { +function chameleon_page($content, $show_blocks = TRUE, $show_messages = TRUE) { $language = isset($GLOBALS['language']) ? $GLOBALS['language']->language : NULL; if (theme_get_setting('toggle_favicon')) { @@ -94,7 +94,9 @@ function chameleon_page($content, $show_blocks = TRUE) { $output .= theme('help'); - $output .= theme('status_messages'); + if ($show_messages) { + $output .= theme('status_messages'); + } $output .= "\n<!-- begin content -->\n"; $output .= $content; diff --git a/themes/garland/page.tpl.php b/themes/garland/page.tpl.php index 3663f6586a4abdfd05d12626f8b10e195e54755c..5f607822866456b67cb4753d1363c5e01e0eb6aa 100644 --- a/themes/garland/page.tpl.php +++ b/themes/garland/page.tpl.php @@ -71,7 +71,7 @@ <?php if (isset($tabs2)): print $tabs2; endif; ?> <?php if ($help): print $help; endif; ?> - <?php if ($messages): print $messages; endif; ?> + <?php if ($show_messages && $messages): print $messages; endif; ?> <?php print $content ?> <span class="clear"></span> <?php print $feed_icons ?> diff --git a/themes/pushbutton/page.tpl.php b/themes/pushbutton/page.tpl.php index acf27949bd441c2dbe7033c47b8eab1f4115e802..027bd026ca467f790ce7fa58fc634e4f6a5dc14a 100644 --- a/themes/pushbutton/page.tpl.php +++ b/themes/pushbutton/page.tpl.php @@ -76,7 +76,7 @@ <div id="help"><?php print $help ?></div> <?php endif; ?> - <?php if ($messages != ""): ?> + <?php if ($show_messages && $messages != ""): ?> <?php print $messages ?> <?php endif; ?> diff --git a/update.php b/update.php index c104b2308007977b4da33173de5a264cdf97076e..c406531619c63a0eb77a8d0f598da739ed381a9a 100644 --- a/update.php +++ b/update.php @@ -283,37 +283,34 @@ function update_fix_watchdog() { * The module whose update will be run. * @param $number * The update number to run. - * - * @return - * TRUE if the update was finished. Otherwise, FALSE. + * @param $context + * The batch conetxt array */ -function update_data($module, $number) { - $ret = module_invoke($module, 'update_'. $number); - // Assume the update finished unless the update results indicate otherwise. - $finished = 1; +function update_do_one($module, $number, &$context) { + $function = $module .'_update_'. $number; + if (function_exists($function)) { + $ret = $function(&$context['sandbox']); + } + if (isset($ret['#finished'])) { - $finished = $ret['#finished']; + $context['finished'] = $ret['#finished']; unset($ret['#finished']); } - // Save the query and results for display by update_finished_page(). - if (!isset($_SESSION['update_results'])) { - $_SESSION['update_results'] = array(); - } - if (!isset($_SESSION['update_results'][$module])) { - $_SESSION['update_results'][$module] = array(); + if (!isset($context['results'][$module])) { + $context['results'][$module] = array(); } - if (!isset($_SESSION['update_results'][$module][$number])) { - $_SESSION['update_results'][$module][$number] = array(); + if (!isset($context['results'][$module][$number])) { + $context['results'][$module][$number] = array(); } - $_SESSION['update_results'][$module][$number] = array_merge($_SESSION['update_results'][$module][$number], $ret); + $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);; - if ($finished == 1) { + if ($context['finished'] == 1) { // Update the installed version drupal_set_installed_schema_version($module, $number); } - return $finished; + $context['message'] = t('Updating @module module', array('@module' => $module)); } function update_selection_page() { @@ -321,8 +318,6 @@ function update_selection_page() { $output .= '<p>Click Update to start the update process.</p>'; drupal_set_title('Drupal database update'); - // Prevent browser from using cached drupal.js or update.js - drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE); $output .= drupal_get_form('update_script_selection_form'); update_task_list('select'); @@ -377,7 +372,10 @@ function update_script_selection_form() { return $form; } -function update_update_page() { +function update_batch() { + global $base_url; + + $operations = array(); // Set the installed version so updates start at the correct place. foreach ($_POST['start'] as $module => $version) { drupal_set_installed_schema_version($module, $version - 1); @@ -386,145 +384,35 @@ function update_update_page() { if ($version <= $max_version) { foreach ($updates as $update) { if ($update >= $version) { - $_SESSION['update_remaining'][] = array('module' => $module, 'version' => $update); + $operations[] = array('update_do_one', array($module, $update)); } } } } - - // Keep track of total number of updates - if (isset($_SESSION['update_remaining'])) { - $_SESSION['update_total'] = count($_SESSION['update_remaining']); - } - - if ($_POST['has_js']) { - return update_progress_page(); - } - else { - return update_progress_page_nojs(); - } -} - -function update_progress_page() { - // Prevent browser from using cached drupal.js or update.js - drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE); - drupal_add_js('misc/update.js', 'core', 'header', FALSE, TRUE); - - drupal_set_title('Updating'); - update_task_list('run'); - $output = '<div id="progress"></div>'; - $output .= '<p id="wait">Please wait while your site is being updated.</p>'; - return $output; -} - -/** - * Perform updates for one second or until finished. - * - * @return - * An array indicating the status after doing updates. The first element is - * the overall percentage finished. The second element is a status message. - */ -function update_do_updates() { - while (isset($_SESSION['update_remaining']) && ($update = reset($_SESSION['update_remaining']))) { - $update_finished = update_data($update['module'], $update['version']); - if ($update_finished == 1) { - // Dequeue the completed update. - unset($_SESSION['update_remaining'][key($_SESSION['update_remaining'])]); - $update_finished = 0; // Make sure this step isn't counted double - } - if (timer_read('page') > 1000) { - break; - } - } - - if ($_SESSION['update_total']) { - $percentage = floor(($_SESSION['update_total'] - count($_SESSION['update_remaining']) + $update_finished) / $_SESSION['update_total'] * 100); - } - else { - $percentage = 100; - } - - // When no updates remain, clear the caches in case the data has been updated. - if (!isset($update['module'])) { - cache_clear_all('*', 'cache', TRUE); - cache_clear_all('*', 'cache_page', TRUE); - cache_clear_all('*', 'cache_filter', TRUE); - drupal_clear_css_cache(); - } - - return array($percentage, isset($update['module']) ? 'Updating '. $update['module'] .' module' : 'Updating complete'); + $batch = array( + 'operations' => $operations, + 'title' => 'Updating', + 'init_message' => 'Starting updates', + 'error_message' => 'An unrecoverable error has occured. You can find the error message below. It is advised to copy it to the clipboard for reference.', + 'finished' => 'update_finished', + ); + batch_set($batch); + batch_process($base_url .'/update.php?op=results', $base_url .'/update.php'); } -/** - * Perform updates for the JS version and return progress. - */ -function update_do_update_page() { - global $conf; - - // HTTP Post required - if ($_SERVER['REQUEST_METHOD'] != 'POST') { - drupal_set_message('HTTP Post is required.', 'error'); - drupal_set_title('Error'); - return ''; - } +function update_finished($success, $results, $operations) { + // clear the caches in case the data has been updated. + cache_clear_all('*', 'cache', TRUE); + cache_clear_all('*', 'cache_page', TRUE); + cache_clear_all('*', 'cache_filter', TRUE); + drupal_clear_css_cache(); - // Error handling: if PHP dies, the output will fail to parse as JSON, and - // the Javascript will tell the user to continue to the op=error page. - list($percentage, $message) = update_do_updates(); - print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message)); + $_SESSION['update_results'] = $results; + $_SESSION['update_success'] = $success; + $_SESSION['updates_remaining'] = $operations; } -/** - * Perform updates for the non-JS version and return the status page. - */ -function update_progress_page_nojs() { - drupal_set_title('Updating'); - update_task_list('run'); - - $new_op = 'do_update_nojs'; - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - // This is the first page so return some output immediately. - $percentage = 0; - $message = 'Starting updates'; - } - else { - // This is one of the later requests: do some updates first. - - // Error handling: if PHP dies due to a fatal error (e.g. non-existant - // function), it will output whatever is in the output buffer, - // followed by the error message. So, we put an explanation in the - // buffer to guide the user when an error happens. - ob_start(); - $fallback = '<p class="error">An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference. Please continue to the <a href="update.php?op=error">update summary</a>.</p>'; - $fallback = theme('maintenance_page', $fallback, 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; - - // Do updates - list($percentage, $message) = update_do_updates(); - if ($percentage == 100) { - $new_op = 'finished'; - } - - // Updates were successful; wipe the output buffer as it's unneeded. - ob_end_clean(); - } - - drupal_set_html_head('<meta http-equiv="Refresh" content="0; URL=update.php?op='. $new_op .'">'); - $output = theme('progress_bar', $percentage, $message); - $output .= '<p>Updating your site will take a few seconds.</p>'; - - // Note: do not output drupal_set_message()s until the summary page. - print theme('maintenance_page', $output, FALSE); - return NULL; -} - -function update_finished_page($success) { +function update_results_page() { drupal_set_title('Drupal database update'); // NOTE: we can't use l() here because the URL would point to 'update.php?q=admin'. $links[] = '<a href="'. base_path() .'">Main page</a>'; @@ -532,18 +420,18 @@ function update_finished_page($success) { update_task_list(); // Report end result - if ($success) { + if ($_SESSION['update_success']) { $output = '<p>Updates were attempted. If you see no failures below, you may proceed happily to the <a href="index.php?q=admin">administration pages</a>. Otherwise, you may need to update your database manually. All errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>.</p>'; } else { - $update = reset($_SESSION['update_remaining']); - $output = '<p class="error">The update process was aborted prematurely while running <strong>update #'. $update['version'] .' in '. $update['module'] .'.module</strong>. All other errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>. You may need to check the <code>watchdog</code> database table manually.</p>'; + list($module, $version) = array_pop(reset($_SESSION['updates_remaining'])); + $output = '<p class="error">The update process was aborted prematurely while running <strong>update #'. $version .' in '. $module .'.module</strong>. All other errors have been <a href="index.php?q=admin/logs/watchdog">logged</a>. You may need to check the <code>watchdog</code> database table manually.</p>'; } if ($GLOBALS['access_check'] == FALSE) { $output .= "<p><strong>Reminder: don't forget to set the <code>\$access_check</code> value at the top of <code>update.php</code> back to <code>TRUE</code>.</strong></p>"; } - + $output .= theme('item_list', $links); // Output a list of queries executed @@ -570,8 +458,9 @@ function update_finished_page($success) { } } $output .= '</div>'; - unset($_SESSION['update_results']); } + unset($_SESSION['update_results']); + unset($_SESSION['update_success']); return $output; } @@ -778,6 +667,45 @@ function update_create_cache_tables() { return $ret; } +/** + * Create the batch table. + * + * This is part of the Drupal 5.x to 6.x migration. + */ +function update_create_batch_table() { + + // If batch table exists, update is not necessary + if (db_table_exists('batch')) { + return; + } + + $ret = array(); + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + $ret[] = update_sql("CREATE TABLE {batch} ( + bid int(11) NOT NULL, + sid varchar(64) NOT NULL, + timestamp int(11) NOT NULL, + batch longtext, + PRIMARY KEY (bid), + KEY sid (sid) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + break; + case 'pgsql': + $ret[] = update_sql("CREATE TABLE {batch} ( + bid int NOT NULL default '0', + sid varchar(64) NOT NULL default '', + timestamp int NOT NULL default '0', + batch text, + PRIMARY KEY (bid), + )"); + $ret[] = update_sql("CREATE INDEX {batch}_sid_idx ON {batch} (sid)"); + break; + } + return $ret; +} + /** * Add the update task list to the current page. */ @@ -807,6 +735,7 @@ function update_task_list($active = NULL) { // variable_(get|set), which only works after a full bootstrap. update_fix_access_table(); update_create_cache_tables(); +update_create_batch_table(); // Turn error reporting back on. From now on, only fatal errors (which are // not passed through the error handler) will cause a message to be printed. @@ -816,6 +745,7 @@ function update_task_list($active = NULL) { if (($access_check == FALSE) || ($user->uid == 1)) { include_once './includes/install.inc'; + include_once './includes/batch.inc'; drupal_load_updates(); update_fix_schema_version(); @@ -825,39 +755,33 @@ function update_task_list($active = NULL) { $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; switch ($op) { - case 'Update': - $output = update_update_page(); - break; - - case 'finished': - $output = update_finished_page(TRUE); - break; - - case 'error': - $output = update_finished_page(FALSE); + // update.php ops + case '': + $output = update_info_page(); break; - case 'do_update': - $output = update_do_update_page(); + case 'selection': + $output = update_selection_page(); break; - case 'do_update_nojs': - $output = update_progress_page_nojs(); + case 'Update': + update_batch(); break; - case 'selection': - $output = update_selection_page(); + case 'results': + $output = update_results_page(); break; + // Regular batch ops : defer to batch processing API default: - $output = update_info_page(); + update_task_list('run'); + $output = _batch_page(); break; } } else { $output = update_access_denied_page(); } - -if (isset($output)) { +if (isset($output) && $output) { print theme('maintenance_page', $output); }