Index: modules/system/system.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v
retrieving revision 1.158
diff -u -p -r1.158 system.admin.inc
--- modules/system/system.admin.inc	19 Jun 2009 20:35:05 -0000	1.158
+++ modules/system/system.admin.inc	26 Jun 2009 02:18:58 -0000
@@ -1082,6 +1082,174 @@ function system_modules_uninstall_submit
 }
 
 /**
+ * Menu callback for the module/theme installer.
+ */
+function system_installer() {
+  $errors = array();
+  $data_set = array();
+
+  $modules = empty($_GET['modules']) ? array() : $_GET['modules'];
+  $existing_modules = system_get_module_data();
+  $md5s = empty($_GET['md5s']) ? array() : $_GET['md5s'];
+  list($errors['modules'], $data_set['modules']) = _system_installer_data($modules, $existing_modules, $md5s, 'module');
+
+  $themes = empty($_GET['themes']) ? array() : $_GET['themes'];
+  $existing_themes = system_get_theme_data();
+  list($errors['themes'], $data_set['themes']) = _system_installer_data($themes, $existing_themes, $md5s, 'theme');
+
+  if (empty($data_set['modules']) && empty($data_set['themes'])) {
+    $errors['modules'][0][] = t('You must choose at least one module or theme.');
+  }
+
+  foreach ($errors as $type => $objects) {
+    foreach ($objects as $name => $error_set) {
+      foreach ($error_set as $error) {
+        drupal_set_message($error, 'error');
+      }
+    }
+  }
+  return drupal_get_form('system_installer_form', $data_set, $errors);
+}
+
+function _system_installer_data($plugins, $existing_plugins, $md5s, $type) {
+  foreach ($plugins as $plugin => $release) {
+    if (isset($existing_plugins[$plugin])) {
+      $errors[$plugin]['install'] = t('The %plugin @type is already available on the <a href="@url">@types page</a>.', array('%plugin' => $existing_plugins[$plugin]->info['name'], '@url' => url('admin/build/' . $type .'s'), '@type' => $type));
+      $data_set[$plugin] = array(array('title' => $existing_plugins[$plugin]->info['name']), $existing_plugins[$plugin]->info['version']);
+    }
+    else {
+      $project = array(
+        'name' => $plugin,
+        'info' => array(
+          'version' => $release,
+        ),
+      );
+      drupal_function_exists('_update_build_fetch_url');
+      $url = _update_build_fetch_url($project);
+      $result = drupal_http_request($url);
+      $data = @update_parse_xml(array($result->data));
+      if (empty($data[$plugin]) || empty($data[$plugin]['releases']) || empty($data[$plugin]['releases'][$release])) {
+        $errors[$plugin]['invalid'] = t('Could not find the %plugin @type.', array('%plugin' => $plugin, '@type' => $type));
+        $data_set[$plugin] = array(array('title' => $plugin), $release);
+        continue;
+      }
+      else {
+        $data_set[$plugin] = array($data[$plugin], $release);
+      }
+      $mdhash = isset($data[$plugin]['releases'][$release]['mdhash']) ? $data[$plugin]['releases'][$release]['mdhash'] : FALSE;
+      if (!isset($md5s[$plugin]) || !$mdhash) {
+        $errors[$plugin]['md5'] = t('No md5 checksum was provided for %plugin.', array('%plugin' => $data[$plugin]['title']));
+      }
+      elseif ($mdhash != $md5s[$plugin]) {
+        $errors[$plugin]['md5'] = t('Incorrect md5 checksum was provided for %plugin.', array('%plugin' => $data[$plugin]['title']));
+      }
+    }
+  }
+
+  return array($errors, $data_set);
+}
+
+/**
+ * Forms API callback for the installer page.
+ */
+function system_installer_form($form_state, $data_set, $errors) {
+  $form = array();
+
+  $form['data'] = array(
+    '#type' => 'value',
+    '#value' => $data_set,
+  );
+
+  $form['#errors'] = $errors;
+
+  foreach (array('themes', 'modules') as $type) {
+    if (!empty($data_set[$type])) {
+      foreach ($data_set[$type] as $plugin => $data) {
+        $form[$type][$plugin] = array('#tree' => TRUE);
+        if (isset($errors[$type][$plugin])) {
+          // Set the "fetch" element to FALSE because an error occurred and the
+          // module or theme should not be installed.
+          $form[$type][$plugin]['fetch'] = array(
+            '#type' => 'value',
+            '#value' => FALSE,
+          );
+          $form[$type][$plugin]['error'] = array(
+            '#markup' => theme('image', 'misc/watchdog-error.png'),
+          );
+        }
+        else {
+          $form[$type][$plugin]['fetch'] = array(
+            '#type' => 'checkbox',
+            '#default_value' => TRUE,
+          );
+        }
+      }
+    }
+  }
+
+  // @TODO: Create a reusable function to create a list of radios/settings for
+  // individual backends that can be used both for this form and the update
+  // form.
+  $form['file_transfer'] = array();
+
+  $form['install'] = array(
+    '#type' => 'submit',
+    '#value' => t('Install'),
+  );
+
+  return $form;
+}
+
+/**
+ * Theming callback for system_installer_form().
+ */
+function theme_system_installer_form($form) {
+  $output = '';
+
+  $output .= '<p>' . t('The following modules and themes are about to be installed on the site. Please take a moment to look this over and make sure everything is correct.') . '</p>';
+
+  $data_set = $form['data']['#value'];
+  $errors = $form['#errors'];
+
+  foreach (array('modules', 'themes') as $type) {
+    if (!empty($data_set['modules'])) {
+      $title = ($type == 'modules' ? t('Modules') : t('Themes'));
+      $output .= '<h3>' . $title . '</h3>';
+
+      $rows = array();
+      foreach ($data_set[$type] as $plugin => $data) {
+        list($data, $release) = $data;
+        $row = array();
+        if (!isset($form['#errors'][$type][$plugin]) || isset($form['#errors'][$type][$plugin]['md5'])) {
+          $row[] = isset($form['#errors'][$type][$plugin]['md5']) ? drupal_render($form[$type][$plugin]['error']) : drupal_render($form[$type][$plugin]['fetch']);
+          $row[] = l($data['title'], $data['link']);
+          $row[] = l($release, $data['releases'][$release]['release_link']);
+          $row[] = format_date($data['releases'][$release]['date']);
+          $row[] = isset($form['#errors'][$type][$plugin]) ? theme('image', 'misc/watchdog-error.png') : theme('image', 'misc/watchdog-ok.png');
+          $rows[] = isset($form['#errors'][$type][$plugin]) ? array('data' => $row, 'class' => 'error') : $row;
+        }
+        else {
+          $row[] = drupal_render($form[$type][$plugin]['error']);
+          $row[] = $data['title'];
+          $row[] = $release;
+          $row[] = t('Unknown');
+          $row[] = theme('image', 'misc/watchdog-error.png');
+          $rows[] = array(
+            'data' => $row,
+            'class' => 'error',
+          );
+        }
+      }
+      $output .= theme('table', array(t('Fetch'), $title, t('Version'), t('Released'), t('MD5 sum confirmed')), $rows);
+    }
+  }
+
+  $output .= drupal_render_children($form);
+
+  return $output;
+}
+
+/**
  * Menu callback. Display blocked IP addresses.
  */
 function system_ip_blocking() {
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.715
diff -u -p -r1.715 system.module
--- modules/system/system.module	23 Jun 2009 12:11:19 -0000	1.715
+++ modules/system/system.module	25 Jun 2009 04:44:11 -0000
@@ -154,6 +154,10 @@ function system_theme() {
       'arguments' => array('form' => NULL),
       'file' => 'system.admin.inc',
     ),
+    'system_installer_form' => array(
+      'arguments' => array('form' => NULL),
+      'file' => 'system.admin.inc',
+    ),
     'status_report' => array(
       'arguments' => array('requirements' => NULL),
       'file' => 'system.admin.inc',
@@ -597,6 +601,16 @@ function system_menu() {
     'type' => MENU_CALLBACK,
   );
 
+  $items['admin/build/install'] = array(
+    'title' => 'Install',
+    'page callback' => 'system_installer',
+    // Note that, while anyone with "administer site configuration" permissions
+    // can access the page, only individuals with file transfer credentials can
+    // actually proceed to install modules and themes from drupal.org.
+    'access arguments' => array('administer site configuration'),
+    'type' => MENU_CALLBACK,
+  );
+
   // Development menu category.
   $items['admin/development'] = array(
     'title' => 'Development',
