66
77use Codeception \TestInterface ;
88use Codeception \Exception \ModuleException ;
9+ use phpseclib \Net \SFTP AS SFTPv2 ;
10+ use phpseclib3 \Net \SFTP AS SFTPv3 ;
11+ use phpseclib4 \Net \SFTP AS SFTPv4 ;
912
1013/**
1114 *
2427 * * SFTP
2528 *
2629 * Connection uses php build in FTP client for FTP,
27- * connection to SFTP uses [phpseclib](https://phpseclib.sourceforge.net/ ) pulled in using composer.
30+ * connection to SFTP uses [phpseclib](https://github.com/phpseclib/phpseclib ) pulled in using composer.
2831 *
29- * For SFTP, add [phpseclib](https://phpseclib.sourceforge.net/ ) to require list.
32+ * For SFTP, add [phpseclib](https://github.com/phpseclib/phpseclib ) to require list.
3033 * ```
31- * "require": {
32- * "phpseclib/phpseclib": "^2.0.14"
33- * }
34+ * composer require phpseclib/phpseclib
3435 * ```
3536 *
3637 * ## Status
@@ -93,7 +94,7 @@ class FTP extends Filesystem
9394{
9495 /**
9596 * FTP/SFTP connection handler
96- * @var null|bool|\Net_SFTP|\phpseclib\Net\SFTP |resource
97+ * @var null|bool|SFTPv2|SFTPv3|SFTPv4 |resource
9798 */
9899 protected $ ftp = null ;
99100
@@ -352,7 +353,7 @@ public function deleteFile(string $filename): void
352353 */
353354 public function deleteDir (string $ dirname ): void
354355 {
355- $ this ->delete ($ this ->absolutizePath ($ dirname ));
356+ $ this ->delete ($ this ->absolutizePath ($ dirname ), true );
356357 }
357358
358359 /**
@@ -516,7 +517,7 @@ private function _closeConnection(): void
516517 */
517518 private function _listFiles (string $ path ): array
518519 {
519- $ files = $ this ->isSFTP () ? @ $ this ->ftp -> nlist ( $ path ) : @ftp_nlist ($ this ->ftp , $ path );
520+ $ files = $ this ->isSFTP () ? $ this ->sftp ( ' nlist ' , $ path ) : @ftp_nlist ($ this ->ftp , $ path );
520521
521522 if ($ files === false ) {
522523 $ this ->fail ("couldn't list files " );
@@ -530,7 +531,7 @@ private function _listFiles(string $path): array
530531 */
531532 private function _directory (): string
532533 {
533- $ pwd = $ this ->isSFTP () ? @ $ this ->ftp -> pwd ( ) : @ftp_pwd ($ this ->ftp );
534+ $ pwd = $ this ->isSFTP () ? $ this ->sftp ( ' pwd ' ) : @ftp_pwd ($ this ->ftp );
534535
535536 if (!$ pwd ) {
536537 $ this ->fail ("couldn't get current directory " );
@@ -544,7 +545,7 @@ private function _directory(): string
544545 */
545546 private function _changeDirectory (string $ path ): void
546547 {
547- $ changed = $ this ->isSFTP () ? @ $ this ->ftp -> chdir ( $ path ) : @ftp_chdir ($ this ->ftp , $ path );
548+ $ changed = $ this ->isSFTP () ? $ this ->sftp ( ' chdir ' , $ path ) : @ftp_chdir ($ this ->ftp , $ path );
548549
549550 if (!$ changed ) {
550551 $ this ->fail ("couldn't change directory {$ path }" );
@@ -565,7 +566,7 @@ private function _openFile(string $filename): void
565566 $ tmp_file = $ this ->config ['tmp ' ] . "/ftp_data_file.tmp " ;
566567
567568 if ($ this ->isSFTP ()) {
568- $ downloaded = @ $ this ->ftp -> get ( $ filename , $ tmp_file );
569+ $ downloaded = $ this ->sftp ( ' get ' , $ filename , $ tmp_file );
569570 } else {
570571 $ downloaded = @ftp_get ($ this ->ftp , $ tmp_file , $ filename , FTP_BINARY );
571572 }
@@ -602,9 +603,9 @@ private function _writeToFile(string $filename, string $contents): void
602603
603604 // Upload the file to server
604605 if ($ this ->isSFTP ()) {
605- $ flag = defined ( ' NET_SFTP_LOCAL_FILE ' ) ? NET_SFTP_LOCAL_FILE : \ phpseclib \ Net \ SFTP ::SOURCE_LOCAL_FILE ;
606+ $ flag = $ this -> ftp ::SOURCE_LOCAL_FILE ;
606607
607- $ uploaded = @ $ this ->ftp -> put ( $ filename , $ tmp_file , $ flag );
608+ $ uploaded = $ this ->sftp ( ' put ' , $ filename , $ tmp_file , $ flag );
608609 } else {
609610 $ uploaded = ftp_put ($ this ->ftp , $ filename , $ tmp_file , FTP_BINARY );
610611 }
@@ -619,7 +620,7 @@ private function _writeToFile(string $filename, string $contents): void
619620 */
620621 private function makeDirectory (string $ path ): void
621622 {
622- $ created = $ this ->isSFTP () ? @ $ this ->ftp -> mkdir ( $ path , true ) : @ftp_mkdir ($ this ->ftp , $ path );
623+ $ created = $ this ->isSFTP () ? $ this ->sftp ( ' mkdir ' , $ path, - 1 , true ) : @ftp_mkdir ($ this ->ftp , $ path );
623624
624625 if (!$ created ) {
625626 $ this ->fail ("couldn't make directory {$ path }" );
@@ -633,7 +634,7 @@ private function makeDirectory(string $path): void
633634 */
634635 private function renameDirectory (string $ path , string $ rename ): void
635636 {
636- $ renamed = $ this ->isSFTP () ? @ $ this ->ftp -> rename ( $ path , $ rename ) : @ftp_rename ($ this ->ftp , $ path , $ rename );
637+ $ renamed = $ this ->isSFTP () ? $ this ->sftp ( ' rename ' , $ path , $ rename ) : @ftp_rename ($ this ->ftp , $ path , $ rename );
637638
638639 if (!$ renamed ) {
639640 $ this ->fail ("couldn't rename directory {$ path } to {$ rename }" );
@@ -647,7 +648,7 @@ private function renameDirectory(string $path, string $rename): void
647648 */
648649 private function delete (string $ filename , bool $ isDir = false ): void
649650 {
650- $ deleted = $ this ->isSFTP () ? @ $ this ->ftp -> delete ( $ filename , $ isDir ) : @$ this ->ftpDelete ($ filename );
651+ $ deleted = $ this ->isSFTP () ? $ this ->sftp ( ' delete ' , $ filename , $ isDir ) : @$ this ->ftpDelete ($ filename );
651652
652653 if (!$ deleted ) {
653654 $ this ->fail ("couldn't delete {$ filename }" );
@@ -685,7 +686,7 @@ private function ftpDelete(string $directory): bool
685686 private function clearDirectory (string $ path ): void
686687 {
687688 $ this ->debug ("Clear directory: {$ path }" );
688- $ this ->delete ($ path );
689+ $ this ->delete ($ path, true );
689690 $ this ->makeDirectory ($ path );
690691 }
691692
@@ -694,7 +695,7 @@ private function clearDirectory(string $path): void
694695 */
695696 private function size (string $ filename ): int
696697 {
697- $ size = $ this ->isSFTP () ? (int )@ $ this ->ftp -> size ( $ filename ) : @ftp_size ($ this ->ftp , $ filename );
698+ $ size = $ this ->isSFTP () ? (int )$ this ->sftp ( ' filesize ' , $ filename ) : @ftp_size ($ this ->ftp , $ filename );
698699
699700 if ($ size < 0 ) {
700701 $ this ->fail ("couldn't get the file size for {$ filename }" );
@@ -709,7 +710,7 @@ private function size(string $filename): int
709710 private function modified (string $ filename ): int
710711 {
711712 if ($ this ->isSFTP ()) {
712- $ info = @ $ this ->ftp -> lstat ( $ filename );
713+ $ info = $ this ->sftp ( ' lstat ' , $ filename );
713714 if ($ info ) {
714715 return $ info ['mtime ' ];
715716 }
@@ -722,29 +723,32 @@ private function modified(string $filename): int
722723
723724 protected function sftpConnect (string $ user , string $ password ): void
724725 {
725- if (class_exists ('Net_SFTP ' )) {
726- $ this ->ftp = new \Net_SFTP ($ this ->config ['host ' ], $ this ->config ['port ' ], $ this ->config ['timeout ' ]);
727- } elseif (class_exists (\phpseclib \Net \SFTP ::class)) {
728- $ this ->ftp = new \phpseclib \Net \SFTP ($ this ->config ['host ' ], $ this ->config ['port ' ], $ this ->config ['timeout ' ]);
726+ if (class_exists (SFTPv4::class)) {
727+ $ this ->ftp = new SFTPv4 ($ this ->config ['host ' ], $ this ->config ['port ' ], $ this ->config ['timeout ' ]);
728+ } elseif (class_exists (SFTPv3::class)) {
729+ $ this ->ftp = new SFTPv3 ($ this ->config ['host ' ], $ this ->config ['port ' ], $ this ->config ['timeout ' ]);
730+ } elseif (class_exists (SFTPv2::class)) {
731+ $ this ->ftp = new SFTPv2 ($ this ->config ['host ' ], $ this ->config ['port ' ], $ this ->config ['timeout ' ]);
729732 } else {
730733 throw new ModuleException ('FTP ' , 'phpseclib/phpseclib library is not installed ' );
731734 }
732735
733736 if (!empty ($ this ->config ['key ' ])) {
734737 $ keyFile = file_get_contents ($ this ->config ['key ' ]);
735738
736- if (class_exists ('Crypt_RSA ' )) {
737- $ password = new \Crypt_RSA ();
739+ if (class_exists (\phpseclib4 \Crypt \PublicKeyLoader::class)) {
740+ $ password = \phpseclib4 \Crypt \PublicKeyLoader ::load ($ keyFile );
741+ } elseif (class_exists (\phpseclib3 \Crypt \PublicKeyLoader::class)) {
742+ $ password = \phpseclib3 \Crypt \PublicKeyLoader ::load ($ keyFile );
738743 } elseif (class_exists (\phpseclib \Crypt \RSA ::class)) {
739744 $ password = new \phpseclib \Crypt \RSA ();
745+ $ password ->loadKey ($ keyFile );
740746 } else {
741747 throw new ModuleException ('FTP ' , 'phpseclib/phpseclib library is not installed ' );
742748 }
743-
744- $ password ->loadKey ($ keyFile );
745749 }
746750
747- if (!$ this ->ftp -> login ( $ user , $ password )) {
751+ if (!$ this ->sftp ( ' login ' , $ user , $ password )) {
748752 $ this ->fail ('failed to authenticate user ' );
749753 }
750754 }
@@ -768,6 +772,25 @@ protected function ftpConnect(string $user, string $password): void
768772 }
769773 }
770774
775+ /**
776+ * Calls a method on the phpseclib SFTP connection.
777+ *
778+ * phpseclib v2/v3 return false on failure, v4 throws exceptions and
779+ * returns void for many methods. Normalize to false on failure and
780+ * true for void results.
781+ */
782+ private function sftp (string $ method , mixed ...$ args ): mixed
783+ {
784+ try {
785+ $ result = @$ this ->ftp ->{$ method }(...$ args );
786+ } catch (\Exception $ e ) {
787+ $ this ->debug ("SFTP {$ method }() failed: {$ e ->getMessage ()}" );
788+ return false ;
789+ }
790+
791+ return $ result ?? true ;
792+ }
793+
771794 protected function isSFTP (): bool
772795 {
773796 return strtolower ($ this ->config ['type ' ]) === 'sftp ' ;
0 commit comments