Index: includes/file.inc =================================================================== --- includes/file.inc (revision 1) +++ includes/file.inc (working copy) @@ -1595,6 +1595,39 @@ return FALSE; } + +/** + * The standard untar callback that simply uses the command line tar command to + * untar the file. + * + * @param $file + * The file to untar. + * @param $directory + * The destination directory of the file; + * @return + * An array containing the locations of the extracted files, or FALSE on + * failure. + */ +function untar($file, $directory = NULL) { + $file_safe = escapeshellarg($file); + $directory_safe = escapeshellarg($directory); + $file_list = array(); + + // Try to use tar to extract the files. + if (function_exists('popen')) { + $cmd = "tar -zvxf $file_safe -C $directory_safe"; + $handle = popen($cmd, 'r'); + while ($line = fgets($handle)) { + $file_list[] = trim($line); + } + pclose($handle); + } + + // If tar returned something, then it is present, so return it. + if (!empty($file_list)) { + return $file_list; + } +} /** * @} End of "defgroup file". */ \ No newline at end of file Index: modules/update/test-assets/fake.tar.gz =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: modules/update/test-assets/fake.tar.gz ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: modules/update/update.test =================================================================== --- modules/update/update.test (revision 0) +++ modules/update/update.test (revision 0) @@ -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"); + } +} Index: modules/update/update.fetch.inc =================================================================== --- modules/update/update.fetch.inc (revision 1) +++ modules/update/update.fetch.inc (working copy) @@ -1,5 +1,5 @@ $project) { $url = _update_build_fetch_url($project, $site_key); - $fetch_url_base = _update_get_fetch_url_base($project); - if (empty($fail[$fetch_url_base]) || count($fail[$fetch_url_base]) < $max_fetch_attempts) { - $xml = drupal_http_request($url); - if (isset($xml->data)) { - $data[] = $xml->data; - } - else { - // Connection likely broken; prepare to give up. - $fail[$fetch_url_base][$key] = 1; - } - } - else { - // Didn't bother trying to fetch. - $fail[$fetch_url_base][$key] = 1; + $xml = drupal_http_request($url); + if (isset($xml->data)) { + $data[] = $xml->data; } } @@ -71,21 +57,14 @@ $available = update_parse_xml($data); } if (!empty($available) && is_array($available)) { - // Record the projects where we failed to fetch data. - foreach ($fail as $fetch_url_base => $failures) { - foreach ($failures as $key => $value) { - $available[$key]['project_status'] = 'not-fetched'; - } - } $frequency = variable_get('update_check_frequency', 1); _update_cache_set('update_available_releases', $available, REQUEST_TIME + (60 * 60 * 24 * $frequency)); - watchdog('update', 'Attempted to fetch information about all available new releases and updates.', array(), WATCHDOG_NOTICE, l(t('view'), 'admin/reports/updates')); + variable_set('update_last_check', REQUEST_TIME); + watchdog('update', 'Fetched information about all available new releases and updates.', array(), WATCHDOG_NOTICE, l(t('view'), 'admin/reports/updates')); } else { watchdog('update', 'Unable to fetch any information about available new releases and updates.', array(), WATCHDOG_ERROR, l(t('view'), 'admin/reports/updates')); } - // Whether this worked or not, we did just (try to) check for updates. - variable_set('update_last_check', REQUEST_TIME); return $available; } @@ -105,8 +84,12 @@ * @see update_get_projects() */ function _update_build_fetch_url($project, $site_key = '') { + $default_url = variable_get('update_fetch_url', UPDATE_DEFAULT_URL); + if (!isset($project['info']['project status url'])) { + $project['info']['project status url'] = $default_url; + } $name = $project['name']; - $url = _update_get_fetch_url_base($project); + $url = $project['info']['project status url']; $url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY; // Only append a site_key and the version information if we have a site_key // in the first place, and if this is not a disabled module or theme. We do @@ -124,22 +107,6 @@ } /** - * Return the base of the URL to fetch available update data for a project. - * - * @param $project - * The array of project information from update_get_projects(). - * @return - * The base of the URL used for fetching available update data. This does - * not include the path elements to specify a particular project, version, - * site_key, etc. - * - * @see _update_build_fetch_url() - */ -function _update_get_fetch_url_base($project) { - return isset($project['info']['project status url']) ? $project['info']['project status url'] : variable_get('update_fetch_url', UPDATE_DEFAULT_URL); -} - -/** * Perform any notifications that should be done once cron fetches new data. * * This method checks the status of the site using the new data and depending Index: modules/update/update.module =================================================================== --- modules/update/update.module (revision 1) +++ modules/update/update.module (working copy) @@ -1,5 +1,6 @@ array('administer site configuration'), 'type' => MENU_CALLBACK, ); + $items['admin/update/download-and-install'] = array( + 'title' => 'Download a module and install it', + 'page callback' => 'update_download_install', + 'access arguments' => array('administer site configuration'), + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -267,7 +271,6 @@ break; case UPDATE_UNKNOWN: case UPDATE_NOT_CHECKED: - case UPDATE_NOT_FETCHED: $requirement_label = isset($project['reason']) ? $project['reason'] : t('Can not determine status'); $requirement['severity'] = REQUIREMENT_WARNING; break; @@ -296,6 +299,50 @@ } /** +* 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' => 'SSHConnection', + 'class_file_location' => drupal_get_path('module', 'update') . '/includes/Connection/SSHConnection.php', + ); + } + // FTP connection is only available if the proper PHP extension is installed or + // allow_url_fopen is on. + if (function_exists('ftp_connect') || ini_get('allow_url_fopen')) { + $connections['ftp'] = array( + 'title' => t('FTP'), + 'class' => 'FTPConnection', + 'class_file_location' => drupal_get_path('module', 'update') . '/includes/Connection/FTPConnection.php', + ); + } + return $connections; +} + +/** +* Implementation of hook_update_untar(). +*/ +function update_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['update_untar_command'] = t('Command line untar'); + } + pclose($handle); + + return $untar_methods; +} + + +/** * Implement hook_form_FORM_ID_alter(). * * Adds a submit handler to the system modules and themes forms, so that if a @@ -483,7 +530,6 @@ case UPDATE_UNKNOWN: case UPDATE_NOT_CHECKED: - case UPDATE_NOT_FETCHED: if ($msg_type == 'core') { $text = t('There was a problem determining the status of available updates for your version of Drupal.', array(), array('langcode' => $langcode)); } @@ -533,7 +579,7 @@ * cache_set(), cache_get(), and cache_clear_all(), there are private helper * functions that implement these same basic tasks but ensure that the cache * is not prematurely cleared, and that the data is always stored in the - * database, even if memcache or another cache backend is in use. + * database, even if memcache or another cache connection is in use. */ /** @@ -632,3 +678,251 @@ /** * @} 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; +} + +/** + * Untars a file into file_directory_temp() . '/update-extraction' + * + * @param $file + * The file to untar. + * @param $directory + * The destination directory of the file; relative to file_directory_temp() . '/update-extraction' + * @return + * An array containing the locations of the extracted files, or FALSE on + * failure. + */ +function update_untar($file, $directory = NULL) { + if (substr($directory,0,1) != DIRECTORY_SEPARATOR) { + $directory = DIRECTORY_SEPARATOR . $directory; + } + $directory = file_directory_temp() . '/update-extraction' . $directory; + if (!file_exists($directory)) { + mkdir($directory); + } + return untar($file, $directory); +} + +/** + * 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; +} + +/** + * See if everything that is needed to use the extension manager is available. + * + * @return + * TRUE if all of the required dependencies are available, FALSE otherwise. + */ +function update_extensions_runnable() { + // See if we have any available connections. + $connections = update_list_connections(); + $untar_methods = update_list_untar_methods(); + return !empty($connections) && !empty($untar_methods); +} + +/** + * Add an extension to the file system. + * + * @param $connection_name + * The machine-readable name of the connection type being used to access the + * server's filesystem; for example, 'ftp'. + * @param $directory + * The directory where the untar'd extension is sitting + * @param $settings + * An array of settings, which will vary among different connections, but may + * include data such as the username, password, and host. + * @return + * TRUE on success, or FALSE on failure. + */ +function update_add_extension($connection_name, $directory, $settings) { + $valid_connections = update_list_connections(); + $active_connection = $valid_connections[$connection_name]; + if (!$active_connection) { + drupal_set_message(t("Could not use @connection connection, your server does not support it", array('@connection' => $connection_name))); + return FALSE; + } + require_once($active_connection['class_file_location']); + $connection_class = $active_connection['class']; + $connection = call_user_func("{$connection_class}::factory", DRUPAL_ROOT, $settings); + $extension_type = _update_get_extension_type($directory); + + try { + if (UPDATE_EXTENSION_TYPE_MODULE == $extension_type) { + $connection->addModule($directory); + } + elseif (UPDATE_EXTENSION_TYPE_THEME == $extension_type) { + $connection->addTheme($directory); + } + } catch (ConnectionException $e) { + drupal_set_message(t($e->getMessage(), $e->getParams()), 'error'); + } + return FALSE; +} + +/** + * Removes an extension from the file system. + * + * @param $connection_name + * The machine-readable name of the connection type being used to access the + * server's filesystem; for example, 'ftp'. + * @param $directory + * The directory where the extension is. + * @param $settings + * An array of settings, which will vary among different connections, but may + * include data such as the username, password, and host. + * @return + * TRUE on success, or FALSE on failure. + */ +function update_remove_extension($connection_name, $directory, $settings) { + $valid_connections = update_list_connections(); + $active_connection = $valid_connections[$connection_name]; + if (!$active_connection) { + drupal_set_message(t("Could not use @connection connection, your server does not support it", array('@connection' => $connection_name))); + return FALSE; + } + require_once($active_connection['class_file_location']); + $connection_class = $active_connection['class']; + $connection = call_user_func("{$connection_class}::factory", DRUPAL_ROOT, $settings); + $extension_type = _update_get_extension_type($directory); + try { + if (UPDATE_EXTENSION_TYPE_MODULE == $extension_type) { + $connection->removeModule($directory); + } + elseif (UPDATE_EXTENSION_TYPE_THEME == $extension_type) { + $connection->removeTheme($directory); + } + } catch (ConnectionException $e) { + drupal_set_message(t($e->getMessage(), $e->getParams()), 'error'); + } + return FALSE; +} + + +/** + * Helper function to determine if it is a module or a theme + */ +function _update_get_extension_type($directory) { + if (count(file_scan_directory($directory, "/\.module$/")) > 0) { + return UPDATE_EXTENSION_TYPE_MODULE; + } + else { + return UPDATE_EXTENSION_TYPE_THEME; + } +} + +/** + * Get a list of all available connections that can upload files onto the system. + * + * @return + * Array containing the names of all available connections. + */ +function update_list_connections() { + $connections = module_invoke_all('update_connections'); + asort($connections); + return $connections; +} + +/** + * Sample (simplified) page callback to handle an update from update status. + * + */ +function update_download_install($module_name) { + $available = update_get_available(); + module_load_include('inc', 'update', 'update.compare'); + $data = update_calculate_project_data($available); + if ($project = $data[$module_name]) { + + } + else { + drupal_set_message(t('@module could not be upgraded.', array('@module' => $module_name))); + } + $tarball = update_get_file($project['releases'][$project['latest_version']]['download_link']); + $files = update_untar($tarball); + + //The first entry is the root dir + $new_module_location = file_directory_temp() . '/update-extraction/' . $files[0]; + + $settings = variable_get('update_connection_settings', array()); + $module_path = drupal_get_path('module', $module_name); + if (!is_dir($module_path)) { + drupal_set_message(t("@module_path does not exist", array('@module_path' => $module_path))); + return FALSE; + } + update_remove_extension(variable_get('update_preferred_connection', 'ftp'), DRUPAL_ROOT . '/' . $module_path, $settings['ftp']); + update_add_extension(variable_get('update_preferred_connection', 'ftp'), $new_module_location, $settings['ftp']); + drupal_goto('admin/reports/updates'); +} + Index: modules/update/includes/Connection/FTPConnection.php =================================================================== --- modules/update/includes/Connection/FTPConnection.php (revision 0) +++ modules/update/includes/Connection/FTPConnection.php (revision 0) @@ -0,0 +1,54 @@ +host = $host; + $this->username = $username; + $this->password = $password; + $this->port = $port; + parent::__construct($drupalRoot); + } + + static function factory($drupalRoot, $settings) { + if (function_exists('ftp_connect')) { + return new FTPExtension($drupalRoot, $settings['host'], $settings['username'], $settings['password'], $settings['port']); + } elseif(ini_get('allow_url_fopen')) { + return new FTPWrapper($drupalRoot, $settings['host'], $settings['username'], $settings['password'], $settings['port']); + } else { + throw new ConnectionException("Cannot start an FTP connection. Install the php ftp module or set allow_url_fopen to On in php.ini"); + } + } + + protected function lazyConnect() { + if ($this->connection == null) { + $this->connect(); + } + } + + function canCreateDirectories() { + return true; + } + + function canCopyDirs() { + return false; + } +} \ No newline at end of file Index: modules/update/includes/Connection/SSHConnection.php =================================================================== --- modules/update/includes/Connection/SSHConnection.php (revision 0) +++ modules/update/includes/Connection/SSHConnection.php (revision 0) @@ -0,0 +1,98 @@ +host = $host; + $this->username = $username; + $this->password = $password; + $this->port = $port; + parent::__construct($drupalRoot); + } + + static function factory($drupalRoot, $settings) { + return new SSHConnection($drupalRoot, $settings['host'], $settings['username'], $settings['password'], $settings['port']); + } + + function connect() { + $parts = explode(':', $this->host); + $port = (count($parts) == 2) ? $parts[1] : $this->port; + $this->connection = @ssh2_connect($this->host, $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."); + } + } + + protected function lazyConnect() { + if ($this->connection == null) { + $this->connect(); + } + } + + function canCreateDirectories() { + $this->lazyConnect(); + $handle = ssh2_exec($this->connection, 'mkdir --version'); + if (!fgets($handle)) { + pclose($handle); + return FALSE; + } + return TRUE; + pclose($handle); + } + + function copyFile($source, $destination) { + $this->lazyConnect(); + if (!ssh2_scp_send($this->connection, $source, $destination)) { + $exception = new ConnectionException("Cannot move @source_file to @destination_file", null, array("@source" => $source, "@destination" => $destination)); + throw $exception; + } + } + + function copyDirectory($source, $destination) { + $this->lazyConnect(); + if (!@ssh2_exec($this->connection, 'cp -Rp ' . escapeshellarg($source) . " " . escapeshellarg($destination))) { + $exception = new ConnectionException("Cannot move directory @directory", null, array("@directory" => $source)); + throw $exception; + } + } + + function mkdir($directory) { + $this->lazyConnect(); + if (!@ssh2_exec($this->connection, 'mkdir ' . escapeshellarg("$directory"))) { + $exception = new ConnectionException("Cannot create directory @directory", null, array("@directory" => $directory)); + throw $exception; + } + } + + function rmdir($directory) { + $this->lazyConnect(); + if (!@ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg("$directory"))) { + $exception = new ConnectionException("Cannot remove @directory", null, array("@directory" => $directory)); + throw $exception; + } + } + + function canCopyDirs() { + return true; + } +} \ No newline at end of file Index: modules/update/includes/Connection/TestConnection.php =================================================================== --- modules/update/includes/Connection/TestConnection.php (revision 0) +++ modules/update/includes/Connection/TestConnection.php (revision 0) @@ -0,0 +1,89 @@ +host = $host; + $this->username = $username; + $this->password = $password; + $this->port = $port; + parent::__construct($drupalRoot); + } + + static function factory($drupalRoot, $settings) { + return new TestConnection($drupalRoot, $settings['host'], $settings['username'], $settings['password'], $settings['port']); + } + + function connect() { + $parts = explode(':', $this->host); + $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 getConnection() { + return $this->connection; + } + + protected function lazyConnect() { + if ($this->connection == null) { + $this->connect(); + } + } + + function canCreateDirectories() { + return true; + } + + function copyFile($source, $destination) { + $this->lazyConnect(); + $this->connection->run("copyFile $source $destination"); + } + + function rmDir($directory) { + $this->lazyConnect(); + $this->connection->run("rmdir $directory"); + } + + function mkdir($directory) { + $this->lazyConnect(); + $this->connection->run("mkdir $directory"); + } + + function copyDirectory($source, $destination) { + $this->lazyConnect(); + $this->connection->run("copyDirectory $source $destination"); + } + + function canCopyDirs() { + return $this->canCopyDirs; + } +} + +class MockTestConnection { + + var $commandsRun = array(); + var $connectionString; + + function run($cmd) { + $this->commandsRun[] = $cmd; + } + + function flushCommands() { + $out = $this->commandsRun; + $this->commandsRun = array(); + return $out; + } +} Index: modules/update/includes/Connection/FTPConnection/FTPWrapper.php =================================================================== --- modules/update/includes/Connection/FTPConnection/FTPWrapper.php (revision 0) +++ modules/update/includes/Connection/FTPConnection/FTPWrapper.php (revision 0) @@ -0,0 +1,68 @@ +host); + $port = (count($parts) == 2) ? $parts[1] : $this->port; + $this->connection = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . "@$this->host:$this->port/"; + if (!is_dir($this->connection)) { + throw new ConnectionException("FTP Connection failed"); + } + } + + function copyFile($source, $destination) { + $this->lazyConnect(); + if (!@copy($this->connection . '/' . $source, $this->connection . '/' . $destination)) { + $exception = new ConnectionException("Cannot copy @source_file to @destination_file", null, array("@source" => $source, "@destination" => $destination)); + throw $exception; + } + } + + 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 rmdir($directory) { + $this->lazyConnect(); + $this->_rmDir($this->connection . DIRECTORY_SEPARATOR . $directory); + } + + function mkdir($directory) { + $this->lazyConnect(); + if (!@mkdir($directory)) { + $exception = new ConnectionException("Cannot create directory @directory", null, array("@directory" => $directory)); + throw $exception; + } + } +} \ No newline at end of file Index: modules/update/includes/Connection/FTPConnection/FTPExtension.php =================================================================== --- modules/update/includes/Connection/FTPConnection/FTPExtension.php (revision 0) +++ modules/update/includes/Connection/FTPConnection/FTPExtension.php (revision 0) @@ -0,0 +1,71 @@ +host); + $port = (count($parts) == 2) ? $parts[1] : $this->port; + $this->connection = ftp_connect($this->host, $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) { + $this->lazyConnect(); + if (!@ftp_put($this->connection, $destination, $source, FTP_BINARY)) { + $exception = new ConnectionException("Cannot move @source to @destination", null, array("@source" => $source, "@destination" => $destination)); + throw $exception; + } + } + + function rmdir($directory) { + $this->lazyConnect(); + $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)); + } + } + + + function mkdir($directory) { + $this->lazyConnect(); + if (!@ftp_mkdir($this->connection, $directory)) { + $exception = new ConnectionException("Cannot create directory @directory", null, array("@directory" => $directory)); + throw $exception; + } + } +} \ No newline at end of file Index: modules/update/includes/Connection.php =================================================================== --- modules/update/includes/Connection.php (revision 0) +++ modules/update/includes/Connection.php (revision 0) @@ -0,0 +1,98 @@ +drupalRoot = $drupalRoot; + } + + function addTheme($directory) { + $destination = $this->drupalRoot . DIRECTORY_SEPARATOR . $this->themeDir; + $this->addExtension($directory, $destination); + } + + function addModule($directory) { + $destination = $this->drupalRoot . DIRECTORY_SEPARATOR . $this->moduleDir; + $this->addExtension($directory, $destination); + } + + function addExtension($directory, $destination) { + //Remove a trailing slash if it exists + $directory = (substr($directory,-1) == DIRECTORY_SEPARATOR) ? substr($directory,0,-1) : $directory; + + if ($this->canCopyDirs()) { + $this->copyDirectory($directory, $destination . basename($directory)); + } else { + $this->mkdir($destination . basename($directory)); + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) { + $relative_path = basename($directory) . substr($filename, strlen($directory)); + if ($file->isDir()) { + $this->mkdir($destination . $relative_path); + } else { + $this->copyFile($file->getPathName(), $destination . $relative_path); + } + } + } + } + + function removeModule($directory) { + $this->rmdir($directory); + } + + function removeTheme($directory) { + $this->rmdir($directory); + } + + /** abstract **/ + function connect($user, $password, $host, $port) { + + } + + /** abstract **/ + function copyFile($source, $destination) { + + } + + /** abstract **/ + function copyDirectory($source, $destination){ + + } + /** abstract **/ + function canCreateDirectories() { + + } + + /** abstract **/ + function mkdir() { + + } + + /** abstract **/ + function canCopyDirs() { + + } + +} + +/* + * class ConnectionException +*/ +class ConnectionException extends Exception { + var $params = array(); + function __construct($message, $code, $params = array()) { + parent::__construct($message, $code); + $this->params = $params; + } + + public function getParams() { + return $this->params; + } +} \ No newline at end of file