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 12 Apr 2007 21:29:21 -0000 @@ -0,0 +1,322 @@ + t('Descriptive batch title'), + * 'init_message' => t('Initial message'), + * 'error_message' => t('Error message'), + * // @current, @remaining, @total and @percent can be used here + * 'progress_message' => t('Remaining @remaining of @total.'), + * 'finished_callback' => 'my_finished_callback', + * 'operations' => array( + * array('callback' => 'my_func_1', 'arguments' => array('foo', 'bar')), + * array('callback' => 'my_func_2', 'arguments' => array('foo2', 'bar2', 'baz2')), + * ... + * ); + * ); + * return drupal_set_batch($batch); + * } + * @endcode + * + * @param $batch + * Associative array with initial batch options. + * @param $execute + * Boolean to run the batch (TRUE) or just store the options (FALSE). + * @return + * An extended batch array. + */ +function drupal_set_batch($batch, $execute = FALSE) { + static $_batch_id = NULL; + + // We accept the batch if none has been set before in this HTTP request, + // or if the batch has already been registered and is being altered (in + // drupal_submit_form) + if (is_array($batch)) { + if (!isset($_batch_id)) { + $_batch_id = time(); + $batch += array( + 'batch_id' => $_batch_id, + 'title' => t('Processing'), + 'init_message' => t('Initializing...'), + 'progress_message' => t('Remaining @remaining of @total.'), + 'path' => 'batch', + ); + } + + // New batch or replacing previously returned batch data + if (isset($batch['batch_id']) && $batch['batch_id'] == $_batch_id) { + if ($execute) { + db_query("INSERT INTO {batch} (bid, batch) VALUES (%d, '%s')", $batch['batch_id'], serialize($batch)); + drupal_goto($batch['path'], 'batch_id='. $batch['batch_id']); + } + return $batch; + } + } +} + +/** + * Default page callback and state based dispatcher for batches. + */ +function batch_page() { + global $_batch; + + // Grab the stored batch operations + if (isset($_REQUEST['batch_id']) && $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $_REQUEST['batch_id']))) { + $_batch = unserialize($data); + } + else { + // @todo: not good for update.php + return drupal_not_found(); + } + + // Register database update for end of processing + register_shutdown_function('batch_shutdown'); + + $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; + switch ($op) { + // Finish with success. + case 'finished': + $output = _batch_finished(TRUE); + break; + + // Finish with error. + // @todo: this is update.php specific (link in the error message) + case 'error': + $output = _batch_finished(FALSE); + break; + + case 'do': + $output = _batch_do(); + break; + + case 'do_nojs': + $output = _batch_progress_page_nojs(); + break; + + default: + $output = _batch_prepare(); + break; + } + + return $output; +} + +/** + * Perform initial preparation for running a batch, choose between the + * JS and non-JS version. + */ +function _batch_prepare() { + global $_batch; + + // Return with success if we have no operations. + if (empty($_batch['operations'])) { + return _batch_finished(TRUE); + } + + $_batch['total'] = count($_batch['operations']); + $_batch['results'] = array(); + + // drupal.js remembers js enabled users via the has_js cookie. + 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() { + global $_batch; + + drupal_set_title($_batch['title']); + + drupal_add_js('misc/progress.js', 'core', 'header', FALSE, TRUE); + $js_setting = array( + 'batch' => array( + 'errorMessage' => $_batch['error_message'], + 'initMessage' => $_batch['init_message'], + // @todo: account for paths like 'update.php' + 'uri' => url($_batch['path'], array('query' => array('batch_id' => $_batch['batch_id']))), + ), + ); + drupal_add_js($js_setting, 'setting'); + drupal_add_js('misc/batch.js', 'core', 'header', FALSE, TRUE); + + $output = '
'; + return $output; +} + +/** + * Inform the browser about progress made in the batch. + */ +function _batch_do() { + 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() { + global $_batch; + + drupal_set_title($_batch['title']); + + $new_op = 'do_nojs'; + + // This is the first page so return some output immediately. + if (!isset($_batch['running'])) { + $percentage = 0; + $message = $_batch['init_message']; + $_batch['running'] = TRUE; + } + + // This is one of the later requests: do some updates first. + else { + + // 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 = $_batch['error_message_no_js']; + $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'; + } + + // Updates successful; remove fallback + ob_end_clean(); + } + // @todo: account for update.php path + $url = url($_batch['path'], array('query' => array('batch_id' => $_batch['batch_id'], 'op' => $new_op))); + drupal_set_html_head(''); + $output = theme('progress_bar', $percentage, $message); + return $output; + + // @todo: update.php has the following : + // Note: do not output drupal_set_message()s until the summary page. + //print theme('maintenance_page', $output, FALSE); + //return NULL; +} + +/** + * Process as many updates as possible in this HTTP request. + */ +function _batch_process() { + global $_batch; + + // While/list/each allows us to traverse a shirinking array. + while (list($key, $op) = each($_batch['operations'])) { + $op_finished = 1; + if (is_array($op) && isset($op['callback']) && function_exists($op['callback'])) { + $additions = array(&$_batch['results'], &$op_finished); + call_user_func_array($op['callback'], array_merge($op['arguments'], $additions)); + $task_message = isset($op['task']) ? $op['task'] : ''; + } + // Remove operation if finished + if ($op_finished == 1) { + unset($_batch['operations'][$key]); + $op_finished = 0; + } + // Skip until next HTTP request if too much time passed + if (timer_read('page') > 1000) { + break; + } + } + + $remaining = count($_batch['operations']); + $total = $_batch['total']; + $current = $total - $remaining + $op_finished; + $percentage = floor($current / $total * 100); + $values = array( + '@current' => floor($current), + '@remaining' => $remaining, + '@total' => $total, + '@percent' => $percentage + ); + $progress_message = strtr($_batch['progress_message'], $values); + + $message = $progress_message .'
'; + // @todo: $task_message will be used by update.php ('Updating foo.module') + $message.= $task_message ? $task_message : ' '; + + return array($percentage, $message); +} + +/** + * Display a result page when processing ends. + * + * @param $success + * A boolean flag on whether processing was successful. + * @return + * Page text to display on result page. + */ +function _batch_finished($success) { + global $_batch; + + // Set the flag for batch_shutdown() to clean the db record. + $_batch['finished'] = TRUE; + + // Determine page contents to output to user + $output = t('Batch processing done.'); + if (isset($_batch['finished_callback']) && function_exists($_batch['finished_callback'])) { + $output = $_batch['finished_callback']($_batch, $success, $_batch['results']); + } + + // Perform the remaining _submit callbacks if any. + if (isset($_batch['post_process'])) { + global $form_values; + $form_values = $_batch['post_process']['form_values']; + $redirect = drupal_submit_form($_batch['post_process']['form_id'], $_batch['post_process']['form']); + // @todo: We have to mimick the end of drupal_process_form + } + + return $output; +} + +/** + * Ensures that the batch data is updated in the database on shutdown. + */ +function batch_shutdown() { + global $_batch; + if (isset($_batch['finished']) && $_batch['finished']) { + db_query("DELETE FROM {batch} WHERE bid = %d", $_batch['batch_id']); + } + else { + db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($_batch), $_batch['batch_id']); + } +} + +// @todo: upgrade for system.install +// @todo: remove old / stale batches from db on cron Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.627 diff -u -p -r1.627 common.inc --- includes/common.inc 6 Apr 2007 13:27:20 -0000 1.627 +++ includes/common.inc 12 Apr 2007 21:29:22 -0000 @@ -1877,6 +1877,7 @@ function _drupal_bootstrap_full() { require_once './includes/unicode.inc'; require_once './includes/image.inc'; require_once './includes/form.inc'; + require_once './includes/batch.inc'; // Set the Drupal custom error handler. set_error_handler('drupal_error_handler'); // Emit the correct charset HTTP header. Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.187 diff -u -p -r1.187 form.inc --- includes/form.inc 9 Apr 2007 13:58:02 -0000 1.187 +++ includes/form.inc 12 Apr 2007 21:29:23 -0000 @@ -415,15 +415,35 @@ function drupal_submit_form($form_id, $f $goto = NULL; if (isset($form['#submit'])) { - foreach ($form['#submit'] as $function => $args) { + // While/list/each allows us to traverse a shrinking array + while (list($function, $args) = each($form['#submit'])) { + array_shift($form['#submit']); + if (function_exists($function)) { $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); $submitted = TRUE; + if (isset($redirect)) { - $goto = $redirect; + // We got redirected to a batch + if (is_array($redirect) && isset($redirect['batch_id'])) { + // Save remaining _submit callbacks + if (!empty($form['#submit'])) { + $redirect['post_process'] = array( + 'form' => $form, + 'form_id' => $form_id, + 'form_values' => $form_values + ); + } + // Overwrite previously set batch and execute + drupal_set_batch($redirect, TRUE); + break; + } + else { + $goto = $redirect; + } } } } Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.347 diff -u -p -r1.347 theme.inc --- includes/theme.inc 9 Apr 2007 13:43:57 -0000 1.347 +++ includes/theme.inc 12 Apr 2007 21:29:25 -0000 @@ -1395,8 +1395,8 @@ function theme_username($object) { function theme_progress_bar($percent, $message) { $output = '
'; $output .= '
'. $percent .'%
'; - $output .= '
'. $message .'
'; $output .= '
'; + $output .= '
'. $message .'
'; $output .= '
'; return $output; 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 12 Apr 2007 21:29:25 -0000 @@ -0,0 +1,33 @@ +// $Id$ + +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); + }); + }); +} \ No newline at end of file Index: misc/drupal.js =================================================================== RCS file: /cvs/drupal/drupal/misc/drupal.js,v retrieving revision 1.30 diff -u -p -r1.30 drupal.js --- misc/drupal.js 9 Apr 2007 13:58:02 -0000 1.30 +++ misc/drupal.js 12 Apr 2007 21:29:25 -0000 @@ -220,7 +220,98 @@ Drupal.getSelection = function (element) return { 'start': element.selectionStart, 'end': element.selectionEnd }; } -// Global Killswitch on the element +/** + * Cookie plugin + * + * Copyright (c) 2006 Klaus Hartl (stilbuero.de) + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +/** + * Create a cookie with the given name and value and other optional parameters. + * + * @example $.cookie('the_cookie', 'the_value'); + * @desc Set the value of a cookie. + * @example $.cookie('the_cookie', 'the_value', {expires: 7, path: '/', domain: 'jquery.com', secure: true}); + * @desc Create a cookie with all available options. + * @example $.cookie('the_cookie', 'the_value'); + * @desc Create a session cookie. + * @example $.cookie('the_cookie', '', {expires: -1}); + * @desc Delete a cookie by setting a date in the past. + * + * @param String name The name of the cookie. + * @param String value The value of the cookie. + * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. + * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. + * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie and will not be retained + * when the the browser exits. + * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). + * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). + * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will + * require a secure protocol (like HTTPS). + * @type undefined + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ + +/** + * Get the value of a cookie with the given name. + * + * @example $.cookie('the_cookie'); + * @desc Get the value of a cookie. + * + * @param String name The name of the cookie. + * @return The value of the cookie. + * @type String + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ +jQuery.cookie = function(name, value, options) { + if (typeof value != 'undefined') { // name and value given, set cookie + options = options || {}; + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toGMTString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toGMTString(); // use expires attribute, max-age is not supported by IE + } + var path = options.path ? '; path=' + options.path : ''; + var domain = options.domain ? '; domain=' + options.domain : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +}; + if (Drupal.jsEnabled) { + // Global Killswitch on the element document.documentElement.className = 'js'; + // Set "Javascript enabled" cookie + $.cookie('has_js', 1); } Index: misc/progress.js =================================================================== RCS file: /cvs/drupal/drupal/misc/progress.js,v retrieving revision 1.14 diff -u -p -r1.14 progress.js --- misc/progress.js 14 Dec 2006 14:21:36 -0000 1.14 +++ misc/progress.js 12 Apr 2007 21:29:25 -0000 @@ -20,9 +20,9 @@ Drupal.progressBar = function (id, updat this.element = document.createElement('div'); this.element.id = id; this.element.className = 'progress'; - $(this.element).html('
'+ - '
 
'+ - '
'); + $(this.element).html('
'+ + '
'+ + '
 
'); } /** Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.89 diff -u -p -r1.89 system.install --- modules/system/system.install 10 Apr 2007 10:10:27 -0000 1.89 +++ modules/system/system.install 12 Apr 2007 21:29:26 -0000 @@ -190,6 +190,12 @@ function system_install() { UNIQUE KEY authname (authname) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {batch} ( + bid int(11) NOT NULL, + batch longtext, + PRIMARY KEY (bid) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {blocks} ( module varchar(64) DEFAULT '' NOT NULL, delta varchar(32) NOT NULL default '0', @@ -659,6 +665,12 @@ function system_install() { UNIQUE (authname) )"); + db_query("CREATE TABLE {batch} ( + bid int NOT NULL default '0', + batch text, + PRIMARY KEY (bid) + )"); + db_query("CREATE TABLE {blocks} ( module varchar(64) DEFAULT '' NOT NULL, delta varchar(32) NOT NULL default '0', Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.464 diff -u -p -r1.464 system.module --- modules/system/system.module 10 Apr 2007 10:10:27 -0000 1.464 +++ modules/system/system.module 12 Apr 2007 21:29:28 -0000 @@ -327,6 +327,12 @@ function system_menu() { 'page callback' => 'system_sql', 'type' => MENU_CALLBACK, ); + // Default callback for batch operations + $items['batch'] = array( + 'page callback' => 'batch_page', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; }