diff --git a/includes/plugin.inc b/includes/plugin.inc new file mode 100644 index 0000000..5d64168 --- /dev/null +++ b/includes/plugin.inc @@ -0,0 +1,280 @@ + TRUE)); + + $form['#attached']['css'][] = drupal_get_path('module', 'update') . '/update.css'; + $form['#attached']['js'][] = drupal_get_path('module', 'update') . '/update.js'; + // Get all the available ways to transfer files. + $available_backends = module_invoke_all('filetransfer_backends'); + if (!count($available_backends)) { + // @TODO: Clean up this error handling + drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error'); + return array(); + } + + uasort($available_backends, 'drupal_sort_weight'); + + // Decide on a default backend. + if (isset($form_state['values']['connection_settings']['update_filetransfer_default'])) { + $update_filetransfer_default = $form_state['values']['connection_settings']['update_filetransfer_default']; + } + elseif ($update_filetransfer_default = variable_get('update_filetransfer_default', NULL)); + else { + $update_filetransfer_default = key($available_backends); + } + + $form['information']['main_header'] = array( + '#prefix' => '
', + '#markup' => t('We do not currently have a web based backup tool. Learn more about how to take a backup.', array('@backup_url' => url('http://drupal.org/node/22281'))), + '#suffix' => '
', + ); + + $form['information']['maint_header'] = array( + '#prefix' => '', + '#markup' => t('It is strongly recommended that you put your site into maintenance mode while performing an update.'), + '#suffix' => '
', + ); + + $form['information']['site_offline'] = array( + '#title' => t('Perform updates with site in maintenance mode'), + '#type' => 'checkbox', + '#default_value' => TRUE, + ); + + return $form; +} + +/** + * Submit function for the main update form. + * + * @see update_update_form() + */ +function update_update_form_submit($form, &$form_state) { + global $base_url; + switch ($form_state['clicked_button']['#name']) { + case 'process_updates': + + $operations = array(); + foreach ($form_state['storage']['projects'] as $project) { + // Put this in the begining (download everything first) + $operations[] = array('update_batch_get_project', array($project)); + $latest_version = _update_get_recommended_version($project); + // This is the .tar.gz from d.o. + $url = $latest_version['download_link']; + + $operations[] = array( + 'update_batch_copy_project', + array( + $project, + $url, + ), + ); + + $operations[] = array('update_batch_update_project', array($project)); + } + + $batch = array( + 'title' => t('Installing updates'), + 'init_message' => t('Preparing update operation'), + 'operations' => $operations, + 'finished' => 'update_batch_finished', + 'file' => drupal_get_path('module', 'update') . '/update.admin.inc', + ); + + $_SESSION['plugin_op'] = $batch; + + if ($form_state['values']['site_offline'] == TRUE) { + // Put site in offline mode. + variable_set('site_offline', TRUE); + } + drupal_goto(url($base_url . '/plugin.php', array('absolute' => TRUE))); + return; + + break; + + default: + $form_state['rebuild'] = TRUE; + // This is the first page, and store the list of selected projects + $form_state['storage']['projects'] = array_keys(array_filter($form_state['values']['projects'])); + break; + } +} + + +function update_install_form(&$form_state) { + $form = array(); + + $form['project_url'] = array( + '#type' => 'textfield', + '#title' => t('URL'), + '#description' => t('Paste the url to a Drupal module or theme archive (.tar.gz) here to install it. (e.g http://ftp.drupal.org/files/projects/projectname.tar.gz)'), + ); + + $form['information'] = array( + '#prefix' => '', + '#markup' => 'Or', + '#suffix' => '', + ); + + $form['project_upload'] = array( + '#type' => 'file', + '#title' => t('Upload a module or theme'), + '#description' => t('Upload a Drupal module or theme (in .tar.gz format) to install it.'), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Install'), + ); + + return $form; +} + +function update_install_form_validate($form, &$form_state) { + if (!($form_state['values']['project_url'] XOR !empty($_FILES['files']['name']['project_upload']))) { + form_set_error('project_url', t('Unable to continue, please provide a url or upload a module / theme')); + return; + //$data = drupal_http_request($form_state['values']['url']); + } +} + +function update_install_form_submit($form, &$form_state) { + global $base_url; + + if ($form_state['values']['project_url']) { + $field = 'project_url'; + $local_cache = update_get_file($form_state['values']['project_url']); + if (!$local_cache) { + form_set_error($field, t('Unable to retreive Drupal project from %url', array('%url' => $form_state['values']['project_url']))); + return; + } + } + elseif ($_FILES['files']['name']['project_upload']) { + $field = 'project_upload'; + // @todo: add some validators here. + $finfo = file_save_upload($field, array(), NULL, FILE_EXISTS_REPLACE); + // @todo: find out if the module is already instealled, if so, throw an error. + $local_cache = $finfo->uri; + } + + watchdog('update', 'Un-tarring ' . drupal_realpath($local_cache)); + + $a = new Archive_Tar(drupal_realpath($local_cache)); + + $files = $a->listContent(); + if (!$files) { + form_set_error($field, t('Provided URL is not a .tar.gz archive', array('%url' => $form_state['values']['url']))); + return; + } + + $project = drupal_substr($files[0]['filename'],0,-1); // Unfortunately, we can only use the directory name for this. :( + + $project_location = DRUPAL_ROOT . '/' . file_directory_path('temporary') . '/update-extraction/' . $project; + update_untar(drupal_realpath($local_cache)); + + $updater = Updater::factory($project_location); + $project_title = Updater::getProjectTitle($project_location); + + if (!$project) { + form_set_error($field, t('Unable to determine %project name', array('%project' => $project_title))); + } + + if ($updater->isInstalled()) { + form_set_error($field, t('%project is already installed', array('%project' => $project_title))); + return; + } + + $operations = array(); + $operations[] = array( + 'update_batch_copy_project', + array( + $project, + $local_cache, + ), + ); + + $batch = array( + 'title' => t('Installing %project', array('%project' => $project_title)), + 'init_message' => t('Preparing update operation'), + 'operations' => $operations, + 'finished' => 'update_batch_finished', + 'file' => drupal_get_path('module', 'update') . '/update.admin.inc', + ); + + $_SESSION['plugin_op'] = $batch; + drupal_goto(url($base_url . '/plugin.php', array('absolute' => TRUE))); + return; + +} + +/** + * Batch operations related to installing / updating projects + */ + +/** + * Batch operation: download a project and put it in a temporary cache. + * + * @param string $project name of the project being installed + * @param array &$context BatchAPI storage + * + * @return void; + */ +function update_batch_get_project($project, &$context) { + if (!isset($context['results']['log'])) { + $context['results']['log'] = array(); + } + if (!isset($context['results']['log'][$project])) { + $context['results']['log'][$project] = array(); + } + if (!isset($context['sandbox']['started'])) { + $context['sandbox']['started'] = TRUE; + $context['message'] = t('Downloading %project', array('%project' => $project)); + $context['finished'] = 0; + return; + } + $latest_version = _update_get_recommended_version($project); + if ($local_cache = update_get_file($latest_version['download_link'])) { + watchdog('update', 'Downloaded %project to %local_cache', array('%project' => $project, '%local_cache' => $local_cache)); + } + else { + $context['success'] = FALSE; + $content['results'][$project][] = t('Failed to download %project', array('%project' => $project)); + } +} + + +/** + * Batch operation: copy a project to it's proper place. + * For updates, will locate the current project and replace it. + * For new installs, will download and try to determine the type from the info file + * and then place it variable_get(update_default_{$type}_location) i.e. update_default_module_location(). + * + * @todo Fix the $project param (refactor) + * @param string $project Either name of the project being installed or a + * @param string $url Location of a tarball to install if recommended version of $project not required + * @param string $filetransfer FileTransfer class + * @param array &$context BatchAPI storage + * + * @return void + */ +function update_batch_copy_project($project, $url, $filetransfer, &$context) { + + // Initialize some variables + if (!isset($context['results']['log'])) { + $context['results']['log'] = array(); + } + + if (!$context['results']['tasks']) { + $context['results']['tasks'] = array(); + } + + /** + * Unfortuantely, because the batch API uses a session and a connection + * pointer will be lost between requests, when doing an update of multiple + * modules, we need to unset the connection pointer to re-init a connect. + */ + unset($filetransfer->connection); + + if (!isset($context['results']['log'][$project])) { + $context['results']['log'][$project] = array(); + } + + if (!empty($context['results']['log'][$project]['#abort'])) { + $context['#finished'] = 1; + return; + } + + $local_cache = update_get_file($url); + + // This extracts the file into the standard place. + try { + update_untar($local_cache); + } + catch (Exception $e) { + _update_batch_create_message($context['results']['log'][$project], $e->getMessage(), FALSE); + $context['results']['log'][$project]['#abort'] = TRUE; + return; + } + + $project_source_dir = DRUPAL_ROOT . '/' . file_directory_path('temporary') . '/update-extraction/' . $project; + $updater = Updater::factory($project_source_dir); + + try { + if ($updater->isInstalled()) { + // This is an update. + $tasks = $updater->update($filetransfer); + } + else { + $tasks = $updater->install($filetransfer); + } + } + catch (UpdaterError $e) { + _update_batch_create_message($context['results']['log'][$project], t("Error installing / updating)"), FALSE); + $context['results']['log'][$project]['#abort'] = TRUE; + return; + } + + _update_batch_create_message($context['results']['log'][$project], t('Installed %project_name successfully', array('%project_name' => $project))); + $context['results']['tasks'] += $tasks; + + $context['finished'] = 1; +} + +/** + * Set the batch to run the update functions. + */ +function update_batch_update_project($project, &$context) { + + if (!empty($context['results']['log'][$project]['#abort'])) { + $context['#finished'] = 1; + return; + } + + if (_update_get_project_type($project) != 'module') { + $context['finished'] = 1; + return; + } + $operations = array(); + require_once './includes/update.inc'; + + foreach (_update_get_schema_updates($project) as $update) { + update_do_one($project, $update, $context); + } +} + +/** + * Batch callback for when the batch is finished. + */ +function update_batch_finished($success, $results) { + foreach ($results['log'] as $module => $messages) { + if (!empty($messages['#abort'])) { + $success = FALSE; + } + } + $_SESSION['update_batch_results']['success'] = $success; + $_SESSION['update_batch_results']['messages'] = $results['log']; + $_SESSION['update_batch_results']['tasks'] = $results['tasks']; +} + +/** + * + * Very stupid function to provide compatibility with update.php's + * silly functions + * Some better error handling is needed, but batch API doesn't seem to support any. + * + * @param array $project_results + * @param string $message + * @param bool $success + */ +function _update_batch_create_message(&$project_results, $message, $success = TRUE) { + $next_number = count($project_results) + 1; + $project_results[$next_number] = array(array('query' => $message, 'success' => $success)); +} + +/** + * Theme main updates page. + * + * @ingroup themeable + */ +function theme_update_available_updates_form($form) { + $last = variable_get('update_last_check', 0); + $output = '' . t('Here you can find information about available updates for your installed modules and themes. Note that each module or theme is part of a "project", which may or may not have the same name, and might include multiple modules or themes within it.') . '
'; - $output .= '' . t('To extend the functionality or to change the look of your site, a number of contributed modules and themes are available.', array('@modules' => 'http://drupal.org/project/modules', '@themes' => 'http://drupal.org/project/themes')) . '
'; - $output .= '' . t('Each time Drupal core or a contributed module or theme is updated, it is important that update.php is run.', array('@update-php' => url($base_url . '/update.php', array('external' => TRUE)))) . '
'; + + return $output; + + case 'admin/reports/updates/install': + global $base_url; + $help = <<' . t($help, array('@module_url' => 'http://drupal.org/project/modules', '@theme_url' => 'http://drupal.org/project/themes')) . '
'; + return $output; case 'admin/appearance': case 'admin/config/modules': @@ -127,18 +139,48 @@ function update_menu() { $items = array(); $items['admin/reports/updates'] = array( - 'title' => 'Available updates', - 'description' => 'Get a status report about available updates for your installed modules and themes.', - 'page callback' => 'update_status', + 'title' => 'Updates and Add-ons', + 'description' => 'Keep your installed modules and themes up to date and install new ones.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('update_update_form'), 'access arguments' => array('administer site configuration'), 'weight' => 10, - 'file' => 'update.report.inc', + 'type' => MENU_NORMAL_ITEM, + 'file' => 'update.admin.inc', ); - $items['admin/reports/updates/list'] = array( - 'title' => 'List', + + $items['admin/reports/updates/updater'] = array( + 'title' => 'Update', + 'description' => 'Update your installed modules and themes', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('update_update_form'), 'access arguments' => array('administer site configuration'), + 'weight' => 10, 'type' => MENU_DEFAULT_LOCAL_TASK, + 'file' => 'update.admin.inc', + ); + + $items['admin/reports/updates/full'] = array( + 'title' => 'Full update report', + 'description' => 'A comprehensive list of all modules installed on your system and their status.', + 'page callback' => 'update_status', + 'access arguments' => array('administer site configuration'), + 'weight' => 30, + 'type' => MENU_LOCAL_TASK, + 'file' => 'update.report.inc', + ); + + $items['admin/update'] = array( + 'title' => 'Updating your site', + 'description' => 'Second step of site update / new project install', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('update_update_form'), + 'access arguments' => array('administer site configuration'), + 'weight' => 10, + 'type' => MENU_CALLBACK, + 'file' => 'update.admin.inc', ); + $items['admin/reports/updates/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', @@ -146,7 +188,9 @@ function update_menu() { 'access arguments' => array('administer site configuration'), 'file' => 'update.settings.inc', 'type' => MENU_LOCAL_TASK, + 'weight' => 50, ); + $items['admin/reports/updates/check'] = array( 'title' => 'Manual update check', 'page callback' => 'update_manual_status', @@ -155,16 +199,42 @@ function update_menu() { 'file' => 'update.fetch.inc', ); + $items['admin/config/modules/install'] = array( + 'title' => 'Install', + 'description' => 'Install new modules and themes', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('update_install_form'), + 'access callback' => 'update_plugin_manager_access', + 'access arguments' => array(), + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'file' => 'update.admin.inc', + ); + + $items['admin/appearance/install'] = array( + 'title' => 'Install', + 'description' => 'Install', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('update_install_form'), + 'access callback' => 'update_plugin_manager_access', + 'access arguments' => array(), + 'weight' => 20, + 'type' => MENU_LOCAL_TASK, + 'file' => 'update.admin.inc', + ); + return $items; } + /** - * Implement the hook_theme() registry. + * Implements hook_theme(). */ function update_theme() { return array( - 'update_settings' => array( + 'update_available_updates_form' => array( 'arguments' => array('form' => NULL), + 'file' => 'update.admin.inc', ), 'update_report' => array( 'arguments' => array('data' => NULL), @@ -645,6 +715,12 @@ function update_flush_caches() { return array(); } + +function update_plugin_manager_access() { + return variable_get('plugin_manager_enabled', TRUE); +} + + /** * @} End of "defgroup update_status_cache". */ diff --git a/modules/update/updaters.inc b/modules/update/updaters.inc new file mode 100644 index 0000000..1c7c5e7 --- /dev/null +++ b/modules/update/updaters.inc @@ -0,0 +1,392 @@ +source = $source; + $this->name = self::getProjectName($source); + $this->title = self::getProjectTitle($source); + } + + /** + * Returns an Updater of the approiate type depending on the source provided. + * + * @param string $source Directory of a Drupal project + * @return object Child of Updater + */ + static function factory($source) { + if (is_dir($source)) { + $updater = self::getUpdaterFromDirectory($source); + } + else { + throw new UpdaterError('Unable to determine the type of the source directory'); + } + return new $updater($source); + } + + /** + * Determines the project type (and assocaited updater class) + * from the available Updater child classes. + * + * @param string $directory Extracted Drupal project. + * @return string The class name which can work with this project type. + */ + static function getUpdaterFromDirectory($directory) { + // Gets a list of possible implementing classes + $updaters = update_get_updaters(); + foreach ($updaters as $updater) { + if (call_user_func("{$updater}::canUpdateDirectory", $directory)) { + return $updater; + } + } + throw new UpdaterError("Cannot determine the type of project"); + } + + /** + * Since there is no enforcement of which info file is the project's info file + * This will get one with the same name as the directory, or the first one it + * finds. Sucks, yes. But needs a larger solution. + * + * @param string $directory + * Directory to search in + * @return string + * Path to the info file + */ + public static function findInfoFile($directory) { + $info_files = file_scan_directory($directory, '/.*\.info/'); + if (!$info_files) { + return FALSE; + } + foreach ($info_files as $info_file) { + if (drupal_substr($info_file->filename, 0, -5) == basename($directory)) { + // Has the same name as the directory, is prefered. + return $info_file->uri; + } + } + // Return the first one. + $info_file = array_shift($info_files); + return $info_file->uri; + } + + /** + * Gets the name of the project directory (basename). + * + * @param string $directory + * @return string + */ + public static function getProjectName($directory) { + return basename($directory); + } + + + /** + * From the info file of a project, gets the project name + * + * @param string $directory + * Directory to search for the info file. + * @return string + */ + public static function getProjectTitle($directory) { + $info_file = self::findInfoFile($directory); + $info = drupal_parse_info_file($info_file); + if (!$info) { + //@TODO: Add the variables, t(), etc + throw new UpdaterError("Unable to parse info file"); + } + return $info['name']; + } + + /** + * Stores default parameters for the Updater. + * + * @param array $overrides + * An array of overrides. + * @return array + * An array of configuration parameters for an update or install operation + */ + private function getInstallArgs($overrides = array()) { + $args = array( + 'make_backup' => FALSE, + 'install_dir' => $this->getInstallDirectory(), + 'backup_dir' => $this->getBackupDir(), + ); + return array_merge($args, $overrides); + } + + /** + * Updates a Drupal project, returns a list of next actions + * + * @param FileTransfer $ft + * Object which is a child of FileTransfer + * @param array $overrides + * An array of settings to override defaults + * @see self::getInstallArgs + * @return array + * An array of links which the user may need to complete the update + */ + public function update(&$ft, $overrides = array()) { + try { + // Establish arguments with possible overrides. + $args = $this->getInstallArgs($overrides); + + // Take a Backup. + if ($args['make_backup']) { + $this->makeBackup($args['install_dir'], $args['backup_dir']); + } + + if (!$this->name) { + //This is bad, don't want delte the install dir. + throw new UpdaterError("Fatal error in update, cowardly refusing to wipe out the install directory"); + } + + // Remove the existing installed file. + $ft->removeDirectory($args['install_dir'] . '/' . $this->name); + // Copy the directory in place. + $ft->copyDirectory($this->source, $args['install_dir']); + // Run the updates + // @TODO: decide if we want to implement this. + $this->postUpdate(); + // For now, just return a list of links of things to do. + return $this->postUpdateTasks(); + } catch (FileTransferException $e) { + throw new UpdaterError(t("File Transfer failed, reason: !reason", array ('!reason' => t($e->getMessage(), $this->arguments)))); + } + } + + /** + * Installs a Drupal project, returns a list of next actions + * + * @param FileTransfer $ft + * Object which is a child of FileTransfer + * @param array $overrides + * An array of settings to override defaults + * @see self::getInstallArgs + * @return array + * An array of links which the user may need to complete the install + */ + public function install(&$ft, $overrides = array()) { + try { + // Establish arguments with possible overrides. + $args = $this->getInstallArgs($overrides); + // Copy the directory in place. + $ft->copyDirectory($this->source, $args['install_dir']); + // Potentially enable something? + // @TODO: decide if we want to implement this. + $this->postInstall(); + // For now, just return a list of links of things to do. + return $this->postInstallTasks(); + } + catch (FileTransferException $e) { + throw new UpdateError(t("File Transfer failed, reason: !reason", array ('!reason' => t($e->getMessage(), $this->arguments)))); + } + } + + function makeBackup(&$ft, $from, $to) { + //@TODO: Not implemented + } + + function getBackupDir() { + return file_directory_path('temporary'); + } + + /** + * Needs to be overridden by children to work + * Actions to take after the update is complete. + */ + function postUpdate() { + + } + + /** + * Needs to be overridden by children to work + * Actions to take after the install is complete. + */ + function postInstall() { + + } + + /** + * + * @return array Links which provide actions to take after the install is finished. + */ + function postInstallTasks() { + return array(); + } + + /** + * + * @return array Links which provide actions to take after the update is finished. + */ + function postUpdateTasks() { + return array(); + } +} + +class ModuleUpdater extends Updater implements DrupalProjectUpdater{ + + static function getInstallDirectory() { + return DRUPAL_ROOT . '/' . conf_path() . '/modules'; + } + + function isInstalled() { + return (bool) drupal_get_path('module', $this->name); + } + + function installProject() { + + } + + static function canUpdateDirectory($directory) { + if (file_scan_directory($directory, '/.*\.module/')) { + return TRUE; + } + return FALSE; + } + + function getSchemeUpdates() { + require_once './includes/install.inc'; + require_once './includes/update.inc'; + + if (_update_get_project_type($project) != 'module') { + return array(); + } + module_load_include('install', $project); + + if (!$updates = drupal_get_schema_versions($project)) { + return array(); + } + $updates_to_run = array(); + $modules_with_updates = update_get_update_list(); + if ($updates = $modules_with_updates[$project]) { + if ($updates['start']) { + return $updates['pending']; + } + } + return array(); + } + + /** + * @return array Links which provide actions to take after the install is finished. + */ + function postInstallTasks() { + + return array ( + l(t('Enable newly added modules in !project', array('!project' => $this->title)), 'admin/config/modules'), + ); + } + +} + +class ThemeUpdater extends Updater implements DrupalProjectUpdater{ + public $installDirectory; + + static function getInstallDirectory() { + return DRUPAL_ROOT . '/' . conf_path() . '/themes'; + } + + function isInstalled() { + return (bool) drupal_get_path('theme', $this->name); + } + + function installProject() { + + } + + static function canUpdateDirectory($directory) { + // This is a shitty test, but don't know how else to confirm + if (file_scan_directory($directory, '/.*\.module/')) { + return FALSE; + } + return TRUE; + } + + function postInstall() { + // Update the system table. + system_get_theme_data(); + + // Active the theme + db_update('system') + ->fields(array('status' => 1)) + ->condition('type', 'theme') + ->condition('name', $this->name) + ->execute(); + } + + function postInstallTasks() { + return array ( + l(t('Set the !project theme as default', array('!project' => $this->title)), 'admin/appearance'), + ); + } + + function postUpdateTasks() { + // If there are schema updates + return array ( + l(t('Run database updates for !project', array('!project' => $this->title)), 'update.php'), + ); + } +} + +/** + * @TODO: implement this. + */ +class ProfileUpdater extends Updater { + function installProject() { + + } +} + +class UpdaterError extends Exception { + +} +?> diff --git a/plugin.php b/plugin.php new file mode 100644 index 0000000..e93b427 --- /dev/null +++ b/plugin.php @@ -0,0 +1,134 @@ +uid != 1) { + print theme('update_page', $output, !$progress_page); + exit; + } + global $base_url; + + //module_list(TRUE, FALSE, $module_list); + + // 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. + ini_set('display_errors', TRUE); + + + drupal_load_updates(); + + update_fix_d7_requirements(); + update_fix_compatibility(); + + if (isset($_SESSION['update_batch_results']) && $results = $_SESSION['update_batch_results']) { + // If this variable is set it means that the user entered FTP details because + // they needed to chmod the sites/default/* dirs. We want to set them back. + if (isset($_SESSION['filetransfer_settings'])) { + $filetransfer_settings = $_SESSION['filetransfer_settings']; + + // This contains the user's sensative credentials, we have to make sure + // that it gets unset. + unset($_SESSION['filetransfer_settings']); + + $filetransfer_backend = variable_get('update_filetransfer_default', NULL); + $ft = update_get_filetransfer($filetransfer_backend, $filetransfer_settings); + // This sets the permissions to non-writable for everyone. + _plugin_manager_create_and_setup_directories($ft, 0755); + } + + //Clear the session out; + unset($_SESSION['update_batch_results']); + + //Add the update.php functions and css + include_once './includes/update.inc'; + drupal_add_css('modules/system/maintenance.css'); + + if ($results['success']) { + variable_set('site_offline', FALSE); + drupal_set_message(t("Update was completed successfully! Your site has been taken out of maintenance mode.")); + } + else { + drupal_set_message(t("Update failed! See the log below for more information. Your site is still in maintenance mode"), 'error'); + } + + $output = ""; + $output .= get_plugin_report($results['messages']); + drupal_set_title('Update complete'); + + $links = array(); + if (is_array($results['tasks'])) { + $links += $results['tasks']; + } + + + $links = array_merge($links, array( + l('Administration pages', 'admin'), + l('Front page', '