Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.24
diff -u -p -r1.24 system.api.php
--- modules/system/system.api.php	10 Mar 2009 16:08:43 -0000	1.24
+++ modules/system/system.api.php	22 Mar 2009 19:02:38 -0000
@@ -12,6 +12,52 @@
  */
 
 /**
+ * Define additional backends to be supported for installing/updating modules
+ * and themes from drupal.org.
+ *
+ * By default, ftp and ssh are enabled if the appropriate PHP extensions are
+ * installed. These backends are handled by system.module. Any additional
+ * backends defined here should also define the following callbacks:
+ *  - "update_NAME_add_extension($files, $root, $host, $username, $password)":
+ *    NAME here corresponds to the keys of the return array; for this example,
+ *    this function would be btp_add_extension(). This function should install
+ *    the files specified by $files to the server specified by $host using the
+ *    credentials specified by $username and $password. $root specifies the
+ *    root Drupal directory, DRUPAL_ROOT by default, but user-alterable in
+ *    case, for example, an ftp user's root directory is different from the
+ *    system's. $type specifies the type of plugin this is; for example,
+ *    'module' or 'theme' depending on the type of resource. Should return TRUE
+ *    on success, or FALSE on failure.
+ *
+ *  - "update_NAME_remove_extension($files, $root $host, $username, $password)":
+ *    NAME here corresponds to the keys of the return array; for this example,
+ *    this function would be btp_remove_extension(). This function should
+ *    install the files specified by $files to the server specified by $host
+ *    using the credentials specified by $username and $password. $root
+ *    specifies the root Drupal directory, DRUPAL_ROOT by default, but user-
+ *    alterable in case, for example, an ftp user's root directory is different
+ *    from the system's. $type specifies the type of plugin this is; for
+ *    example, 'module' or 'theme' depending on the type of resource. Should
+ *    return TRUE on success, or FALSE on failure.
+ *
+ * @return
+ *   An associative array where the keys correspond to the internal name of the
+ *   backend, used for function calls, and the values correspond to the human
+ *   readable name of the backend.
+ */
+function hook_update_backend() {
+  $backends = array();
+
+  // BTP (Banana Transfer Protocol) will only be available if the function
+  // btp_unpeel() exists.
+  if (function_exists('btp_unpeel')) {
+    $backends['btp'] = t('BTP');
+  }
+
+  return $backends;
+}
+
+/**
  * Perform periodic actions.
  *
  * Modules that require to schedule some commands to be executed at regular
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.674
diff -u -p -r1.674 system.module
--- modules/system/system.module	20 Mar 2009 19:18:10 -0000	1.674
+++ modules/system/system.module	22 Mar 2009 19:02:39 -0000
@@ -228,6 +228,34 @@ function system_rdf_namespaces() {
 }
 
 /**
+ * Implementation of hook_update_backend().
+ */
+function system_update_backend() {
+  $backends = array();
+
+  // SSH2 lib backend is only available if the proper PHP extension is
+  // installed.
+  if (function_exists('ssh2_connect')) {
+    $backends['ssh'] = t('SSH');
+  }
+  // FTP backend is only available if the proper PHP extension is installed or
+  // allow_url_fopen is on.
+  // Only bother offering FTP as an option if SSH isn't available. The SSH
+  // extension is rare enough that we can be fairly confident that if it is
+  // available, it is here expressly for this purpose.
+  elseif (function_exists('ftp_connect')) {
+    $backends['ftp_extension'] = t('FTP');
+  }
+  // Only present the ftp wrapper option as a fallback if the ftp extension
+  // isn't available.
+  elseif (ini_get('allow_url_fopen')) {
+    $backends['ftp_wrapper'] = t('FTP');
+  }
+
+  return $backends;
+}
+
+/**
  * Implementation of hook_elements().
  */
 function system_elements() {
Index: modules/update/update.ftp_extension.inc
===================================================================
RCS file: modules/update/update.ftp_extension.inc
diff -N modules/update/update.ftp_extension.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.ftp_extension.inc	22 Mar 2009 02:12:45 -0000
@@ -0,0 +1,165 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * The FTP Backend for the plugin manager.
+ */
+
+/**
+ * Install the supplied files to the appropriate locations.
+ *
+ * @param $files
+ *   An array of files to be added by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_extension_add_extension($files, $root, $host, $username, $password) {
+  // Connect to the local ftp server, parsing for non-standard port.
+  $parts = explode(':', $host);
+  if (count($parts) == 2) {
+    $connect = ftp_connect($parts[0], (int)$parts[1]);
+  }
+  else {
+    $connect = ftp_connect($host);
+  }
+  if (!$connect) {
+    drupal_set_message(t('No ftp server could be found.'), 'error');
+    return FALSE;
+  }
+
+  // Login to the local ftp server.
+  if (!@ftp_login($connect, $username, $password)) {
+    drupal_set_message(t('Could not login to the ftp server.'), 'error');
+    return FALSE;
+  }
+
+  if (!@ftp_chdir($connect, $root)) {
+    drupal_set_message(t('Your provided drupal install directory is invalid.'),  'error');
+    return FALSE;
+  }
+
+  // Prepare the directories to use later.
+  $extract_directory = file_directory_temp() .'/update_extraction/';
+  // Process each of the files.
+  foreach ($files as $index => $file) {
+    ftp_chdir($connect, '/');
+    if (trim($file, '\\/') != $file) {
+      if (!@ftp_mkdir($connect, "$root/$file") && !@ftp_chdir($connect, "$root/$file")) {
+        drupal_set_message(t('Unable to create directory @directory.', array('@directory' => $file)),'error');
+        return FALSE;
+      }
+    }
+    else {
+      if (!@ftp_put($connect, "$root/$file", $extract_directory . $file, FTP_BINARY)) {
+        drupal_set_message(t('Unable to upload @file.', array('@file' => $file)), 'error');
+        return FALSE;
+      }
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Remove the supplied files.
+ *
+ * @param $files
+ *   An array of files to be removed by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_extension_remove_extension($files, $root, $host, $username, $password) {
+  // Connect to the local ftp server.
+  $connect = ftp_connect($host);
+  if (!$connect) {
+    drupal_set_message(t('No ftp server could be found.'), 'error');
+    return FALSE;
+  }
+
+  // Login to the local ftp server.
+  if (!@ftp_login($connect, $username, $password)) {
+    drupal_set_message(t('Could not login to the ftp server.'), 'error');
+    return FALSE;
+  }
+
+  // Try to guess the ftp address for the drupal install.
+  // Start by putting the entire address together and replacing \ with /.
+  $local_path = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT'] . base_path());
+
+  // Grab the last on off the end and explode it.
+  $local_path = explode('/', rtrim($local_path, '/'));
+
+  // If we can't change to the proper directory, abort.
+  if (!@ftp_chdir($connect, $root)) {
+    drupal_set_message(t('Could not reach the proper directory.'), 'error');
+    return FALSE;
+  }
+  ftp_chdir($connect, '/');
+
+  // Process each of the files.
+  foreach ($files as $file) {
+    if (trim($file, '\\/') != $file) {
+      if (!update_ftp_extension_remove_extension_rmdir($connect, "$root/$file")) {
+        return FALSE;
+      }
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Helper function for ftp_remove_extension_library().
+ *
+ * @param $connect
+ *   The FTP connection to use to remove the directory.
+ * @param $directory
+ *   The directory to remove.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_extension_remove_extension_rmdir($connect, $directory){
+  $pwd = ftp_pwd($connect);
+  if (!@ftp_chdir($connect, $directory)) {
+    return FALSE;
+  }
+  $list = @ftp_nlist($connect, '.');
+  foreach ($list as $item){
+    if ($item == '.' || $item == '..') {
+      continue;
+    }
+    if (@ftp_chdir($connect, $item)){
+      ftp_chdir($connect, '..');
+      if (!update_ftp_extension_remove_extension_rmdir($connect, $item)) {
+        return FALSE;
+      }
+    }
+    elseif (!ftp_delete($connect, $item)) {
+      return FALSE;
+    }
+  }
+  ftp_chdir($connect, $pwd);
+  if (!ftp_rmdir($connect, $directory)) {
+    return FAlSE;
+  }
+  return TRUE;
+}
\ No newline at end of file
Index: modules/update/update.ftp_wrapper.inc
===================================================================
RCS file: modules/update/update.ftp_wrapper.inc
diff -N modules/update/update.ftp_wrapper.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.ftp_wrapper.inc	22 Mar 2009 02:12:53 -0000
@@ -0,0 +1,132 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * The FTP Backend for the plugin manager.
+ */
+
+/**
+ * Install the supplied files to the appropriate locations.
+ *
+ * @param $files
+ *   An array of files to be added by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_wrapper_add_extension($files, $root, $host, $username, $password) {
+  // Write the common part of the url.
+  $ftp_base_directory = 'ftp://' . urlencode($username) . ':' . urlencode($password) . "@$host/";
+
+  if (!@is_dir($ftp_base_directory)) {
+    drupal_set_message(t('The supplied username/password combination was not accepted, or no local ftp server is running.'), 'error');
+    return FALSE;
+  }
+
+  $ftp_path = $ftp_base_directory . $root;
+
+  // If it's not a valid path, then quit.
+  if (!is_dir($ftp_path)) {
+    drupal_set_message(t('Could not find the ftp directory for drupal.'), 'error');
+    return FALSE;
+  }
+
+  // Prepare the directories to use later.
+  $extract_directory = file_directory_temp() . '/update_extraction/';
+
+  // Process each of the files.
+  foreach ($files as $index => $file) {
+    if (trim($file, "\\/") != $file) {
+      @mkdir("$ftp_path/$file");
+    }
+    else {
+      @copy($extract_directory . $file, "$ftp_path/$file");
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Remove the supplied files.
+ *
+ * @param $files
+ *   An array of files to be removed by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_wrapper_remove_extension($files, $root, $host, $username, $password) {
+  // Write the common part of the url
+  $ftp_base_dir = 'ftp://' . urlencode($username) . ':' . urlencode($password) . "@$host/";
+
+  if (!@is_dir($ftp_base_dir)) {
+    drupal_set_message(t('The supplied username/password combination was not accepted, or no ftp server is running.'), 'error');
+    return FALSE;
+  }
+
+  $ftp_path = $ftp_base_dir . $root;
+  // If we the FTP path is not valid, abort.
+  if (!is_dir($ftp_path)) {
+    drupal_set_message(t('Could not reach the proper directory.'), 'error');
+    return FALSE;
+  }
+  // Process each of the files.
+  foreach ($files as $file) {
+    if (trim($file, '\\/') != $file) {
+      if (!update_ftp_wrapper_remove_extension_rmdir("$ftp_path/$file")) {
+        return FALSE;
+      }
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Helper function for ftp_remove_extension_wrapper(). Removes a directory
+ * through the FTP stream.
+ *
+ * @param $target
+ *   The target folder to remove.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_wrapper_remove_extension_rmdir($target) {
+  if (is_dir($target)) {
+    $directory = opendir($target);
+    while (($resource = readdir($directory)) !== FALSE) {
+      if ($resource == '.' || $resource == '..') {
+        continue;
+      }
+      if (is_file($target . $resource)) {
+        unlink($target . $resource);
+      }
+      elseif (is_dir($target . $resource)) {
+        update_ftp_wrapper_remove_extension_rmdir($target . $resource . '/');
+      }
+    }
+    closedir($directory);
+    if (rmdir($target)) {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
\ No newline at end of file
Index: modules/update/update.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/update/update.info,v
retrieving revision 1.5
diff -u -p -r1.5 update.info
--- modules/update/update.info	11 Oct 2008 02:33:12 -0000	1.5
+++ modules/update/update.info	22 Mar 2009 19:02:39 -0000
@@ -4,9 +4,12 @@ description = Checks the status of avail
 version = VERSION
 package = Core
 core = 7.x
-files[] = update.module
 files[] = update.compare.inc
 files[] = update.fetch.inc
+files[] = update.ftp_extension.inc
+files[] = update.ftp_wrapper.inc
+files[] = update.install
+files[] = update.module
 files[] = update.report.inc
 files[] = update.settings.inc
-files[] = update.install
+files[] = update.ssh.inc
Index: modules/update/update.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/update/update.module,v
retrieving revision 1.30
diff -u -p -r1.30 update.module
--- modules/update/update.module	22 Jan 2009 03:11:54 -0000	1.30
+++ modules/update/update.module	22 Mar 2009 19:02:39 -0000
@@ -515,3 +515,216 @@ function _update_project_status_sort($a,
   $b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
   return $a_status - $b_status;
 }
+
+/**
+ * Attempts to get a file using drupal_http_request and to store it locally.
+ *
+ * @param $path
+ *   The URL of the file to grab.
+ * @return
+ *   On success the address the files was saved to, FALSE on failure.
+ */
+function update_get_file($path) {
+  // Get each of the specified files.
+  $parsed_url = parse_url($path);
+  $local = file_directory_temp() . '/update_cache/' . basename($parsed_url['path']);
+  if (!file_exists(file_directory_temp() . '/update_cache/')) {
+    mkdir(file_directory_temp() . '/update_cache/');
+  }
+
+  // Check the cache and download the file if needed.
+  if (!file_exists($local)) {
+    // $result->data is the actual contents of the downloaded file. This saves
+    // it into a local file, whose path is stored in $local. $local is stored
+    // relative to the drupal installation.
+    $result = drupal_http_request($path);
+    if ($result->code != 200 || !file_save_data($result->data, $local)) {
+      drupal_set_message(t('@remote could not be saved.', array('@remote' => $path)), 'error');
+      return FALSE;
+    }
+  }
+  return $local;
+}
+
+/**
+ * Untars a file.
+ *
+ * @param $file
+ *   The file to untar.
+ * @param $directory
+ *   The destination directory of the file; for example, 'sites/all/modules'.
+ * @return
+ *   An array containing the locations of the extracted files, or FALSE on
+ *   failure.
+ */
+function update_untar($file, $directory) {
+  $directory_parts = explode('/', $directory);
+  $directory = file_directory_temp() . '/update_extraction/';
+  if (!file_exists($directory)) {
+    mkdir($directory);
+  }
+  foreach ($directory_parts as $directory_part) {
+    $directory .= "/$directory_part";
+    if (!file_exists($directory)) {
+      mkdir($directory);
+    }
+  }
+  $file_safe = escapeshellarg($file);
+  $directory_safe  = escapeshellarg($directory);
+  $file_list = array();
+
+  // Try to use tar to extract the files.
+  if (function_exists('popen')) {
+    $handle = popen("tar -zvxf $file_safe -C $directory_safe", 'r');
+    while ($line = fgets($handle)) {
+      $file_list[] = trim($line);
+    }
+    pclose($handle);
+  }
+
+  // If tar returned something, then it is present, so return it.
+  if (!empty($file_list)) {
+    return $file_list;
+  }
+
+  return FALSE;
+}
+
+/**
+ * Get the information about each extension.
+ *
+ * @param $projects
+ *   The project or projects on which information is desired.
+ * @return
+ *   An array containing info on the supplied projects.
+ */
+function update_get_release_history($projects) {
+  $version = DRUPAL_CORE_COMPATIBILITY;
+  $results = array();
+
+  // If projects isn't an array, turn it into one.
+  if (!is_array($projects)) {
+    $projects = array($projects);
+  }
+
+  // Look up the data for every project requested.
+  foreach ($projects as $project) {
+    $file = drupal_http_request(UPDATE_DEFAULT_URL . "/$project/$version");
+    $xml = simplexml_load_string($file->data);
+
+    // If it failed, then quit.
+    if ($xml == FALSE) {
+      drupal_set_message(t('Downloading the release history failed for @project.', array('@project' => $project)), "error");
+      return FALSE;
+    }
+
+    // Get the title, release_link and download_link.
+    $results[$project]['title'] = (string)$xml->title;
+
+    // Get information about every release.
+    foreach ($xml->releases->release AS $release) {
+      $release_version = (string)$release->version;
+      $results[$project]['release'][] = array(
+        'release_link' => (string)$release->release_link,
+        'download_link' => (string)$release->download_link,
+        'date' => (string)$release->date,
+        'version' => $release_version,
+      );
+      $results[$project]['version'][] = $release_version;
+    }
+  }
+
+  // Order them and then return the results.
+  ksort($results);
+  return $results;
+}
+
+/**
+ * See if everything that is needed to use the extension manager is available.
+ *
+ * @return
+ *   TRUE if all of the required dependencies are available, FALSE otherwise.
+ */
+function update_extensions_runnable() {
+  // See if we have a way to untar the files.
+  $handle = popen("tar --version", "r");
+  if (!fgets($handle)) {
+    pclose($handle);
+    return FALSE;
+  }
+  pclose($handle);
+
+  // See if we have any available backends.
+  $backends = update_list_backends();
+  return !empty($backends);
+}
+
+/**
+ * Add an extension to the file system.
+ *
+ * @param $backend
+ *   The machine-readable name of the backend handling the addition to the file
+ *   system; for example, 'ftp'.
+ * @param $files
+ *   An array of files to be added by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_add_extension($backend, $files, $root, $host, $username, $password) {
+  $function = 'update_' . $backend . '_add_extension';
+  if (drupal_function_exists($function)) {
+    return $function($files, $root, $host, $username, $password);
+  }
+  return FALSE;
+}
+
+/**
+ * Remove an extension from the file system.
+ *
+ * @param $backend
+ *   The machine-readable name of the backend handling the removal from the
+ *   file system; for example, 'ftp'.
+ * @param $files
+ *   An array of files to be removed by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_remove_extension($backend, $files, $root, $host, $username, $password) {
+  $function = 'update_' . $backend . '_remove_extension';
+  if (drupal_function_exists($function)) {
+    return $function($files, $root, $host, $username, $password);
+  }
+  return FALSE;
+}
+
+/**
+ * Get a list of all available backends that can upload files onto the system.
+ *
+ * @return
+ *   Array containing the names of all available backends.
+ */
+function update_list_backends() {
+  $backends = module_invoke_all('update_backend');
+  asort($backends);
+  return $backends;
+}
\ No newline at end of file
Index: modules/update/update.ssh.inc
===================================================================
RCS file: modules/update/update.ssh.inc
diff -N modules/update/update.ssh.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.ssh.inc	22 Mar 2009 02:15:54 -0000
@@ -0,0 +1,112 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * The SSH Backend for the extensions subsystem.
+ */
+
+/**
+ * Install the supplied files to the appropriate locations.
+ *
+ * @param $files
+ *   An array of files to be added by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ssh_add_extension($files, $root, $host, $username, $password) {
+  // Connect to the server via ssh, parsing for non-standard port.
+  $parts = explode(':',$host);
+  if (count($parts) == 2) {
+    $connection = ssh2_connect($parts[0], (int)$parts[1]);
+  }
+  else {
+    $connection = ssh2_connect($host);
+  }
+
+  if ($connection == FALSE) {
+    drupal_set_message(t('Could not connect to ssh on this host.'), 'error');
+    return FALSE;
+  }
+
+  if (!ssh2_auth_password($connection, $username, $password)) {
+    drupal_set_message(t('The supplied username/password combination was not accepted.'), 'error');
+    return FALSE;
+  }
+
+  // Prepare the directories to use later.
+  $extract_directory = file_directory_temp() . '/update_extraction';
+
+  // Process each of the files.
+  foreach ($files as $file) {
+    if (trim($file, '\\/') != $file) {
+      ssh2_exec($connection, 'mkdir ' . escapeshellarg("$root/$file"));
+    }
+    else {
+      ssh2_scp_send($connection, "$extract_directory/$file", "$root/$file");
+    }
+  }
+
+  return TRUE;
+}
+
+/**
+ * Remove the supplied files.
+ *
+ * @param $files
+ *   An array of files to be removed by the specified backend.
+ * @param $root
+ *   The location of the drupal installation; may be different from DRUPAL_ROOT
+ *   depending on the backend being used.
+ * @param $host
+ *   The host server for the backend, such as an FTP server for the ftp
+ *   backend.
+ * @param $username
+ *   The username to use to log in to the backend.
+ * @param $password
+ *   The password to use to log in to the backend.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ssh_remove_extension($files, $root, $host, $username, $password) {
+  // Connect to the server via ssh, parsing for non-standard port.
+  $parts = explode(':', $host);
+  if (count($parts) == 2) {
+    $connection = ssh2_connect($parts[0], (int)$parts[1]);
+  }
+  else {
+    $connection = ssh2_connect($host);
+  }
+
+  if ($connection == FALSE) {
+    drupal_set_message(t('Could not connect to ssh on this host.'), 'error');
+    return FALSE;
+  }
+
+  if (!ssh2_auth_password($connection, $username, $password)) {
+    drupal_set_message(t('The supplied username/password combination was not accepted.'), 'error');
+    return FALSE;
+  }
+
+  // Process each of the files.
+  foreach ($files as $file) {
+    if (trim($file, '\\/') != $file) {
+      ssh2_exec($connection, 'rm -R ' . escapeshellarg("$root/$file"));
+    }
+    else {
+      ssh2_exec($connection, 'rm ' . escapeshellarg("$root/$file"));
+    }
+  }
+
+  return TRUE;
+}
Index: modules/update/update.test
===================================================================
RCS file: modules/update/update.test
diff -N modules/update/update.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.test	22 Mar 2009 02:14:09 -0000
@@ -0,0 +1,285 @@
+<?php
+// $Id$
+
+class UpdateFTPExtensionTestCase extends DrupalWebTestCase {
+  protected $username = 'root';
+  protected $password = 'password';
+  protected $host = 'localhost';
+  protected $root = NULL;
+
+  function getInfo() {
+    return array(
+      'name' => t('FTP extension update functionality'),
+      'description' => t('Test adding and removing a module using the FTP extension. The FTP credentials assumed are host: localhost, path from root to drupal: "drupal", username: "root", and password "password". For other values, you will need to override the settings in settings.php.'),
+      'group' => t('Update')
+    );
+  }
+
+  function setUp() {
+    global $conf;
+    foreach (array('username', 'password', 'host', 'root') as $key) {
+      if (isset($conf["ftp_$key"])) {
+        $this->$key = $conf["ftp_$key"];
+      }
+    }
+    if (is_null($this->root)) {
+      $this->root = getcwd();
+    }
+    parent::setUp('update');
+  }
+
+  /**
+   * Helper function to determine whether or not FTP is enabled with the given
+   * credentials on the given server.
+   *
+   * @return
+   *   TRUE if FTP is enabled and working, or FALSE if Drupal cannot connect to
+   *   FTP for any reason.
+   */
+  function FTPEnabled() {
+    if (!function_exists('ftp_connect')) {
+      $this->pass(t('The FTP extension is not currently available.'));
+      return FALSE;
+    }
+    $parts = explode(':', $this->host);
+    if (count($parts) == 2) {
+      $connect = ftp_connect($parts[0], (int)$parts[1]);
+    }
+    else {
+      $connect = ftp_connect($this->host);
+    }
+    if (!$connect) {
+      $this->pass(t('No FTP server was found at the given location.'));
+      return FALSE;
+    }
+
+    // Login to the local ftp server.
+    if (!@ftp_login($connect, $this->username, $this->password)) {
+      $this->pass(t('Could not login to the ftp server with the given credentials.'));
+      return FALSE;
+    }
+
+    if (!@ftp_chdir($connect, $this->root)) {
+      $this->pass(t('The provided drupal install directory is invalid.'));
+      return FALSE;
+    }
+    $this->pass(t('Connected successfully to FTP'));
+    return TRUE;
+  }
+
+  function testUpdateFTPExtension() {
+    if ($this->FTPEnabled()) {
+      // Upload the module.
+      $path = 'http://ftp.drupal.org/files/projects/coder-7.x-1.x-dev.tar.gz';
+      $local = update_get_file($path);
+      $this->assertTrue($local, t('Tarball fetched from drupal.org without any problems.'));
+      if (!$local) {
+        return;
+      }
+      $files = update_untar($local, 'sites/all/modules');
+      $this->assertTrue(is_array($files) && count($files), t('Module untarred successfully.'));
+      if (!is_array($files) || !count($files)) {
+        return;
+      }
+      foreach ($files as &$file) {
+        $file = 'sites/all/modules/' . $file;
+      }
+      $result = update_add_extension('ftp_extension', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module uploaded successfully.'));
+      if (!$result) {
+        return;
+      }
+
+      // Now, remove the same module.
+      $files = array('sites/all/modules/coder/');
+      $result = update_remove_extension('ftp_extension', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module removed successfully.'));
+    }
+  }
+}
+
+class UpdateFTPWrapperTestCase extends DrupalWebTestCase {
+  protected $username = 'root';
+  protected $password = 'password';
+  protected $host = 'localhost';
+  protected $root = NULL;
+
+  function getInfo() {
+    return array(
+      'name' => t('FTP wrapper update functionality'),
+      'description' => t('Test adding and removing a module using the FTP wrapper. The FTP credentials assumed are host: localhost, path from root to drupal: "drupal", username: "root", and password "password". For other values, you will need to override the settings in settings.php.'),
+      'group' => t('Update')
+    );
+  }
+
+  function setUp() {
+    global $conf;
+    foreach (array('username', 'password', 'host', 'root') as $key) {
+      if (isset($conf["ftp_$key"])) {
+        $this->$key = $conf["ftp_$key"];
+      }
+    }
+    if (is_null($this->root)) {
+      $this->root = getcwd();
+    }
+    parent::setUp('update');
+  }
+
+  /**
+   * Helper function to determine whether or not FTP is enabled with the given
+   * credentials on the given server.
+   *
+   * @return
+   *   TRUE if FTP is enabled and working, or FALSE if Drupal cannot connect to
+   *   FTP for any reason.
+   */
+  function FTPEnabled() {
+    if (!ini_get('allow_url_fopen')) {
+      $this->pass(t('PHP cannot open FTP connections using the FTP wrapper.'));
+      return FALSE;
+    }
+
+    // Write the common part of the url.
+    $ftp_base_directory = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . "@$this->host/";
+
+    if (!@is_dir($ftp_base_directory)) {
+      $this->pass(t('The supplied username/password combination was not accepted, or no local ftp server is running.'));
+      return FALSE;
+    }
+
+    $ftp_path = $ftp_base_directory . $this->root;
+
+    // If it's not a valid path, then quit.
+    if (!is_dir($ftp_path)) {
+      $this->pass(t('Could not find the ftp directory for drupal.'));
+      return FALSE;
+    }
+
+    $this->pass(t('Connected successfully to FTP'));
+    return TRUE;
+  }
+
+  function testUpdateFTPWrapper() {
+    if ($this->FTPEnabled()) {
+      // Upload the module.
+      $path = 'http://ftp.drupal.org/files/projects/coder-7.x-1.x-dev.tar.gz';
+      $local = update_get_file($path);
+      $this->assertTrue($local, t('Tarball fetched from drupal.org without any problems.'));
+      if (!$local) {
+        return;
+      }
+      $files = update_untar($local, 'sites/all/modules');
+      $this->assertTrue(is_array($files) && count($files), t('Module untarred successfully.'));
+      if (!is_array($files) || !count($files)) {
+        return;
+      }
+      foreach ($files as &$file) {
+        $file = 'sites/all/modules/' . $file;
+      }
+      $result = update_add_extension('ftp_wrapper', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module uploaded successfully.'));
+      if (!$result) {
+        return;
+      }
+
+      // Now, remove the same module.
+      $files = array('sites/all/modules/coder/');
+      $result = update_remove_extension('ftp_wrapper', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module removed successfully.'));
+    }
+  }
+}
+
+class UpdateSSHTestCase extends DrupalWebTestCase {
+  protected $username = 'root';
+  protected $password = 'password';
+  protected $host = 'localhost';
+  protected $root = NULL;
+
+  function getInfo() {
+    return array(
+      'name' => t('SSH update functionality'),
+      'description' => t('Test adding and removing a module using SSH. The SSH credentials assumed are host: localhost, path from root to drupal: "drupal", username: "root", and password "password". For other values, you will need to override the settings in settings.php.'),
+      'group' => t('Update')
+    );
+  }
+
+  function setUp() {
+    global $conf;
+    foreach (array('username', 'password', 'host', 'root') as $key) {
+      if (isset($conf["ssh_$key"])) {
+        $this->$key = $conf["ssh_$key"];
+      }
+    }
+    if (is_null($this->root)) {
+      $this->root = getcwd();
+    }
+    parent::setUp('update');
+  }
+
+  /**
+   * Helper function to determine whether or not SSH is enabled with the given
+   * credentials on the given server.
+   *
+   * @return
+   *   TRUE if SSH is enabled and working, or FALSE if Drupal cannot connect to
+   *   SSH for any reason.
+   */
+  function SSHEnabled() {
+    if (!function_exists('ssh2_connect')) {
+      $this->pass(t('PHP cannot open SSH connections.'));
+      return FALSE;
+    }
+
+    $parts = explode(':', $this->host);
+    if (count($parts) == 2) {
+      $connection = ssh2_connect($parts[0], (int)$parts[1]);
+    }
+    else {
+      $connection = ssh2_connect($this->host);
+    }
+
+    if (!$connection) {
+      $this->pass(t('Could not connect to ssh on this host.'));
+      return FALSE;
+    }
+
+    // Login to the local ftp server.
+    if (!ssh2_auth_password($connection, $this->username, $this->password)) {
+      $this->pass(t('The supplied username/password combination was not accepted.'));
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  function testUpdateSSH() {
+    if ($this->SSHEnabled()) {
+      // Upload the module.
+      $path = 'http://ftp.drupal.org/files/projects/coder-7.x-1.x-dev.tar.gz';
+      $local = update_get_file($path);
+      $this->assertTrue($local, t('Tarball fetched from drupal.org without any problems.'));
+      if (!$local) {
+        return;
+      }
+      $files = update_untar($local, 'sites/all/modules');
+      $this->assertTrue(is_array($files) && count($files), t('Module untarred successfully.'));
+      if (!is_array($files) || !count($files)) {
+        return;
+      }
+      foreach ($files as &$file) {
+        $file = 'sites/all/modules/' . $file;
+      }
+      $result = update_add_extension('ssh', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module uploaded successfully.'));
+      if (!$result) {
+        return;
+      }
+
+      // Now, remove the same module.
+      $files = array('sites/all/modules/coder/');
+      $result = update_remove_extension('ssh', $files, $this->root, $this->host, $this->username, $this->password);
+      $this->assertTrue($result, t('Module removed successfully.'));
+    }
+  }
+}
\ No newline at end of file
