Index: modules/update/update.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/update/update.api.php,v
retrieving revision 1.1
diff -u -p -r1.1 update.api.php
--- modules/update/update.api.php	25 Nov 2008 02:37:33 -0000	1.1
+++ modules/update/update.api.php	9 Apr 2009 23:50:46 -0000
@@ -41,5 +41,53 @@ function hook_update_status_alter(&$proj
 }
 
 /**
+ * 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, $settings)":
+ *    NAME here corresponds to the keys of the return array; for this example,
+ *    this function would be update_btp_add_extension(). This function should
+ *    install the files specified by $files to the server specified by the
+ *    array $settings, which may contain data such as the password, username,
+ *    or server location. Should return TRUE on success, or FALSE on failure.
+ *
+ *  - "update_NAME_remove_extension($files, $settings)":
+ *    NAME here corresponds to the keys of the return array; for this example,
+ *    this function would be update_btp_remove_extension(). This function
+ *    should remove the files specified by $files from the server specified by
+ *    the array $settings, which may contain data such as the password,
+ *    username, or server location. Should return TRUE on success, or FALSE on
+ *    failure.
+ *
+ *  - "update_NAME_settings_form($form)":
+ *    NAME here corresponds to the keys of the return array; for this example,
+ *    this function would be update_btp_settings_form(). This function is
+ *    optional, and should be used only if the module needs to make any
+ *    modifications or additions to the default form values, which are
+ *    "username", "password", "host", and "root" (the path to the root drupal
+ *    installation). The settings entered by the user will be passed back to
+ *    the two above functions as the $settings array.
+ *
+ * @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;
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
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	9 Apr 2009 23:50:46 -0000
@@ -0,0 +1,155 @@
+<?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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_extension_add_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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) {
+    if (trim($file, '\\/') != $file) {
+      if (!@ftp_mkdir($connect, "$root/$file") && !@ftp_chdir($connect, "$root/$file")) {
+        drupal_set_message(t('Could not 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('Could not upload file @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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_extension_remove_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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;
+  }
+
+  // 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")) {
+        drupal_set_message(t('Could not remove directory @directory.', array('@directory' => $file)), 'error');
+        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;
+}
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	9 Apr 2009 23:50:46 -0000
@@ -0,0 +1,135 @@
+<?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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_wrapper_add_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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) {
+      if (!@mkdir("$ftp_path/$file")) {
+        drupal_set_message(t('Could not create directory @directory.', array('@directory' => $file)), 'error');
+        return FALSE;
+      }
+    }
+    else {
+      if (!@copy($extract_directory . $file, "$ftp_path/$file")) {
+        drupal_set_message(t('Could not upload file @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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ftp_wrapper_remove_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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")) {
+        drupal_set_message(t('Could not remove directory @directory.', array('@directory' => $file)), 'error');
+        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;
+}
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	9 Apr 2009 23:50:46 -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	9 Apr 2009 23:50:47 -0000
@@ -286,6 +286,31 @@ function update_cron() {
 }
 
 /**
+ * Implementation of hook_update_backend().
+ */
+function update_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.
+  if (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_form_FORM_ID_alter().
  *
  * Adds a submit handler to the system modules and themes forms, so that if a
@@ -515,3 +540,200 @@ 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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_add_extension($backend, $files, $settings) {
+  $function = 'update_' . $backend . '_add_extension';
+  if (drupal_function_exists($function)) {
+    return $function($files, $settings);
+  }
+  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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_remove_extension($backend, $files, $settings) {
+  $function = 'update_' . $backend . '_remove_extension';
+  if (drupal_function_exists($function)) {
+    return $function($files, $settings);
+  }
+  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;
+}
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	9 Apr 2009 23:50:47 -0000
@@ -0,0 +1,120 @@
+<?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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ssh_add_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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) {
+      if (!@ssh2_exec($connection, 'mkdir ' . escapeshellarg("$root/$file"))) {
+        drupal_set_message(t('Could not make directory @directory.', array('@directory' => "$root/$file")), 'error');
+        return FALSE;
+      }
+    }
+    else {
+      if (!@ssh2_scp_send($connection, "$extract_directory/$file", "$root/$file")) {
+        drupal_set_message(t('Could not upload file @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 $settings
+ *   An array of settings, which will vary among different backends, but may
+ *   include data such as the username, password, and host.
+ * @return
+ *   TRUE on success, or FALSE on failure.
+ */
+function update_ssh_remove_extension($files, $settings) {
+  $root = $settings['root'];
+  $host = $settings['host'];
+  $username = $settings['username'];
+  $password = $settings['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) {
+      if (!@ssh2_exec($connection, 'rm -R ' . escapeshellarg("$root/$file"))) {
+        drupal_set_message(t('Could not remove directory @directory.', array('@directory' => $file)), 'error');
+        return FALSE;
+      }
+    }
+    else {
+      if (!@ssh2_exec($connection, 'rm ' . escapeshellarg("$root/$file"))) {
+        drupal_set_message(t('Could not remove file @file.', array('@file' => $file)), 'error');
+        return FALSE;
+      }
+    }
+  }
+
+  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	9 Apr 2009 23:50:47 -0000
@@ -0,0 +1,304 @@
+<?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;
+      }
+      $settings = array(
+        'username' => $this->username,
+        'password' => $this->password,
+        'host' => $this->host,
+        'root' => $this->root,
+      );
+      $result = update_add_extension('ftp_extension', $files, $settings);
+      $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, $settings);
+      $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;
+      }
+      $settings = array(
+        'username' => $this->username,
+        'password' => $this->password,
+        'host' => $this->host,
+        'root' => $this->root,
+      );
+      $result = update_add_extension('ftp_wrapper', $files, $settings);
+      $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, $settings);
+      $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;
+    }
+
+    $this->pass(t('Connect successfully to the server using SSH.'));
+    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;
+      }
+      $settings = array(
+        'username' => $this->username,
+        'password' => $this->password,
+        'host' => $this->host,
+        'root' => $this->root,
+      );
+      $result = update_add_extension('ssh', $files, $settings);
+      $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, $settings);
+      $this->assertTrue($result, t('Module removed successfully.'));
+    }
+  }
+}
