summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.txt1
-rw-r--r--includes/batch.inc297
-rw-r--r--includes/common.inc4
-rw-r--r--includes/form.inc285
-rw-r--r--includes/theme.inc13
-rw-r--r--misc/batch.js31
-rw-r--r--misc/drupal.js3
-rw-r--r--misc/progress.js6
-rw-r--r--modules/system/page.tpl.php2
-rw-r--r--modules/system/system.install18
-rw-r--r--modules/system/system.module23
-rw-r--r--themes/bluemarine/page.tpl.php2
-rw-r--r--themes/chameleon/chameleon.theme6
-rw-r--r--themes/garland/page.tpl.php2
-rw-r--r--themes/pushbutton/page.tpl.php2
-rw-r--r--update.php276
16 files changed, 769 insertions, 202 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index e3718cb47..c2815d1ef 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 000000000..60c0e370b
--- /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 : '&nbsp';
+
+ 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 354eb4f7f..ec2f06432 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 dc843d286..5de5ded31 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/>&nbsp;';
+ $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 84e3406dd..593c2819e 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 000000000..43117e243
--- /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 04176089d..c4fa5d8ab 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 3db804f2f..cf5c12017 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">&nbsp;</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">&nbsp;</div>');
}
/**
diff --git a/modules/system/page.tpl.php b/modules/system/page.tpl.php
index fce977e5f..5bf9e423b 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 f409de7fa..7cf9b1979 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 fab91e2d6..a18439e07 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 fce977e5f..2f101ddf1 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 7d3134b86..1c87653d3 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 3663f6586..5f6078228 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 acf27949b..027bd026c 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 c104b2308..c40653161 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;
}
@@ -779,6 +668,45 @@ function update_create_cache_tables() {
}
/**
+ * 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 @@ drupal_maintenance_theme();
// 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 @@ ini_set('display_errors', TRUE);
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 @@ if (($access_check == FALSE) || ($user->uid == 1)) {
$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);
}