Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.715 diff -u -r1.715 system.module --- modules/system/system.module 23 Jun 2009 12:11:19 -0000 1.715 +++ modules/system/system.module 1 Jul 2009 12:34:51 -0000 @@ -828,36 +828,115 @@ } /** - * Implementation of hook_filetransfer_backends(). + * Implement hook_filetransfer_backends(). */ function system_filetransfer_backends() { $backends = array(); - // SSH2 lib connection is only available if the proper PHP extension is - // installed. - if (function_exists('ssh2_connect')) { - $backends['ssh'] = array( - 'title' => t('SSH'), - 'class' => 'FileTransferSSH', - ); - } + //This is the default, will be available on most systems if (function_exists('ftp_connect')) { $backends['ftp_extension'] = array( - 'title' => t('FTP Extension'), + 'title' => t('FTP'), 'class' => 'FileTransferFTPExtension', + 'settings_form' => 'system_filetransfer_backend_form_ftp', + 'weight' => 0, ); } - + if (ini_get('allow_url_fopen')) { $backends['ftp_wrapper'] = array( - 'title' => t('FTP Wrapper'), + 'title' => t('FTP using file streams'), 'class' => 'FileTransferFTPWrapper', + 'settings_form' => 'system_filetransfer_backend_form_ftp', + 'weight' => 10, + ); + } + + // SSH2 lib connection is only available if the proper PHP extension is + // installed. + if (function_exists('ssh2_connect')) { + $backends['ssh'] = array( + 'title' => t('SSH'), + 'class' => 'FileTransferSSH', + 'settings_form' => 'system_filetransfer_backend_form_ssh', + 'weight' => 20, ); } return $backends; } /** + * Helper function to return a form for configuring a filetransfer backend. + * + * @param string $filetransfer_backend_name + * The name of the backend to return a form for. + * + * @param string $defaults + * An associative array of settings to pre-populate the form with. + */ +function system_get_filetransfer_settings_form($filetransfer_backend_name, $defaults) { + $available_backends = module_invoke_all('filetransfer_backends'); + $form = call_user_func($available_backends[$filetransfer_backend_name]['settings_form']); + + foreach ($form as $name => &$element) { + if (isset($defaults[$name])) { + $element['#default_value'] = $defaults[$name]; + } + } + return $form; +} + +/** + * Returns the form to configure the filetransfer class for FTP + */ +function system_filetransfer_backend_form_ftp() { + $form = _system_filetransfer_backend_form_common(); + $form['port']['#default_value'] = 21; + return $form; +} + +/** + * Returns the form to configure the filetransfer class for SSH + */ +function system_filetransfer_backend_form_ssh() { + $form = _system_filetransfer_backend_form_common(); + $form['port']['#default_value'] = 22; + return $form; +} + +/** + * Helper function because SSH and FTP backends share the same elements + */ +function _system_filetransfer_backend_form_common() { + $form = array(); + + $form['hostname'] = array ( + '#type' => 'textfield', + '#title' => t('Host'), + '#default_value' => 'localhost', + ); + + $form['port'] = array ( + '#type' => 'textfield', + '#title' => t('Port'), + '#default_value' => NULL, + ); + + $form['username'] = array ( + '#type' => 'textfield', + '#title' => t('Username'), + ); + + $form['password'] = array ( + '#type' => 'password', + '#title' => t('Password'), + '#description' => t('This is not saved in the database and is only used to test the connection'), + ); + + return $form; +} + +/** * Implement hook_init(). */ function system_init() { @@ -2543,29 +2622,37 @@ /** * Attempts to get a file using drupal_http_request and to store it locally. * - * @param $path + * @param $url * The URL of the file to grab. + * + * @param $destination + * Where the file should be saved, if a directory is provided, file is saved + * in that directory with its original name. If a filename is provided, + * remote fileis stored to that location. NOTE: Relative to drupal "files" directory" + * + * @param $overwrite boolean + * Defaults to TRUE, will overwrite existing files of the same name. + * * @return * On success the address the files was saved to, FALSE on failure. */ -function system_retrieve_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; - } +function system_retrieve_file($url, $destination = NULL, $overwrite = TRUE) { + if (!$destination) { + $destination = file_directory_temp(); + } + $parsed_url = parse_url($url); + $local = is_dir(file_directory_path() . '/' . $destination) ? $destination . '/' . basename($parsed_url['path']) : $destination; + + if (!$overwrite && file_exists($local)) { + drupal_set_message(t('@remote could not be saved. @local already exists', array('@remote' => $url, '@local' => $local)), 'error'); + return FALSE; + } + + $result = drupal_http_request($url); + if ($result->code != 200 || !file_save_data($result->data, $local)) { + drupal_set_message(t('@remote could not be saved.', array('@remote' => $url)), 'error'); + return FALSE; } + return $local; } Index: includes/filetransfer/filetransfer.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/filetransfer/filetransfer.inc,v retrieving revision 1.1 diff -u -r1.1 filetransfer.inc --- includes/filetransfer/filetransfer.inc 23 Jun 2009 12:11:19 -0000 1.1 +++ includes/filetransfer/filetransfer.inc 1 Jul 2009 12:34:51 -0000 @@ -2,28 +2,34 @@ // $Id: filetransfer.inc,v 1.1 2009/06/23 12:11:19 dries Exp $ /* - * Connection class. + * Base FileTransfer class. * - * This class does file operations on directories not writeable by the - * webserver. It connects back to the server using some backend (for example - * FTP or SSH). To keep security the password should always be asked from the - * user and never stored. + * Classes extending this class perform file operations on directories not + * writeable by the webserver. To achieve this, the class should connect back + * to the server using some backend (for example FTP or SSH). To keep security, + * the password should always be asked from the user and never stored. For + * safety, all methods operate only inside a "jail", by default the Drupal root. */ abstract class FileTransfer { + protected $username; + protected $password; + protected $hostname = 'localhost'; + protected $port; /** * The constructer for the UpdateConnection class. This method is also called * from the classes that extend this class and override this method. */ - function __construct($settings) { - $this->username = $settings['username']; - $this->password = $settings['password']; - $this->hostname = isset($settings['hostname']) ? $settings['hostname'] : 'localhost'; - if (isset($settings['port'])) { - $this->port = $settings['port']; - } + function __construct($jail, $username, $password, $hostname, $port) { + $this->username = $username; + $this->password = $password; + $this->hostname = $hostname; + $this->port = $port; + $this->jail = $jail; } + abstract static function factory($jail, $settings); + /** * Implementation of the magic __get() method. If the connection isn't set to * anything, this will call the connect() method and set it to and return the @@ -31,25 +37,105 @@ * this method. */ function __get($name) { - static $connection; if ($name == 'connection') { - $this->connection = $this->connect(); + $this->connect(); return $this->connection; } } /** + * Connect to the server. + */ + abstract protected function connect(); + + /** + * Copies a directory. + * + * @param $source + * The source path. + * @param $destination + * The destination path. + */ + public final function copyDirectory($source, $destination) { + $this->checkPath($destination); + $this->copyDirectoryJailed($source, $destination); + } + + /** + * Creates a directory. + * + * @param $directory + * The directory to be created. + */ + public final function createDirectory($directory) { + $this->checkPath($directory); + $this->createDirectoryJailed($directory); + } + + /** + * Removes a directory. + * + * @param $directory + * The directory to be removed. + */ + public final function removeDirectory($directory) { + $this->checkPath($directory); + $this->removeDirectoryJailed($directory); + } + + /** + * Copies a file. + * + * @param $source + * The source file. + * @param $destination + * The destination file. + */ + public final function copyFile($source, $destination) { + $this->checkPath($destination); + $this->copyFileJailed($source, $destination); + } + + /** + * Removes a file. + * + * @param $destination + * The destination file to be removed. + */ + public final function removeFile($destination) { + $this->checkPath($destination); + $this->removeFileJailed($destination); + } + + /** + * Checks that the path is inside the jail and throws an exception if not. + * + * @param $path + * A path to check against the jail. + */ + protected final function checkPath($path) { + if (realpath(substr($path, 0, strlen($this->jail))) !== $this->jail) { + throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail)); + } + } + + /** * Copies a directory. * + * We need a separate method to make the $destination is in the jail. + * * @param $source * The source path. * @param $destination * The destination path. */ - protected function copyDirectory($source, $destination) { - $this->createDirectory($destination . basename($source)); + protected function copyDirectoryJailed($source, $destination) { + if ($this->isDirectory($destination)) { + $destination = $destination . '/' . basename($source); + } + $this->createDirectory($destination); foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) { - $relative_path = basename($source) . substr($filename, strlen($source)); + $relative_path = substr($filename, strlen($source)); if ($file->isDir()) { $this->createDirectory($destination . $relative_path); } @@ -65,7 +151,7 @@ * @param $directory * The directory to be created. */ - abstract function createDirectory($directory); + abstract protected function createDirectoryJailed($directory); /** * Removes a directory. @@ -73,7 +159,7 @@ * @param $directory * The directory to be removed. */ - abstract function removeDirectory($directory); + abstract protected function removeDirectoryJailed($directory); /** * Copies a file. @@ -83,8 +169,7 @@ * @param $destination * The destination file. */ - abstract function copyFile($source, $destination); - + abstract protected function copyFileJailed($source, $destination); /** * Removes a file. @@ -92,11 +177,27 @@ * @param $destination * The destination file to be removed. */ - abstract function removeFile($destination); + abstract protected function removeFileJailed($destination); + + /** + * Checks if a particular path is a directory + * + * @param $path + * The path to check + * + * @return boolean + */ + abstract public function isDirectory($path); } /** * FileTransferException class. */ class FileTransferException extends Exception { + public $arguments; + + function __construct($message, $code = 0, $arguments = array()) { + parent::__construct($message, $code); + $this->arguments = $arguments; + } } Index: includes/filetransfer/ftp.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/filetransfer/ftp.inc,v retrieving revision 1.3 diff -u -r1.3 ftp.inc --- includes/filetransfer/ftp.inc 24 Jun 2009 01:45:09 -0000 1.3 +++ includes/filetransfer/ftp.inc 1 Jul 2009 12:34:51 -0000 @@ -2,17 +2,6 @@ // $Id: ftp.inc,v 1.3 2009/06/24 01:45:09 webchick Exp $ /** - * Common code for the FTP connections. - */ -abstract class FileTransferFTP extends FileTransfer { - function __construct($settings) { - // This is the default, if $settings contains a port, this will be overridden. - $this->port = 21; - parent::__construct($settings); - } -} - -/** * Connection class using the FTP URL wrapper. */ class FileTransferFTPWrapper extends FileTransfer { @@ -22,18 +11,21 @@ throw new FileTransferException('FTP Connection failed.'); } } + + static function factory($jail, $settings) { + $settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname']; + $settings['port'] = empty($settings['port']) ? 21 : $settings['port']; + return new FileTransferFTPWrapper($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); + } - function createDirectory($directory) { - if (!@createDirectory($directory)) { + function createDirectoryJailed($directory) { + if (!@mkdir($directory)) { $exception = new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory)); throw $exception; } } - function removeDirectory($directory) { - if (realpath(substr($directory, 0, strlen(DRUPAL_ROOT))) !== DRUPAL_ROOT) { - throw new FileTransferException('@directory is outside of the Drupal root.', NULL, array('@directory' => $directory)); - } + function removeDirectoryJailed($directory) { if (is_dir($directory)) { $dh = opendir($directory); while (($resource = readdir($dh)) !== FALSE) { @@ -49,28 +41,32 @@ } } closedir($dh); - if (!removeDirectory($directory)) { + if (!rmdir($directory)) { $exception = new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory)); throw $exception; } } } - function copyFile($source, $destination) { + function copyFileJailed($source, $destination) { if (!@copy($this->connection . '/' . $source, $this->connection . '/' . $destination)) { throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination)); } } - function removeFile($destination) { + function removeFileJailed($destination) { if (!@unlink($destination)) { throw new FileTransferException('Cannot remove @destination', NULL, array('@destination' => $destination)); } } + + function isDirectory($path) { + return is_dir($this->connection . '/' . $path); + } } class FileTransferFTPExtension extends FileTransfer { - function connect() { + public function connect() { $this->connection = ftp_connect($this->hostname, $this->port); if (!$this->connection) { @@ -80,23 +76,26 @@ throw new FileTransferException("Cannot login to FTP server, please check username and password"); } } + + static function factory($jail, $settings) { + $settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname']; + $settings['port'] = empty($settings['port']) ? 21 : $settings['port']; + return new FileTransferFTPExtension($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); + } - function copyFile($source, $destination) { + protected function copyFileJailed($source, $destination) { if (!@ftp_put($this->connection, $destination, $source, FTP_BINARY)) { throw new FileTransferException("Cannot move @source to @destination", NULL, array("@source" => $source, "@destination" => $destination)); } } - function createDirectory($directory) { - if (!@ftp_createDirectory($this->connection, $directory)) { + protected function createDirectoryJailed($directory) { + if (!@ftp_mkdir($this->connection, $directory)) { throw new FileTransferException("Cannot create directory @directory", NULL, array("@directory" => $directory)); } } - function removeDirectory($directory) { - if (realpath(substr($directory, 0, strlen(DRUPAL_ROOT))) !== DRUPAL_ROOT) { - throw new FileTransferException('@directory is outside of the Drupal root.', NULL, array('@directory' => $directory)); - } + protected function removeDirectoryJailed($directory) { $pwd = ftp_pwd($this->connection); if (!@ftp_chdir($this->connection, $directory)) { throw new FileTransferException("Unable to change to directory @directory", NULL, array('@directory' => $directory)); @@ -107,22 +106,32 @@ continue; } if (@ftp_chdir($this->connection, $item)){ - ftp_chdir($this->connection, '..'); - $this->removeDirectory($item); + ftp_cdup($this->connection); + $this->removeDirectory(ftp_pwd($this->connection) . '/' . $item); } else { - $this->removeFile($item); + $this->removeFile(ftp_pwd($this->connection) . '/' . $item); } } ftp_chdir($this->connection, $pwd); - if (!ftp_removeDirectory($this->connection, $directory)) { + if (!ftp_rmdir($this->connection, $directory)) { throw new FileTransferException("Unable to remove to directory @directory", NULL, array('@directory' => $directory)); } } - function removeFile($destination) { - if (!ftp_delete($this->connection, $item)) { - throw new FileTransferException("Unable to remove to file @file", NULL, array('@file' => $item)); + protected function removeFileJailed($destination) { + if (!ftp_delete($this->connection, $destination)) { + 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)) { + $result = TRUE; } + ftp_chdir($this->connection, $curr); + return $result; } } Index: includes/filetransfer/ssh.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/filetransfer/ssh.inc,v retrieving revision 1.1 diff -u -r1.1 ssh.inc --- includes/filetransfer/ssh.inc 23 Jun 2009 12:11:19 -0000 1.1 +++ includes/filetransfer/ssh.inc 1 Jul 2009 12:34:51 -0000 @@ -6,52 +6,69 @@ */ class FileTransferSSH extends FileTransfer { - function __construct($settings) { - // This is the default, if $settings contains a port, this will be overridden. - $this->port = 22; - parent::__construct($settings); + function __construct($jail, $username, $password, $hostname = "localhost", $port = 22) { + parent::__construct($jail, $username, $password, $hostname, $port); } function connect() { - $this->connection = @ssh2_connect($setings['hostname'], $this->port); + $this->connection = @ssh2_connect($this->hostname, $this->port); if (!$this->connection) { - throw new FileTransferException('SSH Connection failed.'); + throw new FileTransferException('SSH Connection failed to @host:@port', NULL, array('@host' => $this->hostname, '@port' => 21)); } if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) { throw new FileTransferException('The supplied username/password combination was not accepted.'); } } - function copyFile($source, $destination) { + static function factory($jail, $settings) { + $settings['hostname'] = empty($settings['hostname']) ? 'localhost' : $settings['hostname']; + $settings['port'] = empty($settings['port']) ? 22 : $settings['port']; + return new FileTransferSSH($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); + } + + protected function copyFileJailed($source, $destination) { if (!@ssh2_scp_send($this->connection, $source, $destination)) { throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination)); } } - function copyDirectory($source, $destination) { - if (!@ssh2_exec($this->connection, 'cp -Rp ' . escapeshellarg($source) . ' ' . escapeshellarg($destination))) { + protected function copyDirectoryJailed($source, $destination) { + if (@!ssh2_exec($this->connection, 'cp -Rp ' . escapeshellarg($source) . ' ' . escapeshellarg($destination))) { throw new FileTransferException('Cannot copy directory @directory.', NULL, array('@directory' => $source)); } } - function createDirectory($directory) { - if (!@ssh2_exec($this->connection, 'mkdir ' . escapeshellarg($directory))) { + protected function createDirectoryJailed($directory) { + if (@!ssh2_exec($this->connection, 'mkdir ' . escapeshellarg($directory))) { throw new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory)); } } - function removeDirectory($directory) { - if (realpath(substr($directory, 0, strlen(DRUPAL_ROOT))) !== DRUPAL_ROOT) { - throw new FileTransferException('@directory is outside of the Drupal root.', NULL, array('@directory' => $directory)); - } - if (!@ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg($directory))) { + protected function removeDirectoryJailed($directory) { + if (@!ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg($directory))) { throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory)); } } - - function removeFile($destination) { + + protected function removeFileJailed($destination) { if (!@ssh2_exec($this->connection, 'rm ' . escapeshellarg($destination))) { throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $destination)); } } + + /** + * WARNING: This is untested. It is not currently used, but should do the trick. + */ + public function isDirectory($path) { + $directory = escapeshellarg($path); + $cmd = "[ -d {$directory} ] && 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)); + } + } } Index: modules/simpletest/simpletest.info =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v retrieving revision 1.6 diff -u -r1.6 simpletest.info --- modules/simpletest/simpletest.info 8 Jun 2009 09:23:53 -0000 1.6 +++ modules/simpletest/simpletest.info 1 Jul 2009 12:34:51 -0000 @@ -19,6 +19,7 @@ files[] = tests/database_test.test files[] = tests/error.test files[] = tests/file.test +files[] = tests/filetransfer.test files[] = tests/form.test files[] = tests/graph.test files[] = tests/image.test Index: modules/simpletest/tests/filetransfer.test =================================================================== RCS file: modules/simpletest/tests/filetransfer.test diff -N modules/simpletest/tests/filetransfer.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/filetransfer.test 1 Jul 2009 12:34:51 -0000 @@ -0,0 +1,199 @@ + t('FileTransfer unit tests'), + 'description' => t('Test that the jail is respected and that protocols using recursive file move operations work.'), + 'group' => t('System') + ); + } + + function setUp() { + $this->testConnection = TestFileTransfer::factory(DRUPAL_ROOT, array('hostname' => $this->hostname, 'username' => $this->username, 'password' => $this->password, 'port' => $this->port)); + } + + function _getFakeModuleFiles() { + $files = array( + 'fake.module', + 'fake.info', + 'theme' => array( + 'fake.tpl.php' + ), + 'inc' => array( + 'fake.inc' + ) + ); + return $files; + } + + function _buildFakeModule() { + $location = file_directory_temp() . '/fake'; + if (is_dir($location)) { + $ret = 0; + $output = array(); + exec('rm -Rf ' . escapeshellarg($location), $output, $ret); + if ($ret != 0) { + throw new Exception('Error removing fake module directory.'); + } + } + + $files = $this->_getFakeModuleFiles(); + $this->_writeDirectory($location, $files); + return $location; + } + + function _writeDirectory($base, $files = array()) { + mkdir($base); + foreach ($files as $key => $file) { + if (is_array($file)) { + $this->_writeDirectory($base . DIRECTORY_SEPARATOR . $key, $file); + } + else { + //just write the filename into the file + file_put_contents($base . DIRECTORY_SEPARATOR . $file, $file); + } + } + } + + function testJail() { + $source = $this->_buildFakeModule(); + + // This convoluted piece of code is here because our testing framework does + // not support expecting exceptions. + $gotit = FALSE; + try { + $this->testConnection->copyDirectory($source, '/tmp'); + } + catch (FileTransferException $e) { + $gotit = TRUE; + } + $this->assertTrue($gotit, 'Was not able to copy a directory outside of the jailed area.'); + + $gotit = TRUE; + try { + $this->testConnection->copyDirectory($source, DRUPAL_ROOT . '/'. file_directory_path()); + } + catch (FileTransferException $e) { + $gotit = FALSE; + } + $this->assertTrue($gotit, 'Was able to copy a directory inside of the jailed area'); + } + + function testCopyDirectory() { + $directory = $this->_buildFakeModule(); + $drupal_root = DRUPAL_ROOT; + + $this->testConnection->shouldIsDirectoryReturnTrue = TRUE; + $this->testConnection->copyDirectory($directory, "{$drupal_root}/sites/all/modules"); + $expected_commands = array( + "mkdir {$drupal_root}/sites/all/modules/fake", + "copyFile {$directory}/fake.info {$drupal_root}/sites/all/modules/fake/fake.info", + "copyFile {$directory}/fake.module {$drupal_root}/sites/all/modules/fake/fake.module", + "mkdir {$drupal_root}/sites/all/modules/fake/inc", + "copyFile {$directory}/inc/fake.inc {$drupal_root}/sites/all/modules/fake/inc/fake.inc", + "mkdir {$drupal_root}/sites/all/modules/fake/theme", + "copyFile {$directory}/theme/fake.tpl.php {$drupal_root}/sites/all/modules/fake/theme/fake.tpl.php", + ); + + $received_commands = $this->testConnection->connection->flushCommands(); + $this->assertEqual($received_commands, $expected_commands, 'Expected copy files operations made to sites/all/modules'); + + $this->testConnection->shouldIsDirectoryReturnTrue = FALSE; + $this->testConnection->copyDirectory($directory, "{$drupal_root}/sites/all/modules/fake"); + $expected_commands = array( + "mkdir {$drupal_root}/sites/all/modules/fake", + "copyFile {$directory}/fake.info {$drupal_root}/sites/all/modules/fake/fake.info", + "copyFile {$directory}/fake.module {$drupal_root}/sites/all/modules/fake/fake.module", + "mkdir {$drupal_root}/sites/all/modules/fake/inc", + "copyFile {$directory}/inc/fake.inc {$drupal_root}/sites/all/modules/fake/inc/fake.inc", + "mkdir {$drupal_root}/sites/all/modules/fake/theme", + "copyFile {$directory}/theme/fake.tpl.php {$drupal_root}/sites/all/modules/fake/theme/fake.tpl.php", + ); + + $received_commands = $this->testConnection->connection->flushCommands(); + dd($expected_commands); + dd($received_commands); + $this->assertEqual($received_commands, $expected_commands, 'Expected copy files operations made to sites/all/modules/fake'); + + + } +} + +/** + * Mock FileTransfer object for test case. + */ +class TestFileTransfer extends FileTransfer { + protected $host = NULL; + protected $username = NULL; + protected $password = NULL; + protected $port = NULL; + + /** + * This is for testing the CopyRecursive logic. + */ + public $shouldIsDirectoryReturnTrue = FALSE; + + function __construct($jail, $username, $password, $hostname = 'localhost', $port = 9999) { + parent::__construct($jail, $username, $password, $hostname, $port); + } + + static function factory($jail, $settings) { + return new TestFileTransfer($jail, $settings['username'], $settings['password'], $settings['hostname'], $settings['port']); + } + + function connect() { + $parts = explode(':', $this->hostname); + $port = (count($parts) == 2) ? $parts[1] : $this->port; + $this->connection = new MockTestConnection(); + $this->connection->connectionString = 'test://' . urlencode($this->username) . ':' . urlencode($this->password) . "@$this->host:$this->port/"; + } + + function copyFileJailed($source, $destination) { + $this->connection->run("copyFile $source $destination"); + } + + protected function removeDirectoryJailed($directory) { + $this->connection->run("rmdir $directory"); + } + + function createDirectoryJailed($directory) { + $this->connection->run("mkdir $directory"); + } + + function removeFileJailed($destination) { + if (!ftp_delete($this->connection, $item)) { + throw new FileTransferException('Unable to remove to file @file.', NULL, array('@file' => $item)); + } + } + + function isDirectory($path) { + return $this->shouldIsDirectoryReturnTrue; + } +} + +/** + * Mock connection object for test case. + */ +class MockTestConnection { + + var $commandsRun = array(); + var $connectionString; + + function run($cmd) { + $this->commandsRun[] = $cmd; + } + + function flushCommands() { + $out = $this->commandsRun; + $this->commandsRun = array(); + return $out; + } +}