Index: modules/simpletest/tests/filetransfer.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/filetransfer.test,v
retrieving revision 1.3
diff -u -p -r1.3 filetransfer.test
--- modules/simpletest/tests/filetransfer.test	14 Jul 2009 19:31:32 -0000	1.3
+++ modules/simpletest/tests/filetransfer.test	5 Aug 2009 11:14:29 -0000
@@ -139,6 +139,10 @@ class TestFileTransfer extends FileTrans
   function isDirectory($path) {
     return $this->shouldIsDirectoryReturnTrue;
   }
+
+  function isFile($path) {
+    return FALSE;
+  }
 }
 
 /**
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.953
diff -u -p -r1.953 common.inc
--- includes/common.inc	4 Aug 2009 06:38:56 -0000	1.953
+++ includes/common.inc	5 Aug 2009 11:14:32 -0000
@@ -4021,7 +4021,7 @@ function element_sort($a, $b) {
  */
 function element_info($type) {
   $cache = &drupal_static(__FUNCTION__);
-
+  
   if (!isset($cache)) {
     $basic_defaults = element_basic_defaults();
     $cache = array();
@@ -4040,7 +4040,6 @@ function element_info($type) {
     // Allow modules to alter the element type defaults.
     drupal_alter('element_info', $cache);
   }
-
   return $cache[$type];
 }
 
Index: includes/filetransfer/filetransfer.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/filetransfer/filetransfer.inc,v
retrieving revision 1.2
diff -u -p -r1.2 filetransfer.inc
--- includes/filetransfer/filetransfer.inc	1 Jul 2009 13:44:53 -0000	1.2
+++ includes/filetransfer/filetransfer.inc	5 Aug 2009 11:14:32 -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 <type> $path
+   * @param <type> $mode
+   * @param <type> $recursive
+   */
+  public final function chmod($path, $mode, $recursive) {
+    $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 (realpath(substr($path, 0, strlen($this->jail))) !== $this->jail) {
+    $full_jail = $this->chroot . $this->jail;
+    $full_path = 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);
+    if ($this->isFile($path)) {
+      return FALSE;
+    }
+
+    $path = dirname(__FILE__);
+    $path = $this->fixRemotePath($path);
+    $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);
+  }
 }
 
 /**
Index: includes/filetransfer/ftp.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/filetransfer/ftp.inc,v
retrieving revision 1.4
diff -u -p -r1.4 ftp.inc
--- includes/filetransfer/ftp.inc	1 Jul 2009 13:44:53 -0000	1.4
+++ includes/filetransfer/ftp.inc	5 Aug 2009 11:14:32 -0000
@@ -5,13 +5,30 @@
  * Connection class using the FTP URL wrapper.
  */
 class FileTransferFTPWrapper 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);
+  }
+
   function connect() {
     $this->connection = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . '@' . $this->hostname . ':' . $this->port . '/';
     if (!is_dir($this->connection)) {
       throw new FileTransferException('FTP Connection failed.');
     }
   }
-  
+
+  /**
+   * Returns a copy of itself using common defaults.
+   *
+   * @param string $jail
+   * @param array $settings
+   *
+   * @return FileTransferFTPWrapper
+   */
   static function factory($jail, $settings) {
     $settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname'];
     $settings['port'] = empty($settings['port']) ? 21 : $settings['port'];
@@ -19,29 +36,29 @@ class FileTransferFTPWrapper extends Fil
   }
 
   function createDirectoryJailed($directory) {
-    if (!@mkdir($directory)) {
+    if (!@mkdir($this->connection . '/' . $directory)) {
       $exception = new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory));
       throw $exception;
     }
   }
 
   function removeDirectoryJailed($directory) {
-    if (is_dir($directory)) {
-      $dh = opendir($directory);
+    if (is_dir($this->connection . '/' . $directory)) {
+      $dh = opendir($this->connection . '/' .$directory);
       while (($resource = readdir($dh)) !== FALSE) {
         if ($resource == '.' || $resource == '..') {
           continue;
         }
         $full_path = $directory . DIRECTORY_SEPARATOR . $resource;
-        if (is_file($full_path)) {
+        if (is_file($this->connection . '/' .$full_path)) {
           $this->removeFile($full_path);
         }
-        elseif (is_dir($full_path)) {
+        elseif (is_dir($this->connection . '/' .$full_path)) {
           $this->removeDirectory($full_path . '/');
         }
       }
       closedir($dh);
-      if (!rmdir($directory)) {
+      if (!rmdir($this->connection . '/' .$directory)) {
         $exception = new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory));
         throw $exception;
       }
@@ -49,23 +66,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 +126,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,14 +147,14 @@ 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, '.');
@@ -124,14 +181,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));
+  }
 }
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	5 Aug 2009 11:14:32 -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));
+    }
+  }
 }
