=== modified file 'modules/system/system.info' --- modules/system/system.info 2009-06-08 09:23:50 +0000 +++ modules/system/system.info 2009-06-09 05:52:02 +0000 @@ -10,4 +10,5 @@ files[] = system.queue.inc files[] = image.gd.inc files[] = system.install files[] = system.test +files[] = system.untar.inc required = TRUE === modified file 'modules/system/system.module' --- modules/system/system.module 2009-06-08 04:28:19 +0000 +++ modules/system/system.module 2009-06-09 05:51:51 +0000 @@ -2511,3 +2511,22 @@ function system_image_toolkits() { ), ); } + +/** +* Implementation of hook_update_untar(). +*/ +function system_update_untar() { + $untar_methods = array(); + + // See if we have a way to untar the files. + $handle = popen('tar --version', 'r'); + if (fgets($handle)) { + $untar_methods['system_untar_cli'] = array( + 'title' => t('Command line untar'), + 'class' => 'SystemUntar', + ); + } + pclose($handle); + + return $untar_methods; +} === added file 'modules/system/system.untar.inc' --- modules/system/system.untar.inc 1970-01-01 00:00:00 +0000 +++ modules/system/system.untar.inc 2009-06-09 05:49:16 +0000 @@ -0,0 +1,39 @@ +username = $settings['username']; + $this->password = $settings['password']; + $this->hostname = isset($settings['hostname']) ? $settings['hostname'] : 'localhost'; + if (isset($settings['port'])) { + $this->port = $settings['port']; + } + } + + 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) { + return $this->copyDirectory($source, DRUPAL_ROOT . '/' . drupal_get_path($type, $name)); + } + + /** + * Copies a directory. + * + * @param $source + * The source path. + * @param $destination + * The destination path. + */ + 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 'modules/update/connection/update.ftp.inc' --- modules/update/connection/update.ftp.inc 1970-01-01 00:00:00 +0000 +++ modules/update/connection/update.ftp.inc 2009-06-09 05:06:59 +0000 @@ -0,0 +1,108 @@ +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->hostname . '/'; + 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 'modules/update/connection/update.ssh.inc' --- modules/update/connection/update.ssh.inc 1970-01-01 00:00:00 +0000 +++ modules/update/connection/update.ssh.inc 2009-06-09 04:50:34 +0000 @@ -0,0 +1,49 @@ +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 (!@ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg("$directory"))) { + throw new ConnectionException("Cannot remove @directory", null, array("@directory" => $directory)); + } + } +} === added file 'modules/update/connection/update.test.inc' --- modules/update/connection/update.test.inc 1970-01-01 00:00:00 +0000 +++ modules/update/connection/update.test.inc 2009-06-09 05:07:08 +0000 @@ -0,0 +1,52 @@ +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->hostname . '/'; + } + + 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/update/update.info' --- modules/update/update.info 2008-10-11 02:32:32 +0000 +++ modules/update/update.info 2009-06-09 01:46:05 +0000 @@ -10,3 +10,7 @@ files[] = update.fetch.inc files[] = update.report.inc files[] = update.settings.inc files[] = update.install +files[] = connection/update.ftp.inc +files[] = connection/update.ssh.inc +files[] = connection/update.test.inc +files[] = connection/update.connection.inc === modified file 'modules/update/update.module' --- modules/update/update.module 2009-06-08 05:00:11 +0000 +++ modules/update/update.module 2009-06-09 05:54:17 +0000 @@ -146,6 +146,13 @@ 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 +639,160 @@ 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; +} + + +/** + * 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; +} + +/** +* 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; +} + +function update_untar($untar_methods, $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); + } + $untar_class = $untar_methods[variable_get('update_preferred_untar', 'system_untar_cli')]['class']; + return new $untar_class->untar($file, $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_connection'); + if (!$connections) { + return t('No available connection backend.'); + } + $untar_methods = module_invoke_all('update_untar'); + if (!$untar_methods) { + return t('Nothing to untar with.'); + } + $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' => $module_name)); + } + $tarball = update_get_file($project['releases'][$project['latest_version']]['download_link']); + $directory = file_directory_temp() . '/update-extraction/'; + $files = update_untar($untar_methods, $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'); +} === added file 'modules/update/update.test' --- modules/update/update.test 1970-01-01 00:00:00 +0000 +++ modules/update/update.test 2009-06-09 00:16:45 +0000 @@ -0,0 +1,108 @@ + 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 = TestConnection::factory(DRUPAL_ROOT, array('host' => $this->host, 'username' => $this->username, 'password' => $this->password, 'port' => $this->port)); + } + + function testConnect() { + $this->testConnection->connect(); + $this->assertEqual($this->testConnection->getConnection()->connectionString, 'test://' . urlencode($this->username) . ':' . urlencode($this->password) . "@{$this->host}:{$this->port}/", "Connection String correctly built"); + //fail assertion + $this->assertNotEqual($this->testConnection->getConnection()->connectionString, "bull://{$this->username}:{$this->password}@{$this->host}:{$this->port}", "Connection String correctly built"); + } + + 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 testPutModule() { + $directory = $this->_buildFakeModule(); + $drupal_root = DRUPAL_ROOT; + $this->testConnection->canCopyDirs = TRUE; + $this->testConnection->addModule($directory); + + $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"); + + $this->testConnection->canCopyDirs = FALSE; + $this->testConnection->addModule($directory); + $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->getConnection()->flushCommands(); + $this->assertEqual($received_commands, $expected_commands, "Expected copy files operations made"); + } + + function testUntar() { + // I think this is a tad convoluted... why are we passing this method in. + // We should just have a untar command in common.inc which will determine what library / shell to use and do it. + $files = update_untar(dirname(__FILE__) . '/test-assets/fake.tar.gz'); + $this->assertTrue(is_dir(file_directory_temp() . "/update-extraction/fake"), "Created directory from tar extraction"); + } +}