### Eclipse Workspace Patch 1.0 #P Test Drupal 6 Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.472 diff -u -r1.472 system.module --- modules/system/system.module 30 Apr 2007 17:03:28 -0000 1.472 +++ modules/system/system.module 1 May 2007 00:26:08 -0000 @@ -327,6 +327,12 @@ '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,21 @@ 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. + print theme('page', $output, FALSE, FALSE); + } +} \ No newline at end of file Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.99 diff -u -r1.99 system.install --- modules/system/system.install 25 Apr 2007 21:34:32 -0000 1.99 +++ modules/system/system.install 1 May 2007 00:26:07 -0000 @@ -190,6 +190,15 @@ 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 @@ 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', Index: modules/system/page.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/page.tpl.php,v retrieving revision 1.1 diff -u -r1.1 page.tpl.php --- modules/system/page.tpl.php 27 Apr 2007 07:42:54 -0000 1.1 +++ modules/system/page.tpl.php 1 May 2007 00:26:05 -0000 @@ -41,7 +41,7 @@

- + Index: themes/garland/page.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/page.tpl.php,v retrieving revision 1.6 diff -u -r1.6 page.tpl.php --- themes/garland/page.tpl.php 28 Mar 2007 14:08:23 -0000 1.6 +++ themes/garland/page.tpl.php 1 May 2007 00:26:08 -0000 @@ -71,7 +71,7 @@ - + Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.353 diff -u -r1.353 theme.inc --- includes/theme.inc 29 Apr 2007 16:13:12 -0000 1.353 +++ includes/theme.inc 1 May 2007 00:26:05 -0000 @@ -686,10 +686,11 @@ * * @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(''); @@ -710,7 +711,7 @@ '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_progress_bar($percent, $message) { $output = '
'; - $output .= '
'. $percent .'%
'; - $output .= '
'. $message .'
'; $output .= '
'; + $output .= '
'. $percent .'%
'; + $output .= '
'. $message .'
'; $output .= '
'; return $output; @@ -1399,7 +1400,7 @@ $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') : ''); Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.190 diff -u -r1.190 form.inc --- includes/form.inc 24 Apr 2007 13:53:10 -0000 1.190 +++ includes/form.inc 1 May 2007 00:26:04 -0000 @@ -255,6 +255,12 @@ // 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 pgrogress page. + // Execution continues only if programmatic form. + } if (!$form['#programmed']) { drupal_redirect_form($form, $redirect); } @@ -420,7 +426,19 @@ $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 @@ } /** -* 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,249 @@ /** * @} 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 progres of the ongoing operations. + * + * The API is primarily designed to integrate nicely with 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 stupid one : 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 comes as 1 : if left untouched (single-step operation), the operation will + * // be considered as finished. + * $node = node_load(array('uid' => $uid, 'type' => $type)); + * $a['results'][] = $node->nid .' : '. $node->title; + * $a['message'] = $node->title; + * } + * + * // more advanced : 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 = count($results) .' nodes processed.'; + * } + * else { + * $message = 'finished with an error'; + * } + * drupal_set_message($message); + * // provinding data for the redirected page is done through $_SESSION + * foreach ($results as $result) { + * $items[] = 'Loaded node '.$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('Precessing'). + * 'init_message' : message displayed while the processing initializes. + * 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. + * + */ +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(), + ); + } + + // Add the operations in a new 'batch set' + // Batch sets are used to ensure clean code independancy, 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 his own set of operations and results, + // and triggers his own 'finished' callback. + // Batch sets are processed sequentially, with the progress bar starting fresh for every new set. + + $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'] .= '
 '; + $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. + // 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 the error page', 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". + */ Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.637 diff -u -r1.637 common.inc --- includes/common.inc 30 Apr 2007 11:12:35 -0000 1.637 +++ includes/common.inc 1 May 2007 00:26:03 -0000 @@ -2313,11 +2313,11 @@ '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), Index: update.php =================================================================== RCS file: /cvs/drupal/drupal/update.php,v retrieving revision 1.217 diff -u -r1.217 update.php --- update.php 25 Apr 2007 21:28:00 -0000 1.217 +++ update.php 1 May 2007 00:26:02 -0000 @@ -1,5 +1,5 @@ $module)); } function update_selection_page() { @@ -321,8 +318,6 @@ $output .= '

Click Update to start the update process.

'; 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 @@ 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 @@ 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 = '
'; - $output .= '

Please wait while your site is being updated.

'; - 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 = '

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 update summary.

'; - $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 anyway. - list($fallback) = explode('', $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(''); - $output = theme('progress_bar', $percentage, $message); - $output .= '

Updating your site will take a few seconds.

'; - - // 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[] = 'Main page'; @@ -532,12 +420,12 @@ update_task_list(); // Report end result - if ($success) { + if ($_SESSION['update_success']) { $output = '

Updates were attempted. If you see no failures below, you may proceed happily to the administration pages. Otherwise, you may need to update your database manually. All errors have been logged.

'; } else { - $update = reset($_SESSION['update_remaining']); - $output = '

The update process was aborted prematurely while running update #'. $update['version'] .' in '. $update['module'] .'.module. All other errors have been logged. You may need to check the watchdog database table manually.

'; + list($module, $version) = array_pop(reset($_SESSION['updates_remaining'])); + $output = '

The update process was aborted prematurely while running update #'. $version .' in '. $module .'.module. All other errors have been logged. You may need to check the watchdog database table manually.

'; } if ($GLOBALS['access_check'] == FALSE) { @@ -570,8 +458,9 @@ } } $output .= ''; - unset($_SESSION['update_results']); } + unset($_SESSION['update_results']); + unset($_SESSION['update_success']); return $output; } @@ -779,6 +668,45 @@ } /** + * 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. */ function update_task_list($active = NULL) { @@ -807,6 +735,7 @@ // 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 @@ 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 @@ $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); } Index: themes/bluemarine/page.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/themes/bluemarine/page.tpl.php,v retrieving revision 1.21 diff -u -r1.21 page.tpl.php --- themes/bluemarine/page.tpl.php 13 Apr 2007 08:38:05 -0000 1.21 +++ themes/bluemarine/page.tpl.php 1 May 2007 00:26:08 -0000 @@ -41,7 +41,7 @@

- + Index: misc/progress.js =================================================================== RCS file: /cvs/drupal/drupal/misc/progress.js,v retrieving revision 1.14 diff -u -r1.14 progress.js --- misc/progress.js 14 Dec 2006 14:21:36 -0000 1.14 +++ misc/progress.js 1 May 2007 00:26:05 -0000 @@ -20,9 +20,9 @@ this.element = document.createElement('div'); this.element.id = id; this.element.className = 'progress'; - $(this.element).html('
'+ - '
 
'+ - '
'); + $(this.element).html('
'+ + '
'+ + '
 
'); } /** Index: misc/drupal.js =================================================================== RCS file: /cvs/drupal/drupal/misc/drupal.js,v retrieving revision 1.30 diff -u -r1.30 drupal.js --- misc/drupal.js 9 Apr 2007 13:58:02 -0000 1.30 +++ misc/drupal.js 1 May 2007 00:26:05 -0000 @@ -222,5 +222,8 @@ // Global Killswitch on the element if (Drupal.jsEnabled) { + // Global Killswitch on the element document.documentElement.className = 'js'; + // 'js enabled' cookie + document.cookie = 'has_js=1'; } Index: themes/pushbutton/page.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/themes/pushbutton/page.tpl.php,v retrieving revision 1.17 diff -u -r1.17 page.tpl.php --- themes/pushbutton/page.tpl.php 13 Apr 2007 08:38:05 -0000 1.17 +++ themes/pushbutton/page.tpl.php 1 May 2007 00:26:08 -0000 @@ -76,7 +76,7 @@
- + Index: themes/chameleon/chameleon.theme =================================================================== RCS file: /cvs/drupal/drupal/themes/chameleon/chameleon.theme,v retrieving revision 1.63 diff -u -r1.63 chameleon.theme --- themes/chameleon/chameleon.theme 17 Apr 2007 07:19:39 -0000 1.63 +++ themes/chameleon/chameleon.theme 1 May 2007 00:26:08 -0000 @@ -23,7 +23,7 @@ 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 @@ $output .= theme('help'); - $output .= theme('status_messages'); + if ($show_messages) { + $output .= theme('status_messages'); + } $output .= "\n\n"; $output .= $content; Index: includes/batch.inc =================================================================== RCS file: includes/batch.inc diff -N includes/batch.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/batch.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,297 @@ +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'] .'
'. $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 = '
'; + 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'] .'
'. $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 anyway. + list($fallback) = explode('', $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(''); + $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 .'
'; + $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']); + } +} Index: misc/batch.js =================================================================== RCS file: misc/batch.js diff -N misc/batch.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ misc/batch.js 1 Jan 1970 00:00:00 -0000 @@ -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); + }); + }); +}