Index: modules/simpletest/tests/filetransfer.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/filetransfer.test,v
retrieving revision 1.4
diff -u -p -r1.4 filetransfer.test
--- modules/simpletest/tests/filetransfer.test	17 Aug 2009 19:14:41 -0000	1.4
+++ modules/simpletest/tests/filetransfer.test	24 Aug 2009 22:47:17 -0000
@@ -139,6 +139,14 @@ class TestFileTransfer extends FileTrans
   function isDirectory($path) {
     return $this->shouldIsDirectoryReturnTrue;
   }
+
+  function isFile($path) {
+    return FALSE;
+  }
+
+  function chmodJailed($path, $mode, $recursive) {
+    return;
+  }
 }
 
 /**
Index: includes/file.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/file.inc,v
retrieving revision 1.183
diff -u -p -r1.183 file.inc
--- includes/file.inc	19 Aug 2009 08:38:09 -0000	1.183
+++ includes/file.inc	24 Aug 2009 22:47:18 -0000
@@ -1924,4 +1924,4 @@ function drupal_tempnam($directory, $pre
 
 /**
  * @} End of "defgroup file".
- */
+ */
\ No newline at end of file
Index: includes/filetransfer/filetransfer.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/filetransfer/filetransfer.inc,v
retrieving revision 1.3
diff -u -p -r1.3 filetransfer.inc
--- includes/filetransfer/filetransfer.inc	17 Aug 2009 19:14:40 -0000	1.3
+++ includes/filetransfer/filetransfer.inc	24 Aug 2009 22:47:18 -0000
@@ -20,11 +20,7 @@ abstract class FileTransfer {
    * The constructer for the UpdateConnection class. This method is also called
    * from the classes that extend this class and override this method.
    */
-  function __construct($jail, $username, $password, $hostname, $port) {
-    $this->username = $username;
-    $this->password = $password;
-    $this->hostname = $hostname;
-    $this->port = $port;
+  function __construct($jail) {
     $this->jail = $jail;
   }
 
@@ -41,6 +37,11 @@ abstract class FileTransfer {
       $this->connect();
       return $this->connection;
     }
+
+    if ($name == 'chroot') {
+      $this->setChroot();
+      return $this->chroot;
+    }
   }
 
   /**
@@ -57,17 +58,36 @@ abstract class FileTransfer {
    *   The destination path.
    */
   public final function copyDirectory($source, $destination) {
+    $source = $this->sanitizePath($source);
+    $destination = $this->fixRemotePath($destination);
     $this->checkPath($destination);
     $this->copyDirectoryJailed($source, $destination);
   }
 
   /**
+   * @see http://php.net/chmod
+   *
+   * @param string $path
+   * @param long $mode
+   * @param bool $recursive
+   */
+  public final function chmod($path, $mode, $recursive = FALSE) {
+    $path = $this->sanitizePath($path);
+    $path = $this->fixRemotePath($path);
+    $this->checkPath($path);
+    $this->chmodJailed($path, $mode, $recursive);
+  }
+
+  protected abstract function chmodJailed($path, $mode, $recursive);
+
+  /**
    * Creates a directory.
    *
    * @param $directory
    *   The directory to be created.
    */
   public final function createDirectory($directory) {
+    $directory = $this->fixRemotePath($directory);
     $this->checkPath($directory);
     $this->createDirectoryJailed($directory);
   }
@@ -79,6 +99,7 @@ abstract class FileTransfer {
    *   The directory to be removed.
    */
   public final function removeDirectory($directory) {
+    $directory = $this->fixRemotePath($directory);
     $this->checkPath($directory);
     $this->removeDirectoryJailed($directory);
   }
@@ -92,6 +113,8 @@ abstract class FileTransfer {
    *   The destination file.
    */
   public final function copyFile($source, $destination) {
+    $source = $this->sanitizePath($source);
+    $destination = $this->fixRemotePath($destination);
     $this->checkPath($destination);
     $this->copyFileJailed($source, $destination);
   }
@@ -103,6 +126,7 @@ abstract class FileTransfer {
    *   The destination file to be removed.
    */
   public final function removeFile($destination) {
+    $destination = $this->fixRemotePath($destination);
     $this->checkPath($destination);
     $this->removeFileJailed($destination);
   }
@@ -114,12 +138,51 @@ abstract class FileTransfer {
    *   A path to check against the jail.
    */
   protected final function checkPath($path) {
-    if (drupal_realpath(substr($path, 0, strlen($this->jail))) !== $this->jail) {
+    $full_jail = $this->chroot . $this->jail;
+    $full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
+    $full_path = $this->fixRemotePath($full_path, FALSE);
+    if ($full_jail !== $full_path) {
       throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
     }
   }
 
   /**
+   * Returns a modified path suitable for passing to the server.
+   * If a path is a windows path, makes it posix compliant by removing the drive letter.
+   * If $this->chroot has a value, it is stripped from the path to allow for
+   * chroot'd filetransfer systems.
+   *
+   * @param $path
+   * @param $strip_chroot
+   *
+   * @return string;
+   */
+  protected final function fixRemotePath($path, $strip_chroot = TRUE) {
+    $path = $this->sanitizePath($path);
+    $path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
+    if ($strip_chroot) {
+      if ($this->chroot && strpos($path, $this->chroot) === 0) {
+        $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
+      }
+    }
+    return $path;
+  }
+
+  /**
+  * Changes backslahes to slashes, also removes a trailing slash.
+  *
+  * @param string $path
+  * @return string;
+  */
+  function sanitizePath($path) {
+    $path = str_replace('\\', '/', $path); // Windows path sanitiation.
+    if (substr($path, -1) == '/') {
+      $path = substr($path, 0, -1);
+    }
+    return $path;
+  }
+
+  /**
    * Copies a directory.
    *
    * We need a separate method to make the $destination is in the jail.
@@ -178,7 +241,7 @@ abstract class FileTransfer {
    *   The destination file to be removed.
    */
   abstract protected function removeFileJailed($destination);
-  
+
   /**
    * Checks if a particular path is a directory
    *
@@ -188,6 +251,54 @@ abstract class FileTransfer {
    * @return boolean
    */
   abstract public function isDirectory($path);
+
+  /**
+   * Checks if a particular path is a file (not a directory).
+   *
+   * @param $path
+   *   The path to check
+   *
+   * @return boolean
+   */
+  abstract public function isFile($path);
+
+  /**
+   * Gets the chroot property for this connection.  It does this by moving up
+   * the tree until it finds itself.  If successful, it will return a chroot.
+   *
+   * @return string chroot
+   */
+  function findChroot() {
+    // If the file exists as is, there is no chroot.
+    $path = __FILE__;
+    $path = $this->fixRemotePath($path, FALSE);
+    if ($this->isFile($path)) {
+      return FALSE;
+    }
+
+    $path = dirname(__FILE__);
+    $path = $this->fixRemotePath($path, FALSE);
+    $parts = explode('/', $path);
+    $chroot = '';
+    while (count($parts)) {
+      $check = implode($parts, '/');
+      if ($this->isFile($check . '/' . basename(__FILE__))) {
+        // Remove the trailing slash.
+        return substr($chroot,0,-1);
+      }
+      $chroot .= array_shift($parts) . '/';
+    }
+    return FALSE;
+  }
+
+  /**
+   * Sets the chroot and changes the jail to match the correct path scheme
+   *
+   */
+  function setChroot() {
+    $this->chroot = $this->findChroot();
+    $this->jail = $this->fixRemotePath($this->jail);
+  }
 }
 
 /**
@@ -200,4 +311,4 @@ class FileTransferException extends Exce
     parent::__construct($message, $code);
     $this->arguments = $arguments;
   }
-}
+}
\ No newline at end of file
Index: includes/filetransfer/ftp.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/filetransfer/ftp.inc,v
retrieving revision 1.5
diff -u -p -r1.5 ftp.inc
--- includes/filetransfer/ftp.inc	17 Aug 2009 19:14:40 -0000	1.5
+++ includes/filetransfer/ftp.inc	24 Aug 2009 22:47:18 -0000
@@ -49,23 +49,56 @@ class FileTransferFTPWrapper extends Fil
   }
 
   function copyFileJailed($source, $destination) {
-    if (!@copy($this->connection . '/' . $source, $this->connection . '/' . $destination)) {
+    if (!@copy($source, $this->connection . '/' . $destination)) {
       throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination));
     }
   }
 
   function removeFileJailed($destination) {
-    if (!@unlink($destination)) {
+    if (!@unlink($this->connection . '/' .$destination)) {
       throw new FileTransferException('Cannot remove @destination', NULL, array('@destination' => $destination));
     }
   }
-  
+
   function isDirectory($path) {
     return is_dir($this->connection . '/' . $path);
   }
+
+  public function isFile($path) {
+    // This is stupid, but is_file and file_exists don't work! always return true.
+    return @fopen($this->connection . '/' . $path,'r');
+  }
+
+  /**
+   * This is impossible with the stream wrapper,
+   * So we cheat and use the other implementation
+   *
+   * @staticvar FileTransferFTPExtension $ftp_ext_file_transfer
+   * @param string $path
+   * @param long $mode
+   * @param bool $recursive
+   */
+  function chmodJailed($path, $mode, $recursive) {
+    static $ftp_ext_file_transfer;
+
+    if (!$ftp_ext_file_transfer) {
+      $ftp_ext_file_transfer = new FileTransferFTPExtension($this->jail, $this->username, $this->password, $this->hostname, $this->port);
+    }
+    $ftp_ext_file_transfer->chmodJailed($path, $mode, $recursive);
+  }
+
 }
 
 class FileTransferFTPExtension extends FileTransfer {
+
+  public function __construct($jail, $username, $password, $hostname, $port) {
+    $this->username = $username;
+    $this->password = $password;
+    $this->hostname = $hostname;
+    $this->port = $port;
+    parent::__construct($jail);
+  }
+
   public function connect() {
     $this->connection = ftp_connect($this->hostname, $this->port);
 
@@ -76,7 +109,14 @@ class FileTransferFTPExtension extends F
       throw new FileTransferException("Cannot login to FTP server, please check username and password");
     }
   }
-  
+
+  /**
+   * Returns a copy of itself using common defaults.
+   *
+   * @param string $jail
+   * @param array $settings
+   * @return FileTransferFTPExtension
+   */
   static function factory($jail, $settings) {
     $settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname'];
     $settings['port'] = empty($settings['port']) ? 21 : $settings['port'];
@@ -90,17 +130,20 @@ class FileTransferFTPExtension extends F
   }
 
   protected function createDirectoryJailed($directory) {
-    if (!@ftp_mkdir($this->connection, $directory)) {
+    if (!ftp_mkdir($this->connection, $directory)) {
       throw new FileTransferException("Cannot create directory @directory", NULL, array("@directory" => $directory));
     }
   }
 
   protected function removeDirectoryJailed($directory) {
     $pwd = ftp_pwd($this->connection);
-    if (!@ftp_chdir($this->connection, $directory)) {
+    if (!ftp_chdir($this->connection, $directory)) {
       throw new FileTransferException("Unable to change to directory @directory", NULL, array('@directory' => $directory));
     }
     $list = @ftp_nlist($this->connection, '.');
+    if (!$list) {
+      $list = array();
+    }
     foreach ($list as $item){
       if ($item == '.' || $item == '..') {
         continue;
@@ -124,14 +167,40 @@ class FileTransferFTPExtension extends F
       throw new FileTransferException("Unable to remove to file @file", NULL, array('@file' => $destination));
     }
   }
-  
+
   public function isDirectory($path) {
     $result = FALSE;
     $curr = ftp_pwd($this->connection);
-    if (ftp_chdir($this->connection, $path)) {
+    if (@ftp_chdir($this->connection, $path)) {
       $result = TRUE;
     }
     ftp_chdir($this->connection, $curr);
     return $result;
   }
+
+  public function isFile($path) {
+    return ftp_size($this->connection, $path) != -1;
+  }
+
+  function chmodJailed($path, $mode, $recursive) {
+    if (!ftp_chmod($this->connection, $mode, $path)) {
+      throw new FileTransferException("Unable to set permissions on %file", NULL, array ('%file' => $path));
+    }
+    if ($this->isDirectory($path) && $recursive) {
+      $filelist = @ftp_nlist($this->connection, $path);
+      if (!$filelist) {
+        //empty directory - returns false
+        return;
+      }
+      foreach ($filelist as $file) {
+        $this->chmodJailed($file, $mode, $recursive);
+      }
+    }
+  }
 }
+
+if (!function_exists('ftp_chmod')) {
+  function ftp_chmod($ftp_stream, $mode, $filename) {
+    return ftp_site($ftp_stream, sprintf('CHMOD %o %s', $mode, $filename));
+  }
+}
\ No newline at end of file
Index: includes/filetransfer/ssh.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/filetransfer/ssh.inc,v
retrieving revision 1.2
diff -u -p -r1.2 ssh.inc
--- includes/filetransfer/ssh.inc	1 Jul 2009 13:44:53 -0000	1.2
+++ includes/filetransfer/ssh.inc	24 Aug 2009 22:47:18 -0000
@@ -7,7 +7,11 @@
 class FileTransferSSH extends FileTransfer {
 
   function __construct($jail, $username, $password, $hostname = "localhost", $port = 22) {
-    parent::__construct($jail, $username, $password, $hostname, $port);
+    $this->username = $username;
+    $this->password = $password;
+    $this->hostname = $hostname;
+    $this->port = $port;
+    parent::__construct($jail);
   }
 
   function connect() {
@@ -71,4 +75,24 @@ class FileTransferSSH extends FileTransf
       throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
     }
   }
+
+  public function isFile($path) {
+    $file = escapeshellarg($path);
+    $cmd = "[ -f {$file} ] && echo 'yes'";
+    if ($output = @ssh2_exec($this->connection, $cmd)) {
+      if ($output == 'yes') {
+        return TRUE;
+      }
+      return FALSE;
+    } else {
+      throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
+    }
+  }
+
+  function chmodJailed($path, $mode, $recursive) {
+    $cmd = sprintf("chmod %s%o %s", $recursive ? '-R ' : '', $mode, escapeshellarg($path));
+    if (@!ssh2_exec($this->connection, $cmd)) {
+      throw new FileTransferException('Cannot change permissions of @path.', NULL, array('@path' => $path));
+    }
+  }
 }
