=== added directory 'includes/connection' === added file 'includes/connection/connection.inc' --- includes/connection/connection.inc 1970-01-01 00:00:00 +0000 +++ includes/connection/connection.inc 2009-06-20 18:28:10 +0000 @@ -0,0 +1,126 @@ +username = $settings['username']; + $this->password = $settings['password']; + $this->hostname = isset($settings['hostname']) ? $settings['hostname'] : 'localhost'; + if (isset($settings['port'])) { + $this->port = $settings['port']; + } + } + + /** + * 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 + * result; afterwards, the connection will be returned directly without using + * this method. + */ + function __get($name) { + static $connection; + if ($name == 'connection') { + $this->connection = $this->connect(); + return $this->connection; + } + } + + /** + * Copies an extension (theme, theme_engine, module). + * + * @param $source + * The path to the files belonging to the extension. + * @param $type + * The type of the extension (i.e. theme, theme_engine, module). + * @param $name + * The name of the extension to be copied. + */ + function addExtension($source, $type, $name) { + $path = drupal_get_path($type, $name); + if (empty($path)) { + // This default is a workaround for drupal_get_path()'s inability to find + // the path of modules that do not yet exist. We fall back to sites/all/* + // for now, although it would be nice in the future to be able to specify + // which folder an extension should be placed in (to fully utilize Drupal + // multisite features). + $path = 'sites/all/' . $type . 's/' . $name; + } + return $this->copyDirectory($source, DRUPAL_ROOT . '/' . $path); + } + + /** + * Copies a directory. + * + * @param $source + * The source path. + * @param $destination + * The destination path. + */ + protected function copyDirectory($source, $destination) { + $this->mkdir($destination . basename($source)); + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) { + $relative_path = basename($source) . substr($filename, strlen($source)); + if ($file->isDir()) { + $this->mkdir($destination . $relative_path); + } + else { + $this->copyFile($file->getPathName(), $destination . $relative_path); + } + } + } + + /** + * Creates a directory. + * + * @param $directory + * The directory to be created. + */ + abstract function mkdir($directory); + + /** + * Removes a directory. + * + * @param $directory + * The directory to be removed. + */ + abstract function rmdir($directory); + + /** + * Copies a file. + * + * @param $source + * The source file. + * @param $destination + * The destination file. + */ + abstract function copyFile($source, $destination); + +} + +/** + * ConnectionException class. + */ +class ConnectionException extends Exception { + public $params = array(); + function __construct($message, $code, $params = array()) { + $this->params = $params; + parent::__construct($message, $code); + } + + public function getParams() { + return $this->params; + } +} === added file 'includes/connection/ftp.inc' --- includes/connection/ftp.inc 1970-01-01 00:00:00 +0000 +++ includes/connection/ftp.inc 2009-06-20 18:22:25 +0000 @@ -0,0 +1,109 @@ +port = 21; + parent::__construct($settings); + } +} + +/** + * Connection class using the FTP URL wrapper. + */ +class UpdateFTPWrapper extends UpdateFTPConnection { + function connect() { + $this->connection = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . '@' . $this->hostname . ':' . $this->port . '/'; + if (!is_dir($this->connection)) { + throw new ConnectionException('FTP Connection failed.'); + } + } + + function copyFile($source, $destination) { + if (!@copy($this->connection . '/' . $source, $this->connection . '/' . $destination)) { + throw new ConnectionException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination)); + } + } + + function rmDir($directory) { + if (is_dir($directory)) { + $dh = opendir($directory); + while (($resource = readdir($dh)) !== FALSE) { + if ($resource == '.' || $resource == '..') { + continue; + } + $full_path = $directory . DIRECTORY_SEPARATOR . $resource; + if (is_file($full_path)) { + unlink($full_path); + } elseif (is_dir($full_path)) { + $this->_rmDir($full_path . '/'); + } + } + closedir($dh); + if (!rmdir($directory)) { + $exception = new ConnectionException('Cannot remove @directory.', NULL, array('@directory' => $directory)); + throw $exception; + } + } + } + + function mkdir($directory) { + if (!@mkdir($directory)) { + $exception = new ConnectionException('Cannot create directory @directory.', NULL, array('@directory' => $directory)); + throw $exception; + } + } +} + +class UpdateFTPExtension extends UpdateFTPConnection { + function connect() { + $this->connection = ftp_connect($this->hostname, $this->port); + + if (!$this->connection) { + throw new ConnectionException("Cannot connect to FTP Server, please check settings"); + } + if (!ftp_login($this->connection, $this->username, $this->password)) { + throw new ConnectionException("Cannot login to FTP server, please check username and password"); + } + } + + function copyFile($source, $destination) { + if (!@ftp_put($this->connection, $destination, $source, FTP_BINARY)) { + throw new ConnectionException("Cannot move @source to @destination", NULL, array("@source" => $source, "@destination" => $destination)); + } + } + + function mkdir($directory) { + if (!@ftp_mkdir($this->connection, $directory)) { + throw new ConnectionException("Cannot create directory @directory", NULL, array("@directory" => $directory)); + } + } + + function rmdir($directory) { + $pwd = ftp_pwd($this->connection); + if (!@ftp_chdir($this->connection, $directory)) { + throw new ConnectionException("Unable to change to directory @directory", NULL, array('@directory' => $directory)); + } + $list = @ftp_nlist($this->connection, '.'); + foreach ($list as $item){ + if ($item == '.' || $item == '..') { + continue; + } + if (@ftp_chdir($this->connection, $item)){ + ftp_chdir($this->connection, '..'); + $this->rmdir($item); + } + elseif (!ftp_delete($this->connection, $item)) { + throw new ConnectionException("Unable to remove to file @file", NULL, array('@file' => $item)); + } + } + ftp_chdir($this->connection, $pwd); + if (!ftp_rmdir($this->connection, $directory)) { + throw new ConnectionException("Unable to remove to directory @directory", NULL, array('@directory' => $directory)); + } + } +} === added file 'includes/connection/ssh.inc' --- includes/connection/ssh.inc 1970-01-01 00:00:00 +0000 +++ includes/connection/ssh.inc 2009-06-20 18:32:53 +0000 @@ -0,0 +1,51 @@ +port = 22; + parent::__construct($settings); + } + + function connect() { + $this->connection = @ssh2_connect($setings['hostname'], $this->port); + if (!$this->connection) { + throw new ConnectionException('SSH Connection failed.'); + } + if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) { + throw new ConnectionException('The supplied username/password combination was not accepted.'); + } + } + + function copyFile($source, $destination) { + if (!@ssh2_scp_send($this->connection, $source, $destination)) { + throw new ConnectionException('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))) { + throw new ConnectionException('Cannot copy directory @directory.', NULL, array('@directory' => $source)); + } + } + + function mkdir($directory) { + if (!@ssh2_exec($this->connection, 'mkdir ' . escapeshellarg("$directory"))) { + throw new ConnectionException('Cannot create directory @directory.', NULL, array('@directory' => $directory)); + } + } + + function rmdir($directory) { + if (realpath(substr($directory, 0, strlen(DRUPAL_ROOT))) !== DRUPAL_ROOT) { + throw new ConnectionException('@directory is outside of the Drupal root.', NULL, array('@directory' => $directory)); + } + if (!@ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg("$directory"))) { + throw new ConnectionException('Cannot remove @directory.', NULL, array('@directory' => $directory)); + } + } +} === added file 'includes/connection/test.inc' --- includes/connection/test.inc 1970-01-01 00:00:00 +0000 +++ includes/connection/test.inc 2009-06-20 18:22:25 +0000 @@ -0,0 +1,53 @@ +port = 9999; + parent::__construct($settings); + } + + function connect() { + $this->connection = new MockTestConnection($settings); + $this->connection->connectionString = 'test://' . urlencode($this->username) . ':' . urlencode($this->password) . '@' . $this->hostname . ':' . $this->port . '/'; + } + + function getConnection() { + return $this->connection; + } + + function copyFile($source, $destination) { + $this->connection->run("copyFile $source $destination"); + } + + function rmDir($directory) { + $this->connection->run("rmdir $directory"); + } + + function mkdir($directory) { + $this->connection->run("mkdir $directory"); + } + + function copyDirectory($source, $destination) { + $this->connection->run("copyDirectory $source $destination"); + } +} + +class MockTestConnection { + var $commandsRun = array(); + var $connectionString; + + function run($cmd) { + $this->commandsRun[] = $cmd; + } + + function flushCommands() { + $out = $this->commandsRun; + $this->commandsRun = array(); + return $out; + } +} === modified file 'modules/simpletest/simpletest.info' --- modules/simpletest/simpletest.info 2009-06-08 09:23:50 +0000 +++ modules/simpletest/simpletest.info 2009-06-20 18:23:28 +0000 @@ -16,6 +16,7 @@ files[] = tests/batch.test files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test +files[] = tests/connection.test files[] = tests/database_test.test files[] = tests/error.test files[] = tests/file.test === added file 'modules/simpletest/tests/connection.test' --- modules/simpletest/tests/connection.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/connection.test 2009-06-20 18:22:25 +0000 @@ -0,0 +1,113 @@ + t('Update Connection Unit Test'), + 'description' => t('Test that the template pattern as expressed in Connection.php chooses the right workflow'), + 'group' => t('Update'), + ); + } + + function setUp() { + $this->testConnection = new UpdateTestConnection(array('host' => $this->host, 'username' => $this->username, 'password' => $this->password, 'port' => $this->port)); + $this->testConnection->connect(); + } + + /** + * Tests the connection string to make sure it is properly formed. + */ + function testConnect() { + $this->assertEqual($this->testConnection->getConnection()->connectionString, 'test://' . urlencode($this->username) . ':' . urlencode($this->password) . "@{$this->host}:{$this->port}/", t('Connection string correctly built.')); + $this->assertNotEqual($this->testConnection->getConnection()->connectionString, "bull://{$this->username}:{$this->password}@{$this->host}:{$this->port}", t('Connection string correctly built.')); + } + + /** + * Defines the files of a fake module. + */ + protected function getFakeModuleFiles() { + $files = array( + 'fake.module', + 'fake.info', + 'theme' => array( + 'fake.tpl.php' + ), + 'inc' => array( + 'fake.inc' + ) + ); + return $files; + } + + /** + * Adds the fake module to the temporary file directory. + */ + protected function buildFakeModule() { + $location = file_directory_temp() . '/fake'; + $this->removeDirectory($location); + + $files = $this->getFakeModuleFiles(); + $this->writeDirectory($location, $files); + return $location; + } + + /** + * Helper function for buildFakeModule() - writes the fake module to the file + * system. + */ + protected function writeDirectory($base, $files = array()) { + return; + mkdir($base); + foreach ($files as $key => $file) { + if (is_array($file)) { + $this->writeDirectory($base . DIRECTORY_SEPARATOR . $key, $file); + } + else { + // Just write the filename to the file. + file_put_contents($base . DIRECTORY_SEPARATOR . $file, $file); + } + } + } + + /** + * Helper function for buildFakeModule() - deletes the module directory if it + * already exists. + */ + protected function removeDirectory($base) { + if (is_dir($base) && $directory = @opendir($base)) { + while (($file = readdir($directory)) !== FALSE) { + if (is_file("$base/$file")) { + if (!@unlink("$base/$file")) { + throw new Exception(t('Could not remove file @file.', array('@file' => $file))); + } + } + elseif (is_dir("$base/$file")) { + if ($file != '.' && $file != '..') { + $this->removeDirectory("$base/$file"); + } + } + } + closedir($directory); + if (!@rmdir($base)) { + throw new Exception(t('Could not remove directory @directory.', array('@directory' => $base))); + } + } + } + + function testPutModule() { + $directory = $this->buildFakeModule(); + $drupal_root = DRUPAL_ROOT; + $this->testConnection->addExtension($directory, 'module', 'fake'); + + $expected_commands = array("copyDirectory {$directory} {$drupal_root}/sites/all/modules/fake"); + $received_commands = $this->testConnection->getConnection()->flushCommands(); + + $this->assertEqual($received_commands, $expected_commands, "Expected copy directory operation made"); + } +} === modified file 'modules/update/update.module' --- modules/update/update.module 2009-06-08 05:00:11 +0000 +++ modules/update/update.module 2009-06-20 18:22:25 +0000 @@ -146,6 +146,12 @@ function update_menu() { 'access arguments' => array('administer site configuration'), 'type' => MENU_CALLBACK, ); + $items['admin/update/download-and-install'] = array( + 'title' => 'Download a module and install it * REMOVE THIS BEFORE RELEASE! THIS IS A DEMO! * ', + 'page callback' => 'update_download_install', + 'access arguments' => array('administer site configuration'), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -632,3 +638,117 @@ function update_flush_caches() { /** * @} End of "defgroup update_status_cache". */ + +/** + * 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; +} + +/** + * Implementation of hook_update_connections(). + */ +function update_update_connections() { + $connections = array(); + + // SSH2 lib connection is only available if the proper PHP extension is + // installed. + if (function_exists('ssh2_connect')) { + $connections['ssh'] = array( + 'title' => t('SSH'), + 'class' => 'UpdateSSHConnection', + ); + } + if (function_exists('ftp_connect')) { + $connections['ftp_extension'] = array( + 'title' => t('FTP Extension'), + 'class' => 'UpdateFTPExtension', + ); + } + + if (ini_get('allow_url_fopen')) { + $connections['ftp_wrapper'] = array( + 'title' => t('FTP Wrapper'), + 'class' => 'UpdateFTPWrapper', + ); + } + return $connections; +} + +/** + * Attempts to untar a file into a directory. + * + * @param $file + * The file to untar. + * @param $directory + * The directory to untar to. + * @return + * TRUE on success, or FALSE on failure. + */ +function update_untar($file, $directory) { + if (substr($directory, 0, 1) != DIRECTORY_SEPARATOR) { + $directory = DIRECTORY_SEPARATOR . $directory; + } + $directory = file_directory_temp() . '/update-extraction' . $directory; + if (!file_exists($directory)) { + mkdir($directory); + } + $archive_tar = new Archive_tar($file, TRUE); + return $archive_tar->extract($directory); +} + +/** + * Sample (simplified) page callback to handle an update from update status. + * REMOVE THIS BEFORE RELEASE! THIS IS A DEMO! + */ +function update_download_install($type, $name, $password) { + $connections = module_invoke_all('update_connections'); + if (!$connections) { + return t('No available connection backend.'); + } + $available = update_get_available(); + module_load_include('inc', 'update', 'update.compare'); + $data = update_calculate_project_data($available); + if (empty($data[$name])) { + return t('@module could not be upgraded.', array('@module' => $name)); + } + $project = $data[$name]; + $tarball = update_get_file($project['releases'][$project['latest_version']]['download_link']); + $directory = "$type-$name"; + $files = update_untar($tarball, $directory); + $source = $directory . $files[0]; + + $settings = variable_get('update_connection_settings', array()); + $settings['password'] = $password; + $connection_class = $connections[variable_get('update_preferred_connection', 'ftp_wrapper')]['class']; + $connection = new $connection_class($settings); + // @TODO do we want to remove the extension first? Maybe -- but usually files + // are overwriteable and the possible failures and race conditions are + // problematic. + $connection->addExtension($source, $type, $name); + drupal_goto('admin/reports/updates'); +}