From d16761d3bc464c51ce7d4b00c8e6d6c835ee1649 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 19:47:43 +0200 Subject: [PATCH 1/7] Extract parts of SSHConnectionAdapter into AbstractSSHConnectionAdapter --- .../AbstractSSHConnectionAdapter.php | 108 ++++++++++++++++++ .../Connection/SSHConnectionAdapter.php | 99 +--------------- 2 files changed, 109 insertions(+), 98 deletions(-) create mode 100644 src/Deployment/Connection/AbstractSSHConnectionAdapter.php diff --git a/src/Deployment/Connection/AbstractSSHConnectionAdapter.php b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php new file mode 100644 index 0000000..4c84cc7 --- /dev/null +++ b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php @@ -0,0 +1,108 @@ + + */ +abstract class AbstractSSHConnectionAdapter implements ConnectionAdapterInterface +{ + /** + * The hostname to connect to. + * + * @var string + */ + protected $hostname; + + /** + * The username used for authentication. + * + * @var string + */ + protected $authenticationUsername; + + /** + * Returns the username of the user executing the script. + * + * @return string + */ + protected function getCurrentUsername() + { + return $_SERVER['USER']; + } + + /** + * Returns the 'home' directory for the user. + * + * @return string|null + */ + protected function getUserDirectory() + { + $userDirectory = null; + if (isset($_SERVER['HOME'])) { + $userDirectory = $_SERVER['HOME']; + } elseif (isset($_SERVER['USERPROFILE'])) { + $userDirectory = $_SERVER['USERPROFILE']; + } + $userDirectory = realpath($userDirectory.'/../'); + $userDirectory .= '/'.$this->authenticationUsername; + + return $userDirectory; + } + + /** + * Returns the filtered output of the command. + * Removes the command echo and shell prompt from the output. + * + * @param string $output + * @param string $command + * + * @return string + */ + protected function getFilteredOutput($output, $command) + { + $output = str_replace(array("\r\n", "\r"), array("\n", ''), $output); + + $matches = array(); + if (preg_match($this->getOutputFilterRegex($command), $output, $matches) === 1) { + $output = ltrim($matches[1]); + } + + return $output; + } + + /** + * Returns the output filter regex to filter the output. + * + * @param string $command + * + * @return string + */ + protected function getOutputFilterRegex($command) + { + $commandCharacters = str_split(preg_quote($command, '/')); + $commandCharacterRegexWhitespaceFunction = function ($value) { + if ($value !== '\\') { + $value .= '\s?'; + } + + return $value; + }; + + $commandCharacters = array_map($commandCharacterRegexWhitespaceFunction, $commandCharacters); + + return sprintf('/%s(.*)%s/s', implode('', $commandCharacters), substr($this->getShellPromptRegex(), 1, -1)); + } + + /** + * Returns the regex matching the shell prompt. + * + * @return string + */ + protected function getShellPromptRegex() + { + return sprintf('/%s@.*[$|#]/', $this->authenticationUsername); + } +} diff --git a/src/Deployment/Connection/SSHConnectionAdapter.php b/src/Deployment/Connection/SSHConnectionAdapter.php index 5e3f419..d18f822 100644 --- a/src/Deployment/Connection/SSHConnectionAdapter.php +++ b/src/Deployment/Connection/SSHConnectionAdapter.php @@ -14,7 +14,7 @@ * * @author Niels Nijens */ -class SSHConnectionAdapter implements ConnectionAdapterInterface +class SSHConnectionAdapter extends AbstractSSHConnectionAdapter { /** * The password authentication type. @@ -31,20 +31,6 @@ class SSHConnectionAdapter implements ConnectionAdapterInterface */ const AUTHENTICATION_SSH_AGENT = 'agent'; - /** - * The hostname to connect to. - * - * @var string - */ - private $hostname; - - /** - * The username used for authentication. - * - * @var string - */ - private $authenticationUsername; - /** * The authentication credentials. * @@ -398,87 +384,4 @@ public function delete($remoteTarget, $recursive = false) return false; } - - /** - * Returns the username of user executing the script. - * - * @return string - */ - private function getCurrentUsername() - { - return $_SERVER['USER']; - } - - /** - * Returns the 'home' directory for the user. - * - * @return string|null - */ - private function getUserDirectory() - { - $userDirectory = null; - if (isset($_SERVER['HOME'])) { - $userDirectory = $_SERVER['HOME']; - } elseif (isset($_SERVER['USERPROFILE'])) { - $userDirectory = $_SERVER['USERPROFILE']; - } - $userDirectory = realpath($userDirectory.'/../'); - $userDirectory .= '/'.$this->authenticationUsername; - - return $userDirectory; - } - - /** - * Returns the filtered output of the command. - * Removes the command echo and shell prompt from the output. - * - * @param string $output - * @param string $command - * - * @return string - */ - private function getFilteredOutput($output, $command) - { - $output = str_replace(array("\r\n", "\r"), array("\n", ''), $output); - - $matches = array(); - if (preg_match($this->getOutputFilterRegex($command), $output, $matches) === 1) { - $output = ltrim($matches[1]); - } - - return $output; - } - - /** - * Returns the output filter regex to filter the output. - * - * @param string $command - * - * @return string - */ - private function getOutputFilterRegex($command) - { - $commandCharacters = str_split(preg_quote($command, '/')); - $commandCharacterRegexWhitespaceFunction = function ($value) { - if ($value !== '\\') { - $value .= '\s?'; - } - - return $value; - }; - - $commandCharacters = array_map($commandCharacterRegexWhitespaceFunction, $commandCharacters); - - return sprintf('/%s(.*)%s/s', implode('', $commandCharacters), substr($this->getShellPromptRegex(), 1, -1)); - } - - /** - * Returns the regex matching the shell prompt. - * - * @return string - */ - private function getShellPromptRegex() - { - return sprintf('/%s@.*[$|#]/', $this->authenticationUsername); - } } From d6ffaafaf0b345e584fd7dd3ba0b3a7fefbc2d58 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 19:52:16 +0200 Subject: [PATCH 2/7] Fix SSHConnectionAdapter::copy for directories --- .../AbstractSSHConnectionAdapter.php | 18 +++++++++++++++ .../Connection/SSHConnectionAdapter.php | 22 ------------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Deployment/Connection/AbstractSSHConnectionAdapter.php b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php index 4c84cc7..c57b356 100644 --- a/src/Deployment/Connection/AbstractSSHConnectionAdapter.php +++ b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php @@ -23,6 +23,24 @@ abstract class AbstractSSHConnectionAdapter implements ConnectionAdapterInterfac */ protected $authenticationUsername; + /** + * {@inheritdoc} + */ + public function copy($remoteSource, $remoteDestination) + { + if ($this->isConnected()) { + $arguments = array( + '--recursive', + $remoteSource, + $remoteDestination, + ); + + return $this->executeCommand('cp', $arguments)->isSuccessful(); + } + + return false; + } + /** * Returns the username of the user executing the script. * diff --git a/src/Deployment/Connection/SSHConnectionAdapter.php b/src/Deployment/Connection/SSHConnectionAdapter.php index d18f822..105a4d0 100644 --- a/src/Deployment/Connection/SSHConnectionAdapter.php +++ b/src/Deployment/Connection/SSHConnectionAdapter.php @@ -313,28 +313,6 @@ public function move($remoteSource, $remoteDestination) return false; } - /** - * {@inheritdoc} - */ - public function copy($remoteSource, $remoteDestination) - { - if ($this->isConnected()) { - $temporaryFile = tmpfile(); - - if ($this->getFile($remoteSource, $temporaryFile) === false) { - fclose($temporaryFile); - - return false; - } - - rewind($temporaryFile); - - return $this->putContents($remoteDestination, $temporaryFile); - } - - return false; - } - /** * {@inheritdoc} */ From 309b55adff820ba291faf04e424dbffd5ca18981 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 21:46:13 +0200 Subject: [PATCH 3/7] Add test for connection adapter recursive directory creation --- .../Connection/ConnectionAdapterTestCase.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Deployment/Connection/ConnectionAdapterTestCase.php b/tests/Deployment/Connection/ConnectionAdapterTestCase.php index eb4fd41..7d202ed 100644 --- a/tests/Deployment/Connection/ConnectionAdapterTestCase.php +++ b/tests/Deployment/Connection/ConnectionAdapterTestCase.php @@ -337,6 +337,19 @@ public function testCreateDirectory() $this->assertTrue(is_dir($this->workspaceUtility->getWorkspacePath().'/existing-directory')); } + /** + * Tests if ConnectionAdapterInterface::createDirectory creates a new directories recursively. + * + * @depends testCreateDirectory + */ + public function testCreateDirectoryRecursive() + { + $this->connectionAdapter->connect(); + + $this->assertTrue($this->connectionAdapter->createDirectory($this->workspaceUtility->getWorkspacePath().'/existing-directory/subdirectory', 0770, true)); + $this->assertTrue(is_dir($this->workspaceUtility->getWorkspacePath().'/existing-directory/subdirectory')); + } + /** * Tests if ConnectionAdapterInterface::createFile creates a new file. * From 8a0304020d285ccac51b104f223a0f94793588d0 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 21:34:19 +0200 Subject: [PATCH 4/7] Add InteractiveProcess --- src/Process/InteractiveProcess.php | 120 +++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/Process/InteractiveProcess.php diff --git a/src/Process/InteractiveProcess.php b/src/Process/InteractiveProcess.php new file mode 100644 index 0000000..2abc358 --- /dev/null +++ b/src/Process/InteractiveProcess.php @@ -0,0 +1,120 @@ + + */ +class InteractiveProcess extends Process +{ + /** + * The timestamp of the last time the process produced output. + * + * @var float + */ + protected $lastOutputTime; + + /** + * The instance containing the pipes used by the process. + * + * @var PipesInterface + */ + protected $processPipes; + + /** + * The InputStream instance. + * + * @var InputStream + */ + private $inputStream; + + /** + * The output buffer. + * + * @var string + */ + private $outputBuffer = ''; + + /** + * Constructs a new InteractiveProcess instance. + * + * @param string $command The command line to run + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int|float|null $timeout The timeout in seconds or null to disable + * @param array $options An array of options for proc_open + */ + public function __construct($command, $cwd = null, array $env = null, $timeout = 60, array $options = array()) + { + $this->inputStream = new InputStream(); + + parent::__construct($command, $cwd, $env, $this->inputStream, $timeout, $options); + + $this->setPty(true); + } + + /** + * Returns the output of the interactive process when there is a match for $expectRegex. + * + * @param string $expectRegex + * + * @return string + */ + public function read($expectRegex) + { + $this->lastOutputTime = microtime(true); + + $output = ''; + while (true) { + $this->checkTimeout(); + $this->readIntoOutputBuffer(); + + $matches = array(); + if (preg_match($expectRegex, $this->outputBuffer, $matches) === 1) { + $outputLength = strpos($this->outputBuffer, $matches[0]) + strlen($matches[0]); + + $output = substr($this->outputBuffer, 0, $outputLength); + $this->outputBuffer = substr($this->outputBuffer, $outputLength); + + break; + } + } + + return $output; + } + + /** + * Writes a command into the interactive process. + * + * @param string $command + */ + public function write($command) + { + $this->inputStream->write($command); + + if ($this->isStarted()) { + $this->readIntoOutputBuffer(); + } + } + + /** + * Reads the output from the pipes of the process into the output buffer. + */ + private function readIntoOutputBuffer() + { + $result = $this->processPipes->readAndWrite(true, false); + foreach ($result as $type => $data) { + if ($type !== 3) { + $this->lastOutputTime = microtime(true); + + $this->outputBuffer .= $data; + } + } + } +} From 35792aa436f0ee7dd18adab63c5d7e61d74761ff Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 21:35:52 +0200 Subject: [PATCH 5/7] Make option separator configurable in ProcessUtility::escapeArguments --- src/Utility/ProcessUtility.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Utility/ProcessUtility.php b/src/Utility/ProcessUtility.php index 1781f39..57b267d 100644 --- a/src/Utility/ProcessUtility.php +++ b/src/Utility/ProcessUtility.php @@ -17,7 +17,7 @@ class ProcessUtility * * @return string */ - public static function escapeArguments(array $arguments, $command = null) + public static function escapeArguments(array $arguments, $command = null, $optionSeparator = '=') { $processedArguments = array(); foreach ($arguments as $key => $value) { @@ -30,7 +30,7 @@ public static function escapeArguments(array $arguments, $command = null) if ($optionValue === null) { $processedArguments[] = $key; } elseif (is_string($optionValue)) { - $processedArguments[] = trim(sprintf('%s=%s', $key, $optionValue)); + $processedArguments[] = trim(sprintf('%s%s%s', $key, $optionSeparator, $optionValue)); } } } elseif (is_scalar($value)) { From 7e86fc63e53c2023fc6dcc8a34966bb41803531a Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 21:41:24 +0200 Subject: [PATCH 6/7] Make putContents and putFile consistent throughout all connection adapters --- .../AbstractSSHConnectionAdapter.php | 29 +++++++++++++++++++ .../Connection/LocalConnectionAdapter.php | 4 +++ .../Connection/SSHConnectionAdapter.php | 24 --------------- .../Connection/ConnectionAdapterTestCase.php | 20 +++++++++++-- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/Deployment/Connection/AbstractSSHConnectionAdapter.php b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php index c57b356..8ca3d7e 100644 --- a/src/Deployment/Connection/AbstractSSHConnectionAdapter.php +++ b/src/Deployment/Connection/AbstractSSHConnectionAdapter.php @@ -41,6 +41,35 @@ public function copy($remoteSource, $remoteDestination) return false; } + /** + * {@inheritdoc} + */ + public function putContents($destinationFilename, $data) + { + if ($this->isConnected()) { + $data = preg_replace('/\n$/', '', $data); + + return $this->executeCommand(sprintf("cat < \"%s\"\n%s\nEOF\n", $destinationFilename, $data))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function putFile($sourceFilename, $destinationFilename) + { + if ($this->isConnected()) { + $data = @file_get_contents($sourceFilename); + if ($data !== false) { + return $this->putContents($destinationFilename, $data); + } + } + + return false; + } + /** * Returns the username of the user executing the script. * diff --git a/src/Deployment/Connection/LocalConnectionAdapter.php b/src/Deployment/Connection/LocalConnectionAdapter.php index 1e77437..327eddc 100644 --- a/src/Deployment/Connection/LocalConnectionAdapter.php +++ b/src/Deployment/Connection/LocalConnectionAdapter.php @@ -192,6 +192,10 @@ public function changePermissions($remoteTarget, $fileMode, $recursive = false) */ public function putContents($remoteFilename, $data) { + if (substr($data, -1) !== "\n") { + $data .= "\n"; + } + $result = file_put_contents($remoteFilename, $data); return ($result !== false); diff --git a/src/Deployment/Connection/SSHConnectionAdapter.php b/src/Deployment/Connection/SSHConnectionAdapter.php index 105a4d0..693c250 100644 --- a/src/Deployment/Connection/SSHConnectionAdapter.php +++ b/src/Deployment/Connection/SSHConnectionAdapter.php @@ -327,30 +327,6 @@ public function changePermissions($remoteTarget, $fileMode, $recursive = false) return false; } - /** - * {@inheritdoc} - */ - public function putContents($destinationFilename, $data) - { - if ($this->isConnected()) { - return $this->connection->put($destinationFilename, $data, SFTP::SOURCE_STRING); - } - - return false; - } - - /** - * {@inheritdoc} - */ - public function putFile($sourceFilename, $destinationFilename) - { - if ($this->isConnected()) { - return $this->connection->put($destinationFilename, $sourceFilename, SFTP::SOURCE_LOCAL_FILE); - } - - return false; - } - /** * {@inheritdoc} */ diff --git a/tests/Deployment/Connection/ConnectionAdapterTestCase.php b/tests/Deployment/Connection/ConnectionAdapterTestCase.php index 7d202ed..6172989 100644 --- a/tests/Deployment/Connection/ConnectionAdapterTestCase.php +++ b/tests/Deployment/Connection/ConnectionAdapterTestCase.php @@ -454,7 +454,21 @@ public function testPutContents() $this->assertTrue($this->connectionAdapter->putContents($this->workspaceUtility->getWorkspacePath().'/test.txt', 'test')); $this->assertFileExists($this->workspaceUtility->getWorkspacePath().'/test.txt'); - $this->assertSame('test', file_get_contents($this->workspaceUtility->getWorkspacePath().'/test.txt')); + $this->assertSame("test\n", file_get_contents($this->workspaceUtility->getWorkspacePath().'/test.txt')); + } + + /** + * Tests if ConnectionAdapterInterface::putContents puts data in a file. + * + * @depends testPutContents + */ + public function testPutContentsWithNewline() + { + $this->connectionAdapter->connect(); + + $this->assertTrue($this->connectionAdapter->putContents($this->workspaceUtility->getWorkspacePath().'/test.txt', "test\n")); + $this->assertFileExists($this->workspaceUtility->getWorkspacePath().'/test.txt'); + $this->assertSame("test\n", file_get_contents($this->workspaceUtility->getWorkspacePath().'/test.txt')); } /** @@ -464,13 +478,13 @@ public function testPutContents() */ public function testPutFile() { - $this->workspaceUtility->createFile('/test.txt', 'test'); + $this->workspaceUtility->createFile('/test.txt', "test\n"); $this->connectionAdapter->connect(); $this->assertTrue($this->connectionAdapter->putFile($this->workspaceUtility->getWorkspacePath().'/test.txt', $this->workspaceUtility->getWorkspacePath().'/test2.txt')); $this->assertFileExists($this->workspaceUtility->getWorkspacePath().'/test2.txt'); - $this->assertSame('test', file_get_contents($this->workspaceUtility->getWorkspacePath().'/test2.txt')); + $this->assertSame("test\n", file_get_contents($this->workspaceUtility->getWorkspacePath().'/test2.txt')); } /** From 8439d2b6abd2d1fb643abee6fafb1b52c10a1e25 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 1 Aug 2016 21:45:39 +0200 Subject: [PATCH 7/7] Add NativeSSHConnectionAdapter --- .../Connection/NativeSSHConnectionAdapter.php | 360 ++++++++++++++++++ .../NativeSSHConnectionAdapterTest.php | 77 ++++ 2 files changed, 437 insertions(+) create mode 100644 src/Deployment/Connection/NativeSSHConnectionAdapter.php create mode 100644 tests/Deployment/Connection/NativeSSHConnectionAdapterTest.php diff --git a/src/Deployment/Connection/NativeSSHConnectionAdapter.php b/src/Deployment/Connection/NativeSSHConnectionAdapter.php new file mode 100644 index 0000000..b1814e4 --- /dev/null +++ b/src/Deployment/Connection/NativeSSHConnectionAdapter.php @@ -0,0 +1,360 @@ + + */ +class NativeSSHConnectionAdapter extends AbstractSSHConnectionAdapter +{ + /** + * The location of the SSH configuration file. + * + * @var string + */ + private $configurationFile; + + /** + * The InteractiveProcess instance containing the SSH process. + * + * @var InteractiveProcess + */ + private $process; + + /** + * Constructs a new NativeSSHConnectionAdapter instance. + * + * @param string $hostname + * @param string|null $authenticationUsername + * @param string $configurationFile + */ + public function __construct($hostname, $authenticationUsername = null, $configurationFile = '~/.ssh/config') + { + $this->hostname = $hostname; + $this->authenticationUsername = $authenticationUsername; + if (isset($this->authenticationUsername) === false) { + $this->authenticationUsername = $this->getCurrentUsername(); + } + $this->configurationFile = $configurationFile; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if ($this->isConnected()) { + return true; + } + + $arguments = array( + $this->hostname, + ); + + if (isset($this->authenticationUsername)) { + $arguments = array_merge(array('-l' => $this->authenticationUsername), $arguments); + } + + $configurationFile = preg_replace('/^~/', $this->getUserDirectory(), $this->configurationFile); + if (is_file($configurationFile)) { + $arguments = array_merge(array('-F' => $configurationFile), $arguments); + } + + $this->process = new InteractiveProcess(ProcessUtility::escapeArguments($arguments, 'ssh', '')); + $this->process->setTimeout(null); + $this->process->setIdleTimeout(5); + $this->process->start(); + + $this->process->read($this->getShellPromptRegex()); + + return $this->process->isRunning(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->isConnected()) { + $this->process->write("logout\n"); + $this->process->stop(); + + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + if ($this->process instanceof InteractiveProcess) { + return $this->process->isRunning(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function isFile($remoteFilename) + { + if ($this->isConnected()) { + return $this->executeCommand(sprintf('[ ! -L "%s" ] && [ -f "%s" ]', $remoteFilename, $remoteFilename))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function isLink($remoteTarget) + { + if ($this->isConnected()) { + return $this->executeCommand(sprintf('[ -L "%s" ]', $remoteTarget))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function isDirectory($remoteDirectory) + { + if ($this->isConnected()) { + return $this->executeCommand(sprintf('[ ! -L "%s" ] && [ -d "%s" ]', $remoteDirectory, $remoteDirectory))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function readLink($remoteTarget) + { + if ($this->isConnected()) { + $result = $this->executeCommand('readlink', array($remoteTarget)); + if ($result->isSuccessful()) { + return trim($result->getOutput()); + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function changeWorkingDirectory($remoteDirectory) + { + if ($this->isConnected()) { + return $this->executeCommand('cd', array($remoteDirectory))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function executeCommand($command, array $arguments = array()) + { + if ($this->isConnected()) { + if (empty($arguments) === false) { + $command = ProcessUtility::escapeArguments($arguments, $command); + } + + $this->process->write($command."\n"); + $output = $this->getFilteredOutput($this->process->read($this->getShellPromptRegex()), $command); + + $this->process->write("echo $?\n"); + + $exitCode = intval($this->getFilteredOutput($this->process->read($this->getShellPromptRegex()), 'echo $?')); + $errorOutput = ''; + + return new ProcessExecutionResult($exitCode, $output, $errorOutput); + } + + return new ProcessExecutionResult(126, '', "Connection adapter not connected.\n"); + } + + /** + * {@inheritdoc} + */ + public function getWorkingDirectory() + { + if ($this->isConnected()) { + $result = $this->executeCommand('pwd'); + if ($result->isSuccessful()) { + return trim($result->getOutput()); + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getDirectoryContentsList($remoteDirectory) + { + if ($this->isConnected()) { + $result = $this->executeCommand(sprintf('ls -1A --color=never "%s"', $remoteDirectory)); + if ($result->isSuccessful()) { + return $result->getOutputAsArray(); + } + } + + return array(); + } + + /** + * {@inheritdoc} + */ + public function getContents($remoteFilename) + { + if ($this->isConnected()) { + $result = $this->executeCommand('cat', array($remoteFilename)); + if ($result->isSuccessful()) { + return $result->getOutput(); + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getFile($remoteFilename, $localFilename) + { + if ($this->isConnected()) { + $remoteContents = $this->getContents($remoteFilename); + if ($remoteContents !== false) { + return @file_put_contents($localFilename, $remoteContents) !== false; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function createDirectory($remoteDirectory, $fileMode = 0770, $recursive = false) + { + if ($this->isConnected()) { + $arguments = array( + sprintf('--mode=%o', $fileMode), + $remoteDirectory, + ); + + if ($recursive === true) { + array_unshift($arguments, '--parents'); + } + + return $this->executeCommand('mkdir', $arguments)->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function createFile($remoteFilename, $fileMode = 0770) + { + if ($this->isConnected()) { + return ($this->executeCommand('touch', array($remoteFilename))->isSuccessful() && $this->changePermissions($remoteFilename, $fileMode)); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function link($remoteSource, $remoteTarget) + { + if ($this->isConnected()) { + return $this->executeCommand(sprintf('ln -s "%s" "%s"', $remoteSource, $remoteTarget))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function move($remoteSource, $remoteDestination) + { + if ($this->isConnected()) { + return $this->executeCommand('mv', array($remoteSource, $remoteDestination))->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function changePermissions($remoteTarget, $fileMode, $recursive = false) + { + if ($this->isConnected()) { + $arguments = array( + sprintf('%o', $fileMode), + $remoteTarget, + ); + + if ($recursive === true) { + array_unshift($arguments, '--recursive'); + } + + return $this->executeCommand('chmod', $arguments)->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function delete($remoteTarget, $recursive = false) + { + if ($this->isConnected()) { + $arguments = array( + $remoteTarget, + ); + + if ($recursive === true) { + array_unshift($arguments, '--recursive'); + } + + return $this->executeCommand('rm', $arguments)->isSuccessful(); + } + + return false; + } + + /** + * {@inheritdoc} + */ + protected function getOutputFilterRegex($command) + { + $regex = parent::getOutputFilterRegex($command); + + return str_replace('(.*)', '(.*)\033]0;', $regex); + } +} diff --git a/tests/Deployment/Connection/NativeSSHConnectionAdapterTest.php b/tests/Deployment/Connection/NativeSSHConnectionAdapterTest.php new file mode 100644 index 0000000..5e54adc --- /dev/null +++ b/tests/Deployment/Connection/NativeSSHConnectionAdapterTest.php @@ -0,0 +1,77 @@ + + */ +class NativeSSHConnectionAdapterTest extends ConnectedConnectionAdapterTestCase +{ + /** + * Unsets the USERPROFILE environment variable and sets the HOME environment variable. + */ + public function tearDown() + { + parent::tearDown(); + + if (isset($_SERVER['USERPROFILE'])) { + $_SERVER['HOME'] = $_SERVER['USERPROFILE']; + unset($_SERVER['USERPROFILE']); + } + } + + /** + * Tests if NativeSSHConnectionAdapter::connect returns true, but does not recreate the connection. + */ + public function testCallingConnectTwiceDoesNotRecreateConnection() + { + $this->connectionAdapter->connect(); + + $connection = $this->getObjectAttribute($this->connectionAdapter, 'process'); + + $this->assertTrue($this->connectionAdapter->connect()); + $this->assertAttributeSame($connection, 'process', $this->connectionAdapter); + } + + /** + * Tests if NativeSSHConnectionAdapter::connect returns true when connecting with an existing SSH configuration file. + */ + public function testConnectWithSSHConfigurationReturnsTrue() + { + $username = $this->getSSHUsername(); + if (isset($username) === false) { + $username = $_SERVER['USER']; + } + + touch('/home/'.$username.'/.ssh/config'); + + $this->testConnectReturnsTrue(); + } + + /** + * {@inheritdoc} + */ + protected function createConnectionAdapter() + { + return new NativeSSHConnectionAdapter('localhost', $this->getSSHUsername()); + } + + /** + * Returns the SSH username configured in the PHPUnit configuration. + * + * @return string|null + */ + private function getSSHUsername() + { + $username = getenv('ssh.username'); + if (empty($username)) { + $username = null; + } + + return $username; + } +}