| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899 |
- <?php
- namespace Codeception\Module;
- use Codeception\TestInterface;
- use Codeception\Exception\ModuleException;
- /**
- *
- * Works with SFTP/FTP servers.
- *
- * In order to test the contents of a specific file stored on any remote FTP/SFTP system
- * this module downloads a temporary file to the local system. The temporary directory is
- * defined by default as ```tests/_data``` to specify a different directory set the tmp config
- * option to your chosen path.
- *
- * Don't forget to create the folder and ensure its writable.
- *
- * Supported and tested FTP types are:
- *
- * * FTP
- * * SFTP
- *
- * Connection uses php build in FTP client for FTP,
- * connection to SFTP uses [phpseclib](http://phpseclib.sourceforge.net/) pulled in using composer.
- *
- * For SFTP, add [phpseclib](http://phpseclib.sourceforge.net/) to require list.
- * ```
- * "require": {
- * "phpseclib/phpseclib": "^2.0.14"
- * }
- * ```
- *
- * ## Status
- *
- * * Stability:
- * - FTP: **stable**
- * - SFTP: **stable**
- *
- * ## Config
- *
- * * type: ftp - type of connection ftp/sftp (defaults to ftp).
- * * host *required* - hostname/ip address of the ftp server.
- * * port: 21 - port number for the ftp server
- * * timeout: 90 - timeout settings for connecting the ftp server.
- * * user: anonymous - user to access ftp server, defaults to anonymous authentication.
- * * password - password, defaults to empty for anonymous.
- * * key - path to RSA key for sftp.
- * * tmp - path to local directory for storing tmp files.
- * * passive: true - Turns on or off passive mode (FTP only)
- * * cleanup: true - remove tmp files from local directory on completion.
- *
- * ### Example
- * #### Example (FTP)
- *
- * modules:
- * enabled: [FTP]
- * config:
- * FTP:
- * type: ftp
- * host: '127.0.0.1'
- * port: 21
- * timeout: 120
- * user: 'root'
- * password: 'root'
- * key: ~/.ssh/id_rsa
- * tmp: 'tests/_data/ftp'
- * passive: true
- * cleanup: false
- *
- * #### Example (SFTP)
- *
- * modules:
- * enabled: [FTP]
- * config:
- * FTP:
- * type: sftp
- * host: '127.0.0.1'
- * port: 22
- * timeout: 120
- * user: 'root'
- * password: 'root'
- * key: ''
- * tmp: 'tests/_data/ftp'
- * cleanup: false
- *
- *
- * This module extends the Filesystem module, file contents methods are inherited from this module.
- */
- class FTP extends Filesystem
- {
- /**
- * FTP/SFTP connection handler
- */
- protected $ftp = null;
- /**
- * Configuration options and default settings
- *
- * @var array
- */
- protected $config = [
- 'type' => 'ftp',
- 'port' => 21,
- 'timeout' => 90,
- 'user' => 'anonymous',
- 'password' => '',
- 'key' => '',
- 'tmp' => 'tests/_data',
- 'passive' => false,
- 'cleanup' => true
- ];
- /**
- * Required configuration fields
- *
- * @var array
- */
- protected $requiredFields = ['host'];
- // ----------- SETUP METHODS BELOW HERE -------------------------//
- /**
- * Setup connection and login with config settings
- *
- * @param \Codeception\TestInterface $test
- */
- public function _before(TestInterface $test)
- {
- // Login using config settings
- $this->loginAs($this->config['user'], $this->config['password']);
- }
- /**
- * Close the FTP connection & Clear up
- */
- public function _after(TestInterface $test)
- {
- $this->_closeConnection();
- // Clean up temp files
- if ($this->config['cleanup']) {
- if (file_exists($this->config['tmp'] . '/ftp_data_file.tmp')) {
- unlink($this->config['tmp'] . '/ftp_data_file.tmp');
- }
- }
- }
- /**
- * Change the logged in user mid-way through your test, this closes the
- * current connection to the server and initialises and new connection.
- *
- * On initiation of this modules you are automatically logged into
- * the server using the specified config options or defaulted
- * to anonymous user if not provided.
- *
- * ``` php
- * <?php
- * $I->loginAs('user','password');
- * ?>
- * ```
- *
- * @param String $user
- * @param String $password
- */
- public function loginAs($user = 'anonymous', $password = '')
- {
- $this->_openConnection($user, $password); // Create new connection and login.
- }
- /**
- * Enters a directory on the ftp system - FTP root directory is used by default
- *
- * @param $path
- */
- public function amInPath($path)
- {
- $this->_changeDirectory($this->path = $this->absolutizePath($path) . ($path == '/' ? '' : DIRECTORY_SEPARATOR));
- $this->debug('Moved to ' . $this->path);
- }
- /**
- * Resolve path
- *
- * @param $path
- * @return string
- */
- protected function absolutizePath($path)
- {
- if (strpos($path, '/') === 0) {
- return $path;
- }
- return $this->path . $path;
- }
- // ----------- SEARCH METHODS BELOW HERE ------------------------//
- /**
- * Checks if file exists in path on the remote FTP/SFTP system.
- * DOES NOT OPEN the file when it's exists
- *
- * ``` php
- * <?php
- * $I->seeFileFound('UserModel.php','app/models');
- * ?>
- * ```
- *
- * @param $filename
- * @param string $path
- */
- public function seeFileFound($filename, $path = '')
- {
- $files = $this->grabFileList($path);
- $this->debug("see file: {$filename}");
- $this->assertContains($filename, $files, "file {$filename} not found in {$path}");
- }
- /**
- * Checks if file exists in path on the remote FTP/SFTP system, using regular expression as filename.
- * DOES NOT OPEN the file when it's exists
- *
- * ``` php
- * <?php
- * $I->seeFileFoundMatches('/^UserModel_([0-9]{6}).php$/','app/models');
- * ?>
- * ```
- *
- * @param $regex
- * @param string $path
- */
- public function seeFileFoundMatches($regex, $path = '')
- {
- foreach ($this->grabFileList($path) as $filename) {
- preg_match($regex, $filename, $matches);
- if (!empty($matches)) {
- $this->debug("file '{$filename}' matches '{$regex}'");
- return;
- }
- }
- $this->fail("no file matches found for '{$regex}'");
- }
- /**
- * Checks if file does not exist in path on the remote FTP/SFTP system
- *
- * @param $filename
- * @param string $path
- */
- public function dontSeeFileFound($filename, $path = '')
- {
- $files = $this->grabFileList($path);
- $this->debug("don't see file: {$filename}");
- $this->assertNotContains($filename, $files);
- }
- /**
- * Checks if file does not exist in path on the remote FTP/SFTP system, using regular expression as filename.
- * DOES NOT OPEN the file when it's exists
- *
- * @param $regex
- * @param string $path
- */
- public function dontSeeFileFoundMatches($regex, $path = '')
- {
- foreach ($this->grabFileList($path) as $filename) {
- preg_match($regex, $filename, $matches);
- if (!empty($matches)) {
- $this->fail("file matches found for {$regex}");
- }
- }
- $this->assertTrue(true);
- $this->debug("no files match '{$regex}'");
- }
- // ----------- UTILITY METHODS BELOW HERE -------------------------//
- /**
- * Opens a file (downloads from the remote FTP/SFTP system to a tmp directory for processing)
- * and stores it's content.
- *
- * Usage:
- *
- * ``` php
- * <?php
- * $I->openFile('composer.json');
- * $I->seeInThisFile('codeception/codeception');
- * ?>
- * ```
- *
- * @param $filename
- */
- public function openFile($filename)
- {
- $this->_openFile($this->absolutizePath($filename));
- }
- /**
- * Saves contents to tmp file and uploads the FTP/SFTP system.
- * Overwrites current file on server if exists.
- *
- * ``` php
- * <?php
- * $I->writeToFile('composer.json', 'some data here');
- * ?>
- * ```
- *
- * @param $filename
- * @param $contents
- */
- public function writeToFile($filename, $contents)
- {
- $this->_writeToFile($this->absolutizePath($filename), $contents);
- }
- /**
- * Create a directory on the server
- *
- * ``` php
- * <?php
- * $I->makeDir('vendor');
- * ?>
- * ```
- *
- * @param $dirname
- */
- public function makeDir($dirname)
- {
- $this->makeDirectory($this->absolutizePath($dirname));
- }
- /**
- * Currently not supported in this module, overwrite inherited method
- *
- * @param $src
- * @param $dst
- */
- public function copyDir($src, $dst)
- {
- $this->fail('copyDir() currently unsupported by FTP module');
- }
- /**
- * Rename/Move file on the FTP/SFTP server
- *
- * ``` php
- * <?php
- * $I->renameFile('composer.lock', 'composer_old.lock');
- * ?>
- * ```
- *
- * @param $filename
- * @param $rename
- */
- public function renameFile($filename, $rename)
- {
- $this->renameDirectory($this->absolutizePath($filename), $this->absolutizePath($rename));
- }
- /**
- * Rename/Move directory on the FTP/SFTP server
- *
- * ``` php
- * <?php
- * $I->renameDir('vendor', 'vendor_old');
- * ?>
- * ```
- *
- * @param $dirname
- * @param $rename
- */
- public function renameDir($dirname, $rename)
- {
- $this->renameDirectory($this->absolutizePath($dirname), $this->absolutizePath($rename));
- }
- /**
- * Deletes a file on the remote FTP/SFTP system
- *
- * ``` php
- * <?php
- * $I->deleteFile('composer.lock');
- * ?>
- * ```
- *
- * @param $filename
- */
- public function deleteFile($filename)
- {
- $this->delete($this->absolutizePath($filename));
- }
- /**
- * Deletes directory with all subdirectories on the remote FTP/SFTP server
- *
- * ``` php
- * <?php
- * $I->deleteDir('vendor');
- * ?>
- * ```
- *
- * @param $dirname
- */
- public function deleteDir($dirname)
- {
- $this->delete($this->absolutizePath($dirname));
- }
- /**
- * Erases directory contents on the FTP/SFTP server
- *
- * ``` php
- * <?php
- * $I->cleanDir('logs');
- * ?>
- * ```
- *
- * @param $dirname
- */
- public function cleanDir($dirname)
- {
- $this->clearDirectory($this->absolutizePath($dirname));
- }
- // ----------- GRABBER METHODS BELOW HERE -----------------------//
- /**
- * Grabber method for returning file/folders listing in an array
- *
- * ```php
- * <?php
- * $files = $I->grabFileList();
- * $count = $I->grabFileList('TEST', false); // Include . .. .thumbs.db
- * ?>
- * ```
- *
- * @param string $path
- * @param bool $ignore - suppress '.', '..' and '.thumbs.db'
- * @return array
- */
- public function grabFileList($path = '', $ignore = true)
- {
- $absolutize_path = $this->absolutizePath($path)
- . ($path != '' && substr($path, -1) != '/' ? DIRECTORY_SEPARATOR : '');
- $files = $this->_listFiles($absolutize_path);
- $display_files = [];
- if (is_array($files) && !empty($files)) {
- $this->debug('File List:');
- foreach ($files as &$file) {
- if (strtolower($file) != '.' &&
- strtolower($file) != '..' &&
- strtolower($file) != 'thumbs.db'
- ) { // Ignore '.', '..' and 'thumbs.db'
- // Replace full path from file listings if returned in listing
- $file = str_replace(
- $absolutize_path,
- '',
- $file
- );
- $display_files[] = $file;
- $this->debug(' - ' . $file);
- }
- }
- return $ignore ? $display_files : $files;
- }
- $this->debug("File List: <empty>");
- return [];
- }
- /**
- * Grabber method for returning file/folders count in directory
- *
- * ```php
- * <?php
- * $count = $I->grabFileCount();
- * $count = $I->grabFileCount('TEST', false); // Include . .. .thumbs.db
- * ?>
- * ```
- *
- * @param string $path
- * @param bool $ignore - suppress '.', '..' and '.thumbs.db'
- * @return int
- */
- public function grabFileCount($path = '', $ignore = true)
- {
- $count = count($this->grabFileList($path, $ignore));
- $this->debug("File Count: {$count}");
- return $count;
- }
- /**
- * Grabber method to return file size
- *
- * ```php
- * <?php
- * $size = $I->grabFileSize('test.txt');
- * ?>
- * ```
- *
- * @param $filename
- * @return bool
- */
- public function grabFileSize($filename)
- {
- $fileSize = $this->size($filename);
- $this->debug("{$filename} has a file size of {$fileSize}");
- return $fileSize;
- }
- /**
- * Grabber method to return last modified timestamp
- *
- * ```php
- * <?php
- * $time = $I->grabFileModified('test.txt');
- * ?>
- * ```
- *
- * @param $filename
- * @return bool
- */
- public function grabFileModified($filename)
- {
- $time = $this->modified($filename);
- $this->debug("{$filename} was last modified at {$time}");
- return $time;
- }
- /**
- * Grabber method to return current working directory
- *
- * ```php
- * <?php
- * $pwd = $I->grabDirectory();
- * ?>
- * ```
- *
- * @return string
- */
- public function grabDirectory()
- {
- $pwd = $this->_directory();
- $this->debug("PWD: {$pwd}");
- return $pwd;
- }
- // ----------- SERVER CONNECTION METHODS BELOW HERE -------------//
- /**
- * Open a new FTP/SFTP connection and authenticate user.
- *
- * @param string $user
- * @param string $password
- */
- private function _openConnection($user = 'anonymous', $password = '')
- {
- $this->_closeConnection(); // Close connection if already open
- if ($this->isSFTP()) {
- $this->sftpConnect($user, $password);
- } else {
- $this->ftpConnect($user, $password);
- }
- $pwd = $this->grabDirectory();
- $this->path = $pwd . ($pwd == '/' ? '' : DIRECTORY_SEPARATOR);
- }
- /**
- * Close open FTP/SFTP connection
- */
- private function _closeConnection()
- {
- if (!$this->ftp) {
- return;
- }
- if (!$this->isSFTP()) {
- ftp_close($this->ftp);
- $this->ftp = null;
- }
- }
- /**
- * Get the file listing for FTP/SFTP connection
- *
- * @param String $path
- * @return array
- */
- private function _listFiles($path)
- {
- if ($this->isSFTP()) {
- $files = @$this->ftp->nlist($path);
- } else {
- $files = @ftp_nlist($this->ftp, $path);
- }
- if ($files === false) {
- $this->fail("couldn't list files");
- }
- return $files;
- }
- /**
- * Get the current directory for the FTP/SFTP connection
- *
- * @return string
- */
- private function _directory()
- {
- if ($this->isSFTP()) {
- // == DIRECTORY_SEPARATOR ? '' : $pwd;
- $pwd = @$this->ftp->pwd();
- } else {
- $pwd = @ftp_pwd($this->ftp);
- }
- if (!$pwd) {
- $this->fail("couldn't get current directory");
- }
- }
- /**
- * Change the working directory on the FTP/SFTP server
- *
- * @param $path
- */
- private function _changeDirectory($path)
- {
- if ($this->isSFTP()) {
- $changed = @$this->ftp->chdir($path);
- } else {
- $changed = @ftp_chdir($this->ftp, $path);
- }
- if (!$changed) {
- $this->fail("couldn't change directory {$path}");
- }
- }
- /**
- * Download remote file to local tmp directory and open contents.
- *
- * @param $filename
- */
- private function _openFile($filename)
- {
- // Check local tmp directory
- if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) {
- $this->fail('tmp directory not found or is not writable');
- }
- // Download file to local tmp directory
- $tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp";
- if ($this->isSFTP()) {
- $downloaded = @$this->ftp->get($filename, $tmp_file);
- } else {
- $downloaded = @ftp_get($this->ftp, $tmp_file, $filename, FTP_BINARY);
- }
- if (!$downloaded) {
- $this->fail('failed to download file to tmp directory');
- }
- // Open file content to variable
- if ($this->file = file_get_contents($tmp_file)) {
- $this->filepath = $filename;
- } else {
- $this->fail('failed to open tmp file');
- }
- }
- /**
- * Write data to local tmp file and upload to server
- *
- * @param $filename
- * @param $contents
- */
- private function _writeToFile($filename, $contents)
- {
- // Check local tmp directory
- if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) {
- $this->fail('tmp directory not found or is not writable');
- }
- // Build temp file
- $tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp";
- file_put_contents($tmp_file, $contents);
- // Update variables
- $this->filepath = $filename;
- $this->file = $contents;
- // Upload the file to server
- if ($this->isSFTP()) {
- $flag = defined('NET_SFTP_LOCAL_FILE') ? NET_SFTP_LOCAL_FILE : \phpseclib\Net\SFTP::SOURCE_LOCAL_FILE;
- $uploaded = @$this->ftp->put($filename, $tmp_file, $flag);
- } else {
- $uploaded = ftp_put($this->ftp, $filename, $tmp_file, FTP_BINARY);
- }
- if (!$uploaded) {
- $this->fail('failed to upload file to server');
- }
- }
- /**
- * Make new directory on server
- *
- * @param $path
- */
- private function makeDirectory($path)
- {
- if ($this->isSFTP()) {
- $created = @$this->ftp->mkdir($path, true);
- } else {
- $created = @ftp_mkdir($this->ftp, $path);
- }
- if (!$created) {
- $this->fail("couldn't make directory {$path}");
- }
- $this->debug("Make directory: {$path}");
- }
- /**
- * Rename/Move directory/file on server
- *
- * @param $path
- * @param $rename
- */
- private function renameDirectory($path, $rename)
- {
- if ($this->isSFTP()) {
- $renamed = @$this->ftp->rename($path, $rename);
- } else {
- $renamed = @ftp_rename($this->ftp, $path, $rename);
- }
- if (!$renamed) {
- $this->fail("couldn't rename directory {$path} to {$rename}");
- }
- $this->debug("Renamed directory: {$path} to {$rename}");
- }
- /**
- * Delete file on server
- *
- * @param $filename
- */
- private function delete($filename, $isDir = false)
- {
- if ($this->isSFTP()) {
- $deleted = @$this->ftp->delete($filename, $isDir);
- } else {
- $deleted = @$this->ftpDelete($filename);
- }
- if (!$deleted) {
- $this->fail("couldn't delete {$filename}");
- }
- $this->debug("Deleted: {$filename}");
- }
- /**
- * Function to recursively delete folder, used for PHP FTP build in client.
- *
- * @param $directory
- * @return bool
- */
- private function ftpDelete($directory)
- {
- // here we attempt to delete the file/directory
- if (!(@ftp_rmdir($this->ftp, $directory) || @ftp_delete($this->ftp, $directory))) {
- // if the attempt to delete fails, get the file listing
- $filelist = @ftp_nlist($this->ftp, $directory);
- // loop through the file list and recursively delete the FILE in the list
- foreach ($filelist as $file) {
- $this->ftpDelete($file);
- }
- // if the file list is empty, delete the DIRECTORY we passed
- $this->ftpDelete($directory);
- }
- return true;
- }
- /**
- * Clear directory on server of all content
- *
- * @param $path
- */
- private function clearDirectory($path)
- {
- $this->debug("Clear directory: {$path}");
- $this->delete($path);
- $this->makeDirectory($path);
- }
- /**
- * Return the size of a given file
- *
- * @param $filename
- * @return bool
- */
- private function size($filename)
- {
- if ($this->isSFTP()) {
- $size = (int)@$this->ftp->size($filename);
- } else {
- $size = @ftp_size($this->ftp, $filename);
- }
- if ($size > 0) {
- return $size;
- }
- $this->fail("couldn't get the file size for {$filename}");
- }
- /**
- * Return the last modified time of a given file
- *
- * @param $filename
- * @return bool
- */
- private function modified($filename)
- {
- if ($this->isSFTP()) {
- $info = @$this->ftp->lstat($filename);
- if ($info) {
- return $info['mtime'];
- }
- } else {
- if ($time = @ftp_mdtm($this->ftp, $filename)) {
- return $time;
- }
- }
- $this->fail("couldn't get the file size for {$filename}");
- }
- /**
- * @param $user
- * @param $password
- */
- protected function sftpConnect($user, $password)
- {
- if (class_exists('Net_SFTP')) {
- $this->ftp = new \Net_SFTP($this->config['host'], $this->config['port'], $this->config['timeout']);
- } elseif (class_exists('phpseclib\Net\SFTP')) {
- $this->ftp = new \phpseclib\Net\SFTP($this->config['host'], $this->config['port'], $this->config['timeout']);
- } else {
- throw new ModuleException('phpseclib/phpseclib library is not installed');
- }
- if ($this->ftp === false) {
- $this->ftp = null;
- $this->fail('failed to connect to ftp server');
- }
- if (isset($this->config['key'])) {
- $keyFile = file_get_contents($this->config['key']);
- if (class_exists('Crypt_RSA')) {
- $password = new \Crypt_RSA();
- } elseif (class_exists('phpseclib\Crypt\RSA')) {
- $password = new \phpseclib\Crypt\RSA();
- } else {
- throw new ModuleException('phpseclib/phpseclib library is not installed');
- }
- $password->loadKey($keyFile);
- }
- if (!$this->ftp->login($user, $password)) {
- $this->fail('failed to authenticate user');
- }
- }
- /**
- * @param $user
- * @param $password
- */
- protected function ftpConnect($user, $password)
- {
- $this->ftp = ftp_connect($this->config['host'], $this->config['port'], $this->config['timeout']);
- if ($this->ftp === false) {
- $this->ftp = null;
- $this->fail('failed to connect to ftp server');
- }
- // Login using given access details
- if (!@ftp_login($this->ftp, $user, $password)) {
- $this->fail('failed to authenticate user');
- }
- // Set passive mode option (ftp only option)
- if (isset($this->config['passive'])) {
- ftp_pasv($this->ftp, $this->config['passive']);
- }
- }
- protected function isSFTP()
- {
- return strtolower($this->config['type']) == 'sftp';
- }
- }
|