Last active
January 13, 2019 01:09
-
-
Save bhubbard/370ce6023ce9fbb5605581ae425e841d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* PemFTP - A Ftp implementation in pure PHP | |
* | |
* @package PemFTP | |
* @since 2.5.0 | |
* | |
* @version 1.0 | |
* @copyright Alexey Dotsenko | |
* @author Alexey Dotsenko | |
* @link https://www.phpclasses.org/browse/package/1743.html Site | |
* @license LGPL https://www.opensource.org/licenses/lgpl-license.html | |
*/ | |
/** | |
* FTP implementation using fsockopen to connect. | |
* | |
* @package PemFTP | |
* @subpackage Pure | |
* @since 2.5.0 | |
* | |
* @version 1.0 | |
* @copyright Alexey Dotsenko | |
* @author Alexey Dotsenko | |
* @link https://www.phpclasses.org/browse/package/1743.html Site | |
* @license LGPL https://www.opensource.org/licenses/lgpl-license.html | |
*/ | |
class ftp_pure extends ftp_base { | |
/** | |
* Constructor. | |
* | |
* @access public | |
* @param bool $verb (default: false) Verb. | |
* @param bool $le (default: false) LE. | |
*/ | |
public function __construct( $verb = false, $le = false ) { | |
parent::__construct( false, $verb, $le ); | |
} | |
/** | |
* _settimeout function. | |
* | |
* @access private | |
* @param mixed $sock Sock. | |
*/ | |
private function _settimeout( $sock ) { | |
if ( ! @stream_set_timeout( $sock, $this->_timeout ) ) { | |
$this->PushError( '_settimeout', 'socket set send timeout' ); | |
$this->_quit(); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _connect function. | |
* | |
* @access private | |
* @param mixed $host Host. | |
* @param mixed $port Port. | |
*/ | |
private function _connect( $host, $port ) { | |
$this->SendMSG( 'Creating socket' ); | |
$sock = @fsockopen( $host, $port, $errno, $errstr, $this->_timeout ); | |
if ( ! $sock ) { | |
$this->PushError( '_connect', 'socket connect failed', $errstr . ' (' . $errno . ')' ); | |
return false; | |
} | |
$this->_connected = true; | |
return $sock; | |
} | |
/** | |
* _readmsg function. | |
* | |
* @access private | |
* @param string $fnction (default: '_readmsg') FNCTION. | |
*/ | |
private function _readmsg( $fnction = '_readmsg' ) { | |
if ( ! $this->_connected ) { | |
$this->PushError( $fnction, 'Connect first' ); | |
return false; | |
} | |
$result = true; | |
$this->_message = ''; | |
$this->_code = 0; | |
$go = true; | |
do { | |
$tmp = @fgets( $this->_ftp_control_sock, 512 ); | |
if ( false === $tmp ) { | |
$go = $result = false; | |
$this->PushError( $fnction, 'Read failed' ); | |
} else { | |
$this->_message .= $tmp; | |
if ( preg_match( '/^([0-9]{3})(-(.*[' . CRLF . "]{1,2})+\\1)? [^" . CRLF . ']+[' . CRLF . ']{1,2}$/', $this->_message, $regs ) ) { | |
$go = false; | |
} | |
} | |
} while ( $go ); | |
if ( $this->LocalEcho ) { | |
echo 'GET < ' . rtrim( $this->_message, CRLF ) . CRLF; | |
} | |
$this->_code = (int) $regs[1]; | |
return $result; | |
} | |
/** | |
* _exec function. | |
* | |
* @access private | |
* @param mixed $cmd CMD. | |
* @param string $fnction (default: '_exec') FNCTION. | |
*/ | |
private function _exec( $cmd, $fnction = '_exec' ) { | |
if ( ! $this->_ready ) { | |
$this->PushError( $fnction, 'Connect first' ); | |
return false; | |
} | |
if ( $this->LocalEcho ) { | |
echo 'PUT > ',$cmd,CRLF; | |
} | |
$status = @fputs( $this->_ftp_control_sock, $cmd . CRLF ); | |
if ( false === $status ) { | |
$this->PushError( $fnction, 'socket write failed' ); | |
return false; | |
} | |
$this->_lastaction = time(); | |
if ( ! $this->_readmsg( $fnction ) ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _data_prepare function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
*/ | |
private function _data_prepare( $mode = FTP_ASCII ) { | |
if ( ! $this->_settype( $mode ) ) { | |
return false; | |
} | |
if ( $this->_passive ) { | |
if ( ! $this->_exec( 'PASV', 'pasv' ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
$ip_port = explode( ',', preg_replace( '/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s', "\\1", $this->_message ) ); | |
$this->_datahost = $ip_port[0] . '.' . $ip_port[1] . '.' . $ip_port[2] . '.' . $ip_port[3]; | |
$this->_dataport = ( ( (int) $ip_port[4] ) << 8 ) + ( (int) $ip_port[5] ); | |
$this->SendMSG( 'Connecting to ' . $this->_datahost . ':' . $this->_dataport ); | |
$this->_ftp_data_sock = @fsockopen( $this->_datahost, $this->_dataport, $errno, $errstr, $this->_timeout ); | |
if ( ! $this->_ftp_data_sock ) { | |
$this->PushError( '_data_prepare', 'fsockopen fails', $errstr . ' (' . $errno . ')' ); | |
$this->_data_close(); | |
return false; | |
} else { | |
$this->_ftp_data_sock; | |
} | |
} else { | |
$this->SendMSG( 'Only passive connections available!' ); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _data_read function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
* @param mixed $fp (default: null) FP. | |
*/ | |
private function _data_read( $mode = FTP_ASCII, $fp = null ) { | |
if ( is_resource( $fp ) ) { | |
$out = 0; | |
} else { | |
$out = ''; | |
} | |
if ( ! $this->_passive ) { | |
$this->SendMSG( 'Only passive connections available!' ); | |
return false; | |
} | |
while ( ! feof( $this->_ftp_data_sock ) ) { | |
$block = fread( $this->_ftp_data_sock, $this->_ftp_buff_size ); | |
if ( FTP_BINARY != $mode ) { | |
$block = preg_replace( "/\r\n|\r|\n/", $this->_eol_code[ $this->OS_local ], $block ); | |
} | |
if ( is_resource( $fp ) ) { | |
$out += fwrite( $fp, $block, strlen( $block ) ); | |
} else { | |
$out .= $block; | |
} | |
} | |
return $out; | |
} | |
/** | |
* _data_write function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
* @param mixed $fp (default: null) FP. | |
*/ | |
private function _data_write( $mode = FTP_ASCII, $fp = null ) { | |
if ( is_resource( $fp ) ) { | |
$out = 0; | |
} else { | |
$out = ''; | |
} | |
if ( ! $this->_passive ) { | |
$this->SendMSG( 'Only passive connections available!' ); | |
return false; | |
} | |
if ( is_resource( $fp ) ) { | |
while ( ! feof( $fp ) ) { | |
$block = fread( $fp, $this->_ftp_buff_size ); | |
if ( ! $this->_data_write_block( $mode, $block ) ) { | |
return false; | |
} | |
} | |
} elseif ( ! $this->_data_write_block( $mode, $fp ) ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _data_write_block function. | |
* | |
* @access private | |
* @param mixed $mode Mode. | |
* @param mixed $block Block. | |
*/ | |
private function _data_write_block( $mode, $block ) { | |
if ( FTP_BINARY != $mode ) { | |
$block = preg_replace( "/\r\n|\r|\n/", $this->_eol_code[ $this->OS_remote ], $block ); | |
} | |
do { | |
if ( ( $t = @fwrite( $this->_ftp_data_sock, $block ) ) === false ) { | |
$this->PushError( '_data_write', "Can't write to socket" ); | |
return false; | |
} | |
$block = substr( $block, $t ); | |
} while ( ! empty( $block ) ); | |
return true; | |
} | |
/** | |
* _data_close function. | |
* | |
* @access private | |
*/ | |
private function _data_close() { | |
@fclose( $this->_ftp_data_sock ); | |
$this->SendMSG( 'Disconnected data from remote host' ); | |
return true; | |
} | |
/** | |
* _quit function. | |
* | |
* @access private | |
* @param bool $force (default: false) Force. | |
*/ | |
private function _quit( $force = false ) { | |
if ( $this->_connected || $force ) { | |
@fclose( $this->_ftp_control_sock ); | |
$this->_connected = false; | |
$this->SendMSG( 'Socket closed' ); | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* PemFTP - A Ftp implementation in pure PHP | |
* | |
* @package PemFTP | |
* @since 2.5.0 | |
* | |
* @version 1.0 | |
* @copyright Alexey Dotsenko | |
* @author Alexey Dotsenko | |
* @link http://www.phpclasses.org/browse/package/1743.html Site | |
* @license LGPL http://www.opensource.org/licenses/lgpl-license.html | |
*/ | |
/** | |
* Socket Based FTP implementation | |
* | |
* @package PemFTP | |
* @subpackage Socket | |
* @since 2.5.0 | |
* | |
* @version 1.0 | |
* @copyright Alexey Dotsenko | |
* @author Alexey Dotsenko | |
* @link http://www.phpclasses.org/browse/package/1743.html Site | |
* @license LGPL http://www.opensource.org/licenses/lgpl-license.html | |
*/ | |
class ftp_sockets extends ftp_base { | |
/** | |
* Constructor. | |
* | |
* @access public | |
* @param bool $verb (default: false) Verb. | |
* @param bool $le (default: false) LE. | |
*/ | |
public function __construct( $verb = false, $le = false ) { | |
parent::__construct( true, $verb, $le ); | |
} | |
/** | |
* _settimeout function. | |
* | |
* @access private | |
* @param mixed $sock Sock. | |
*/ | |
private function _settimeout( $sock ) { | |
if ( ! @socket_set_option( | |
$sock, | |
SOL_SOCKET, | |
SO_RCVTIMEO, | |
array( | |
'sec' => $this->_timeout, | |
'usec' => 0, | |
) | |
) ) { | |
$this->PushError( '_connect', 'socket set receive timeout', socket_strerror( socket_last_error( $sock ) ) ); | |
@socket_close( $sock ); | |
return false; | |
} | |
if ( ! @socket_set_option( | |
$sock, | |
SOL_SOCKET, | |
SO_SNDTIMEO, | |
array( | |
'sec' => $this->_timeout, | |
'usec' => 0, | |
) | |
) ) { | |
$this->PushError( '_connect', 'socket set send timeout', socket_strerror( socket_last_error( $sock ) ) ); | |
@socket_close( $sock ); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _connect function. | |
* | |
* @access private | |
* @param mixed $host Host. | |
* @param mixed $port Port. | |
*/ | |
private function _connect( $host, $port ) { | |
$this->SendMSG( 'Creating socket' ); | |
if ( ! ( $sock = @socket_create( AF_INET, SOCK_STREAM, SOL_TCP ) ) ) { | |
$this->PushError( '_connect', 'socket create failed', socket_strerror( socket_last_error( $sock ) ) ); | |
return false; | |
} | |
if ( ! $this->_settimeout( $sock ) ) { | |
return false; | |
} | |
$this->SendMSG( 'Connecting to "' . $host . ':' . $port . '"' ); | |
if ( ! ( $res = @socket_connect( $sock, $host, $port ) ) ) { | |
$this->PushError( '_connect', 'socket connect failed', socket_strerror( socket_last_error( $sock ) ) ); | |
@socket_close( $sock ); | |
return false; | |
} | |
$this->_connected = true; | |
return $sock; | |
} | |
/** | |
* _readmsg function. | |
* | |
* @access private | |
* @param string $fnction (default: '_readmsg') FNCTION. | |
*/ | |
private function _readmsg( $fnction = '_readmsg' ) { | |
if ( ! $this->_connected ) { | |
$this->PushError( $fnction, 'Connect first' ); | |
return false; | |
} | |
$result = true; | |
$this->_message = ''; | |
$this->_code = 0; | |
$go = true; | |
do { | |
$tmp = @socket_read( $this->_ftp_control_sock, 4096, PHP_BINARY_READ ); | |
if ( false === $tmp ) { | |
$go = $result = false; | |
$this->PushError( $fnction, 'Read failed', socket_strerror( socket_last_error( $this->_ftp_control_sock ) ) ); | |
} else { | |
$this->_message .= $tmp; | |
$go = ! preg_match( "/^([0-9]{3})(-.+\\1)? [^" . CRLF . ']+' . CRLF . '$/Us', $this->_message, $regs ); | |
} | |
} while ( $go ); | |
if ( $this->LocalEcho ) { | |
echo 'GET < ' . rtrim( $this->_message, CRLF ) . CRLF; | |
} | |
$this->_code = (int) $regs[1]; | |
return $result; | |
} | |
/** | |
* _exec function. | |
* | |
* @access private | |
* @param mixed $cmd CMD. | |
* @param string $fnction (default: '_exec') FNCTION. | |
*/ | |
private function _exec( $cmd, $fnction = '_exec' ) { | |
if ( ! $this->_ready ) { | |
$this->PushError( $fnction, 'Connect first' ); | |
return false; | |
} | |
if ( $this->LocalEcho ) { | |
echo 'PUT > ',$cmd,CRLF; | |
} | |
$status = @socket_write( $this->_ftp_control_sock, $cmd . CRLF ); | |
if ( false === $status ) { | |
$this->PushError( $fnction, 'socket write failed', socket_strerror( socket_last_error( $this->stream ) ) ); | |
return false; | |
} | |
$this->_lastaction = time(); | |
if ( ! $this->_readmsg( $fnction ) ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _data_prepare function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
*/ | |
private function _data_prepare( $mode = FTP_ASCII ) { | |
if ( ! $this->_settype( $mode ) ) { | |
return false; | |
} | |
$this->SendMSG( 'Creating data socket' ); | |
$this->_ftp_data_sock = @socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); | |
if ( $this->_ftp_data_sock < 0 ) { | |
$this->PushError( '_data_prepare', 'socket create failed', socket_strerror( socket_last_error( $this->_ftp_data_sock ) ) ); | |
return false; | |
} | |
if ( ! $this->_settimeout( $this->_ftp_data_sock ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( $this->_passive ) { | |
if ( ! $this->_exec( 'PASV', 'pasv' ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
$ip_port = explode( ',', preg_replace( '/^.+ \\(?([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)\\)?.*$/s', "\\1", $this->_message ) ); | |
$this->_datahost = $ip_port[0] . '.' . $ip_port[1] . '.' . $ip_port[2] . '.' . $ip_port[3]; | |
$this->_dataport = ( ( (int) $ip_port[4] ) << 8 ) + ( (int) $ip_port[5] ); | |
$this->SendMSG( 'Connecting to ' . $this->_datahost . ':' . $this->_dataport ); | |
if ( ! @socket_connect( $this->_ftp_data_sock, $this->_datahost, $this->_dataport ) ) { | |
$this->PushError( '_data_prepare', 'socket_connect', socket_strerror( socket_last_error( $this->_ftp_data_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} else { | |
$this->_ftp_temp_sock = $this->_ftp_data_sock; | |
} | |
} else { | |
if ( ! @socket_getsockname( $this->_ftp_control_sock, $addr, $port ) ) { | |
$this->PushError( '_data_prepare', "can't get control socket information", socket_strerror( socket_last_error( $this->_ftp_control_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! @socket_bind( $this->_ftp_data_sock, $addr ) ) { | |
$this->PushError( '_data_prepare', "can't bind data socket", socket_strerror( socket_last_error( $this->_ftp_data_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! @socket_listen( $this->_ftp_data_sock ) ) { | |
$this->PushError( '_data_prepare', "can't listen data socket", socket_strerror( socket_last_error( $this->_ftp_data_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! @socket_getsockname( $this->_ftp_data_sock, $this->_datahost, $this->_dataport ) ) { | |
$this->PushError( '_data_prepare', "can't get data socket information", socket_strerror( socket_last_error( $this->_ftp_data_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_exec( 'PORT ' . str_replace( '.', ',', $this->_datahost . '.' . ( $this->_dataport >> 8 ) . '.' . ( $this->_dataport & 0x00FF ) ), '_port' ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* _data_read function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
* @param mixed $fp (default: null) FP. | |
*/ | |
private function _data_read( $mode = FTP_ASCII, $fp = null ) { | |
$NewLine = $this->_eol_code[ $this->OS_local ]; | |
if ( is_resource( $fp ) ) { | |
$out = 0; | |
} else { | |
$out = ''; | |
} | |
if ( ! $this->_passive ) { | |
$this->SendMSG( 'Connecting to ' . $this->_datahost . ':' . $this->_dataport ); | |
$this->_ftp_temp_sock = socket_accept( $this->_ftp_data_sock ); | |
if ( false === $this->_ftp_temp_sock ) { | |
$this->PushError( '_data_read', 'socket_accept', socket_strerror( socket_last_error( $this->_ftp_temp_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
} | |
while ( ( $block = @socket_read( $this->_ftp_temp_sock, $this->_ftp_buff_size, PHP_BINARY_READ ) ) !== false ) { | |
if ( '' === $block ) { | |
break; | |
} | |
if ( FTP_BINARY != $mode ) { | |
$block = preg_replace( "/\r\n|\r|\n/", $this->_eol_code[ $this->OS_local ], $block ); | |
} | |
if ( is_resource( $fp ) ) { | |
$out += fwrite( $fp, $block, strlen( $block ) ); | |
} else { | |
$out .= $block; | |
} | |
} | |
return $out; | |
} | |
/** | |
* _data_write function. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
* @param mixed $fp (default: null) FP. | |
*/ | |
private function _data_write( $mode = FTP_ASCII, $fp = null ) { | |
$NewLine = $this->_eol_code[ $this->OS_local ]; | |
if ( is_resource( $fp ) ) { | |
$out = 0; | |
} else { | |
$out = ''; | |
} | |
if ( ! $this->_passive ) { | |
$this->SendMSG( 'Connecting to ' . $this->_datahost . ':' . $this->_dataport ); | |
$this->_ftp_temp_sock = socket_accept( $this->_ftp_data_sock ); | |
if ( false === $this->_ftp_temp_sock ) { | |
$this->PushError( '_data_write', 'socket_accept', socket_strerror( socket_last_error( $this->_ftp_temp_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
} | |
if ( is_resource( $fp ) ) { | |
while ( ! feof( $fp ) ) { | |
$block = fread( $fp, $this->_ftp_buff_size ); | |
if ( ! $this->_data_write_block( $mode, $block ) ) { | |
return false; | |
} | |
} | |
} elseif ( ! $this->_data_write_block( $mode, $fp ) ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* _data_write_block function. | |
* | |
* @access private | |
* @param mixed $mode Mode. | |
* @param mixed $block Block. | |
*/ | |
private function _data_write_block( $mode, $block ) { | |
if ( FTP_BINARY != $mode ) { | |
$block = preg_replace( "/\r\n|\r|\n/", $this->_eol_code[ $this->OS_remote ], $block ); | |
} | |
do { | |
if ( ( $t = @socket_write( $this->_ftp_temp_sock, $block ) ) === false ) { | |
$this->PushError( '_data_write', 'socket_write', socket_strerror( socket_last_error( $this->_ftp_temp_sock ) ) ); | |
$this->_data_close(); | |
return false; | |
} | |
$block = substr( $block, $t ); | |
} while ( ! empty( $block ) ); | |
return true; | |
} | |
/** | |
* _data_close function. | |
* | |
* @access private | |
*/ | |
private function _data_close() { | |
@socket_close( $this->_ftp_temp_sock ); | |
@socket_close( $this->_ftp_data_sock ); | |
$this->SendMSG( 'Disconnected data from remote host' ); | |
return true; | |
} | |
/** | |
* _quit function. | |
* | |
* @access private | |
*/ | |
private function _quit() { | |
if ( $this->_connected ) { | |
@socket_close( $this->_ftp_control_sock ); | |
$this->_connected = false; | |
$this->SendMSG( 'Socket closed' ); | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* PemFTP - A Ftp implementation in pure PHP | |
* | |
* @package PemFTP | |
* @since 2.5 | |
* | |
* @version 1.0 | |
* @copyright Alexey Dotsenko | |
* @author Alexey Dotsenko | |
* @link https://www.phpclasses.org/browse/package/1743.html Site | |
* @license LGPL https://www.opensource.org/licenses/lgpl-license.html | |
*/ | |
/** | |
* Defines the newline characters, if not defined already. | |
* | |
* This can be redefined. | |
* | |
* @since 2.5 | |
* @var string | |
*/ | |
if ( ! defined( 'CRLF' ) ) { | |
define( 'CRLF', "\r\n" ); | |
} | |
/** | |
* Sets whatever to autodetect ASCII mode. | |
* | |
* This can be redefined. | |
* | |
* @since 2.5 | |
* @var int | |
*/ | |
if ( ! defined( 'FTP_AUTOASCII' ) ) { | |
define( 'FTP_AUTOASCII', -1 ); | |
} | |
/** | |
* | |
* This can be redefined. | |
* | |
* @since 2.5 | |
* @var int | |
*/ | |
if ( ! defined( 'FTP_BINARY' ) ) { | |
define( 'FTP_BINARY', 1 ); | |
} | |
/** | |
* | |
* This can be redefined. | |
* | |
* @since 2.5 | |
* @var int | |
*/ | |
if ( ! defined( 'FTP_ASCII' ) ) { | |
define( 'FTP_ASCII', 0 ); | |
} | |
/** | |
* Whether to force FTP. | |
* | |
* This can be redefined. | |
* | |
* @since 2.5 | |
* @var bool | |
*/ | |
if ( ! defined( 'FTP_FORCE' ) ) { | |
define( 'FTP_FORCE', true ); | |
} | |
/** | |
* @since 2.5 | |
* @var string | |
*/ | |
define( 'FTP_OS_Unix', 'u' ); | |
/** | |
* @since 2.5 | |
* @var string | |
*/ | |
define( 'FTP_OS_Windows', 'w' ); | |
/** | |
* @since 2.5 | |
* @var string | |
*/ | |
define( 'FTP_OS_Mac', 'm' ); | |
/** | |
* PemFTP base class. | |
*/ | |
class ftp_base { | |
/* Public variables. */ | |
/** | |
* LocalEcho | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $LocalEcho; | |
/** | |
* Verbose | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $Verbose; | |
/** | |
* OS_local | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $OS_local; | |
/** | |
* OS_remote | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $OS_remote; | |
/* Private variables. */ | |
/** | |
* _lastaction | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_lastaction; | |
/** | |
* _errors | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_errors; | |
/** | |
* _type | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_type; | |
/** | |
* _umask | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_umask; | |
/** | |
* _timeout | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_timeout; | |
/** | |
* _passive | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_passive; | |
/** | |
* _host | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_host; | |
/** | |
* _fullhost | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_fullhost; | |
/** | |
* _port | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_port; | |
/** | |
* _datahost | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_datahost; | |
/** | |
* _dataport | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_dataport; | |
/** | |
* _ftp_control_sock | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_ftp_control_sock; | |
/** | |
* _ftp_data_sock | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_ftp_data_sock; | |
/** | |
* _ftp_temp_sock | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_ftp_temp_sock; | |
/** | |
* _ftp_buff_size | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_ftp_buff_size; | |
/** | |
* _login | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_login; | |
/** | |
* _password | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_password; | |
/** | |
* _connected | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_connected; | |
/** | |
* _ready | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_ready; | |
/** | |
* _code | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_code; | |
/** | |
* _message | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_message; | |
/** | |
* _can_restore | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_can_restore; | |
/** | |
* _port_available | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_port_available; | |
/** | |
* _curtype | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_curtype; | |
/** | |
* _features | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_features; | |
/** | |
* _error_array | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_error_array; | |
/** | |
* AuthorizedTransferMode | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $AuthorizedTransferMode; | |
/** | |
* OS_FullName | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $OS_FullName; | |
/** | |
* _eol_code | |
* | |
* @var mixed | |
* @access private | |
*/ | |
var $_eol_code; | |
/** | |
* AutoAsciiExt | |
* | |
* @var mixed | |
* @access public | |
*/ | |
var $AutoAsciiExt; | |
/** | |
* Constructor. | |
* | |
* @access public | |
* @param bool $port_mode (default: false) Port Mode. | |
* @param bool $verb (default: false) Verb. | |
* @param bool $le (default: false) LE. | |
*/ | |
public function __construct( $port_mode = false, $verb = false, $le = false ) { | |
$this->LocalEcho = $le; | |
$this->Verbose = $verb; | |
$this->_lastaction = null; | |
$this->_error_array = array(); | |
$this->_eol_code = array( | |
FTP_OS_Unix => "\n", | |
FTP_OS_Mac => "\r", | |
FTP_OS_Windows => "\r\n", | |
); | |
$this->AuthorizedTransferMode = array( FTP_AUTOASCII, FTP_ASCII, FTP_BINARY ); | |
$this->OS_FullName = array( | |
FTP_OS_Unix => 'UNIX', | |
FTP_OS_Windows => 'WINDOWS', | |
FTP_OS_Mac => 'MACOS', | |
); | |
$this->AutoAsciiExt = array( 'ASP', 'BAT', 'C', 'CPP', 'CSS', 'CSV', 'JS', 'H', 'HTM', 'HTML', 'SHTML', 'INI', 'LOG', 'PHP3', 'PHTML', 'PL', 'PERL', 'SH', 'SQL', 'TXT' ); | |
$this->_port_available = ( true == $port_mode ); | |
$this->SendMSG( 'Staring FTP client class' . ( $this->_port_available ? '' : ' without PORT mode support' ) ); | |
$this->_connected = false; | |
$this->_ready = false; | |
$this->_can_restore = false; | |
$this->_code = 0; | |
$this->_message = ''; | |
$this->_ftp_buff_size = 4096; | |
$this->_curtype = null; | |
$this->SetUmask( 0022 ); | |
$this->SetType( FTP_AUTOASCII ); | |
$this->SetTimeout( 30 ); | |
$this->Passive( ! $this->_port_available ); | |
$this->_login = 'anonymous'; | |
$this->_password = 'anon@ftp.com'; | |
$this->_features = array(); | |
$this->OS_local = FTP_OS_Unix; | |
$this->OS_remote = FTP_OS_Unix; | |
$this->features = array(); | |
if ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' ) { | |
$this->OS_local = FTP_OS_Windows; | |
} elseif ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'MAC' ) { | |
$this->OS_local = FTP_OS_Mac; | |
} | |
} | |
/** | |
* FTP Base. | |
* | |
* @access public | |
* @param bool $port_mode (default: false) Port Mode. | |
*/ | |
public function ftp_base( $port_mode = false ) { | |
$this->__construct( $port_mode ); | |
} | |
/** | |
* Parse Listing. | |
* | |
* @access public | |
* @param mixed $line Line. | |
*/ | |
public function parselisting( $line ) { | |
$is_windows = ( FTP_OS_Windows == $this->OS_remote ); | |
if ( $is_windows && preg_match( '/([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|<DIR>) +(.+)/', $line, $lucifer ) ) { | |
$b = array(); | |
if ( $lucifer[3] < 70 ) { | |
$lucifer[3] += 2000; | |
} else { | |
$lucifer[3] += 1900; } // 4digit year fix. | |
$b['isdir'] = ( '<DIR>' == $lucifer[7] ); | |
if ( $b['isdir'] ) { | |
$b['type'] = 'd'; | |
} else { | |
$b['type'] = 'f'; | |
} | |
$b['size'] = $lucifer[7]; | |
$b['month'] = $lucifer[1]; | |
$b['day'] = $lucifer[2]; | |
$b['year'] = $lucifer[3]; | |
$b['hour'] = $lucifer[4]; | |
$b['minute'] = $lucifer[5]; | |
$b['time'] = @mktime( $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) == 0 ? 12 : 0 ), $lucifer[5], 0, $lucifer[1], $lucifer[2], $lucifer[3] ); | |
$b['am/pm'] = $lucifer[6]; | |
$b['name'] = $lucifer[8]; | |
} elseif ( ! $is_windows && $lucifer = preg_split( '/[ ]/', $line, 9, PREG_SPLIT_NO_EMPTY ) ) { | |
// echo $line."\n"; | |
$lcount = count( $lucifer ); | |
if ( $lcount < 8 ) { | |
return ''; | |
} | |
$b = array(); | |
$b['isdir'] = 'd' === $lucifer[0]{0}; | |
$b['islink'] = 'l' === $lucifer[0]{0}; | |
if ( $b['isdir'] ) { | |
$b['type'] = 'd'; | |
} elseif ( $b['islink'] ) { | |
$b['type'] = 'l'; | |
} else { | |
$b['type'] = 'f'; | |
} | |
$b['perms'] = $lucifer[0]; | |
$b['number'] = $lucifer[1]; | |
$b['owner'] = $lucifer[2]; | |
$b['group'] = $lucifer[3]; | |
$b['size'] = $lucifer[4]; | |
if ( 8 == $lcount ) { | |
sscanf( $lucifer[5], '%d-%d-%d', $b['year'], $b['month'], $b['day'] ); | |
sscanf( $lucifer[6], '%d:%d', $b['hour'], $b['minute'] ); | |
$b['time'] = @mktime( $b['hour'], $b['minute'], 0, $b['month'], $b['day'], $b['year'] ); | |
$b['name'] = $lucifer[7]; | |
} else { | |
$b['month'] = $lucifer[5]; | |
$b['day'] = $lucifer[6]; | |
if ( preg_match( '/([0-9]{2}):([0-9]{2})/', $lucifer[7], $l2 ) ) { | |
$b['year'] = date( 'Y' ); | |
$b['hour'] = $l2[1]; | |
$b['minute'] = $l2[2]; | |
} else { | |
$b['year'] = $lucifer[7]; | |
$b['hour'] = 0; | |
$b['minute'] = 0; | |
} | |
$b['time'] = strtotime( sprintf( '%d %s %d %02d:%02d', $b['day'], $b['month'], $b['year'], $b['hour'], $b['minute'] ) ); | |
$b['name'] = $lucifer[8]; | |
} | |
} | |
return $b; | |
} | |
/** | |
* Send Message. | |
* | |
* @access public | |
* @param string $message (default: '') Message. | |
* @param bool $crlf (default: true) CRLF. | |
*/ | |
public function SendMSG( $message = '', $crlf = true ) { | |
if ( $this->Verbose ) { | |
echo $message . ( $crlf ? CRLF : '' ); | |
flush(); | |
} | |
return true; | |
} | |
/** | |
* Set Type. | |
* | |
* @access public | |
* @param mixed $mode (default: FTP_AUTOASCII) Mode. | |
*/ | |
public function SetType( $mode = FTP_AUTOASCII ) { | |
if ( ! in_array( $mode, $this->AuthorizedTransferMode ) ) { | |
$this->SendMSG( 'Wrong type' ); | |
return false; | |
} | |
$this->_type = $mode; | |
$this->SendMSG( 'Transfer type: ' . ( FTP_BINARY == $this->_type ? 'binary' : ( FTP_ASCII == $this->_type ? 'ASCII' : 'auto ASCII' ) ) ); | |
return true; | |
} | |
/** | |
* _settype. | |
* | |
* @access private | |
* @param mixed $mode (default: FTP_ASCII) Mode. | |
*/ | |
private function _settype( $mode = FTP_ASCII ) { | |
if ( $this->_ready ) { | |
if ( FTP_BINARY == $mode ) { | |
if ( FTP_BINARY != $this->_curtype ) { | |
if ( ! $this->_exec( 'TYPE I', 'SetType' ) ) { | |
return false; | |
} | |
$this->_curtype = FTP_BINARY; | |
} | |
} elseif ( FTP_ASCII != $this->_curtype ) { | |
if ( ! $this->_exec( 'TYPE A', 'SetType' ) ) { | |
return false; | |
} | |
$this->_curtype = FTP_ASCII; | |
} | |
} else { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Passive. | |
* | |
* @access public | |
* @param mixed $pasv (default: null) Pasv. | |
*/ | |
public function Passive( $pasv = null ) { | |
if ( is_null( $pasv ) ) { | |
$this->_passive = ! $this->_passive; | |
} else { | |
$this->_passive = $pasv; | |
} | |
if ( ! $this->_port_available && ! $this->_passive ) { | |
$this->SendMSG( 'Only passive connections available!' ); | |
$this->_passive = true; | |
return false; | |
} | |
$this->SendMSG( 'Passive mode ' . ( $this->_passive ? 'on' : 'off' ) ); | |
return true; | |
} | |
/** | |
* Set Server. | |
* | |
* @access public | |
* @param mixed $host Server Host. | |
* @param int $port (default: 21) Server Port. | |
* @param bool $reconnect (default: true) Reconnect. | |
*/ | |
public function SetServer( $host, $port = 21, $reconnect = true ) { | |
if ( ! is_long( $port ) ) { | |
$this->verbose = true; | |
$this->SendMSG( 'Incorrect port syntax' ); | |
return false; | |
} else { | |
$ip = @gethostbyname( $host ); | |
$dns = @gethostbyaddr( $host ); | |
if ( ! $ip ) { | |
$ip = $host; | |
} | |
if ( ! $dns ) { | |
$dns = $host; | |
} | |
// Validate the IPAddress PHP4 returns -1 for invalid, PHP5 false. | |
// -1 === "255.255.255.255" which is the broadcast address which is also going to be invalid. | |
$ipaslong = ip2long( $ip ); | |
if ( ( false == $ipaslong ) || ( -1 === $ipaslong ) ) { | |
$this->SendMSG( 'Wrong host name/address "' . $host . '"' ); | |
return false; | |
} | |
$this->_host = $ip; | |
$this->_fullhost = $dns; | |
$this->_port = $port; | |
$this->_dataport = $port - 1; | |
} | |
$this->SendMSG( 'Host "' . $this->_fullhost . '(' . $this->_host . '):' . $this->_port . '"' ); | |
if ( $reconnect ) { | |
if ( $this->_connected ) { | |
$this->SendMSG( 'Reconnecting' ); | |
if ( ! $this->quit( FTP_FORCE ) ) { | |
return false; | |
} | |
if ( ! $this->connect() ) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
/** | |
* Set Umask. | |
* | |
* @access public | |
* @param int $umask (default: 0022) Umask. | |
*/ | |
public function SetUmask( $umask = 0022 ) { | |
$this->_umask = $umask; | |
umask( $this->_umask ); | |
$this->SendMSG( 'UMASK 0' . decoct( $this->_umask ) ); | |
return true; | |
} | |
/** | |
* Set Timeout. | |
* | |
* @access public | |
* @param int $timeout (default: 30) Timeout. | |
*/ | |
public function SetTimeout( $timeout = 30 ) { | |
$this->_timeout = $timeout; | |
$this->SendMSG( 'Timeout ' . $this->_timeout ); | |
if ( $this->_connected ) { | |
if ( ! $this->_settimeout( $this->_ftp_control_sock ) ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Connect. | |
* | |
* @access public | |
* @param mixed $server (default: null) Server. | |
*/ | |
public function connect( $server = null ) { | |
if ( ! empty( $server ) ) { | |
if ( ! $this->SetServer( $server ) ) { | |
return false; | |
} | |
} | |
if ( $this->_ready ) { | |
return true; | |
} | |
$this->SendMsg( 'Local OS : ' . $this->OS_FullName[ $this->OS_local ] ); | |
if ( ! ( $this->_ftp_control_sock = $this->_connect( $this->_host, $this->_port ) ) ) { | |
$this->SendMSG( 'Error : Cannot connect to remote host "' . $this->_fullhost . ' :' . $this->_port . '"' ); | |
return false; | |
} | |
$this->SendMSG( 'Connected to remote host "' . $this->_fullhost . ':' . $this->_port . '". Waiting for greeting.' ); | |
do { | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
$this->_lastaction = time(); | |
} while ( $this->_code < 200 ); | |
$this->_ready = true; | |
$syst = $this->systype(); | |
if ( ! $syst ) { | |
$this->SendMSG( "Can't detect remote OS" ); | |
} else { | |
if ( preg_match( '/win|dos|novell/i', $syst[0] ) ) { | |
$this->OS_remote = FTP_OS_Windows; | |
} elseif ( preg_match( '/os/i', $syst[0] ) ) { | |
$this->OS_remote = FTP_OS_Mac; | |
} elseif ( preg_match( '/(li|u)nix/i', $syst[0] ) ) { | |
$this->OS_remote = FTP_OS_Unix; | |
} else { | |
$this->OS_remote = FTP_OS_Mac; | |
} | |
$this->SendMSG( 'Remote OS: ' . $this->OS_FullName[ $this->OS_remote ] ); | |
} | |
if ( ! $this->features() ) { | |
$this->SendMSG( "Can't get features list. All supported - disabled" ); | |
} else { | |
$this->SendMSG( 'Supported features: ' . implode( ', ', array_keys( $this->_features ) ) ); | |
} | |
return true; | |
} | |
/** | |
* Quit. | |
* | |
* @access public | |
* @param bool $force (default: false) Force Quit. | |
*/ | |
public function quit( $force = false ) { | |
if ( $this->_ready ) { | |
if ( ! $this->_exec( 'QUIT' ) && ! $force ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() && ! $force ) { | |
return false; | |
} | |
$this->_ready = false; | |
$this->SendMSG( 'Session finished' ); | |
} | |
$this->_quit(); | |
return true; | |
} | |
/** | |
* FTP Login. | |
* | |
* @access public | |
* @param mixed $user (default: null) User. | |
* @param mixed $pass (default: null) Password. | |
*/ | |
public function login( $user = null, $pass = null ) { | |
if ( ! is_null( $user ) ) { | |
$this->_login = $user; | |
} else { | |
$this->_login = 'anonymous'; | |
} | |
if ( ! is_null( $pass ) ) { | |
$this->_password = $pass; | |
} else { | |
$this->_password = 'anon@anon.com'; | |
} | |
if ( ! $this->_exec( 'USER ' . $this->_login, 'login' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
if ( 230 != $this->_code ) { | |
if ( ! $this->_exec( ( ( 331 == $this->_code ) ? 'PASS ' : 'ACCT ' ) . $this->_password, 'login' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
} | |
$this->SendMSG( 'Authentication succeeded' ); | |
if ( empty( $this->_features ) ) { | |
if ( ! $this->features() ) { | |
$this->SendMSG( "Can't get features list. All supported - disabled" ); | |
} else { | |
$this->SendMSG( 'Supported features: ' . implode( ', ', array_keys( $this->_features ) ) ); | |
} | |
} | |
return true; | |
} | |
/** | |
* PWD. | |
* | |
* @access public | |
*/ | |
public function pwd() { | |
if ( ! $this->_exec( 'PWD', 'pwd' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return preg_replace( '/^[0-9]{3} "(.+)".*$/s', "\\1", $this->_message ); | |
} | |
/** | |
* CDUP. | |
* | |
* @access public | |
*/ | |
public function cdup() { | |
if ( ! $this->_exec( 'CDUP', 'cdup' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* CHDIR. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function chdir( $pathname ) { | |
if ( ! $this->_exec( 'CWD ' . $pathname, 'chdir' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Remove Directory. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function rmdir( $pathname ) { | |
if ( ! $this->_exec( 'RMD ' . $pathname, 'rmdir' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Make Directory. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function mkdir( $pathname ) { | |
if ( ! $this->_exec( 'MKD ' . $pathname, 'mkdir' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Rename. | |
* | |
* @access public | |
* @param mixed $from From Name. | |
* @param mixed $to To Name. | |
*/ | |
public function rename( $from, $to ) { | |
if ( ! $this->_exec( 'RNFR ' . $from, 'rename' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
if ( 350 == $this->_code ) { | |
if ( ! $this->_exec( 'RNTO ' . $to, 'rename' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
} else { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Filesize. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function filesize( $pathname ) { | |
if ( ! isset( $this->_features['SIZE'] ) ) { | |
$this->PushError( 'filesize', 'not supported by server' ); | |
return false; | |
} | |
if ( ! $this->_exec( 'SIZE ' . $pathname, 'filesize' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return preg_replace( '/^[0-9]{3} ([0-9]+).*$/s', "\\1", $this->_message ); | |
} | |
/** | |
* Abort. | |
* | |
* @access public | |
*/ | |
public function abort() { | |
if ( ! $this->_exec( 'ABOR', 'abort' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
if ( 426 != $this->_code ) { | |
return false; | |
} | |
if ( ! $this->_readmsg( 'abort' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* MDTM. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function mdtm( $pathname ) { | |
if ( ! isset( $this->_features['MDTM'] ) ) { | |
$this->PushError( 'mdtm', 'not supported by server' ); | |
return false; | |
} | |
if ( ! $this->_exec( 'MDTM ' . $pathname, 'mdtm' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
$mdtm = preg_replace( '/^[0-9]{3} ([0-9]+).*$/s', "\\1", $this->_message ); | |
$date = sscanf( $mdtm, '%4d%2d%2d%2d%2d%2d' ); | |
$timestamp = mktime( $date[3], $date[4], $date[5], $date[1], $date[2], $date[0] ); | |
return $timestamp; | |
} | |
/** | |
* SYSTYPE. | |
* | |
* @access public | |
*/ | |
public function systype() { | |
if ( ! $this->_exec( 'SYST', 'systype' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
$DATA = explode( ' ', $this->_message ); | |
return array( $DATA[1], $DATA[3] ); | |
} | |
/** | |
* DELETE. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function delete( $pathname ) { | |
if ( ! $this->_exec( 'DELE ' . $pathname, 'delete' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Site. | |
* | |
* @access public | |
* @param mixed $command Command. | |
* @param string $fnction (default: 'site') FNCTION. | |
*/ | |
public function site( $command, $fnction = 'site' ) { | |
if ( ! $this->_exec( 'SITE ' . $command, $fnction ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* CHMOD. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
* @param mixed $mode Mode. | |
*/ | |
public function chmod( $pathname, $mode ) { | |
if ( ! $this->site( sprintf( 'CHMOD %o %s', $mode, $pathname ), 'chmod' ) ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Restore. | |
* | |
* @access public | |
* @param mixed $from From. | |
*/ | |
public function restore( $from ) { | |
if ( ! isset( $this->_features['REST'] ) ) { | |
$this->PushError( 'restore', 'not supported by server' ); | |
return false; | |
} | |
if ( FTP_BINARY != $this->_curtype ) { | |
$this->PushError( 'restore', "can't restore in ASCII mode" ); | |
return false; | |
} | |
if ( ! $this->_exec( 'REST ' . $from, 'resore' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Features. | |
* | |
* @access public | |
*/ | |
public function features() { | |
if ( ! $this->_exec( 'FEAT', 'features' ) ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
$f = preg_split( '/[' . CRLF . ']+/', preg_replace( '/[0-9]{3}[ -].*[' . CRLF . ']+/', '', $this->_message ), -1, PREG_SPLIT_NO_EMPTY ); | |
$this->_features = array(); | |
foreach ( $f as $k => $v ) { | |
$v = explode( ' ', trim( $v ) ); | |
$this->_features[ array_shift( $v ) ] = $v; | |
} | |
return true; | |
} | |
/** | |
* Raw List. | |
* | |
* @access public | |
* @param string $pathname (default: '') Pathname. | |
* @param string $arg (default: '') Arguments. | |
*/ | |
public function rawlist( $pathname = '', $arg = '' ) { | |
return $this->_list( ( $arg ? ' ' . $arg : '' ) . ( $pathname ? ' ' . $pathname : '' ), 'LIST', 'rawlist' ); | |
} | |
/** | |
* NLST. | |
* | |
* @access public | |
* @param string $pathname (default: '') Pathname. | |
* @param string $arg (default: '') Arguments. | |
*/ | |
public function nlist( $pathname = '', $arg = '' ) { | |
return $this->_list( ( $arg ? ' ' . $arg : '' ) . ( $pathname ? ' ' . $pathname : '' ), 'NLST', 'nlist' ); | |
} | |
/** | |
* Is Exists. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function is_exists( $pathname ) { | |
return $this->file_exists( $pathname ); | |
} | |
/** | |
* File Exists. | |
* | |
* @access public | |
* @param mixed $pathname Pathname. | |
*/ | |
public function file_exists( $pathname ) { | |
$exists = true; | |
if ( ! $this->_exec( 'RNFR ' . $pathname, 'rename' ) ) { | |
$exists = false; | |
} else { | |
if ( ! $this->_checkCode() ) { | |
$exists = false; | |
} | |
$this->abort(); | |
} | |
if ( $exists ) { | |
$this->SendMSG( 'Remote file ' . $pathname . ' exists' ); | |
} else { | |
$this->SendMSG( 'Remote file ' . $pathname . ' does not exist' ); | |
} | |
return $exists; | |
} | |
/** | |
* FGET. | |
* | |
* @access public | |
* @param mixed $fp FP. | |
* @param mixed $remotefile RemoteFile. | |
* @param int $rest (default: 0) Rest. | |
*/ | |
public function fget( $fp, $remotefile, $rest = 0 ) { | |
if ( $this->_can_restore && 0 != $rest ) { | |
fseek( $fp, $rest ); | |
} | |
$pi = pathinfo( $remotefile ); | |
if ( FTP_ASCII == $this->_type || ( FTP_AUTOASCII == $this->_type && in_array( strtoupper( $pi['extension'] ), $this->AutoAsciiExt ) ) ) { | |
$mode = FTP_ASCII; | |
} else { | |
$mode = FTP_BINARY; | |
} | |
if ( ! $this->_data_prepare( $mode ) ) { | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
$this->restore( $rest ); | |
} | |
if ( ! $this->_exec( 'RETR ' . $remotefile, 'get' ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
$out = $this->_data_read( $mode, $fp ); | |
$this->_data_close(); | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return $out; | |
} | |
/** | |
* Get. | |
* | |
* @access public | |
* @param mixed $remotefile Remote File. | |
* @param mixed $localfile (default: null) Local File. | |
* @param int $rest (default: 0) Rest. | |
*/ | |
public function get( $remotefile, $localfile = null, $rest = 0 ) { | |
if ( is_null( $localfile ) ) { | |
$localfile = $remotefile; | |
} | |
if ( @file_exists( $localfile ) ) { | |
$this->SendMSG( 'Warning : local file will be overwritten' ); | |
} | |
$fp = @fopen( $localfile, 'w' ); | |
if ( ! $fp ) { | |
$this->PushError( 'get', "can't open local file", 'Cannot create "' . $localfile . '"' ); | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
fseek( $fp, $rest ); | |
} | |
$pi = pathinfo( $remotefile ); | |
if ( FTP_ASCII == $this->_type || ( FTP_AUTOASCII == $this->_type && in_array( strtoupper( $pi['extension'] ), $this->AutoAsciiExt ) ) ) { | |
$mode = FTP_ASCII; | |
} else { | |
$mode = FTP_BINARY; | |
} | |
if ( ! $this->_data_prepare( $mode ) ) { | |
fclose( $fp ); | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
$this->restore( $rest ); | |
} | |
if ( ! $this->_exec( 'RETR ' . $remotefile, 'get' ) ) { | |
$this->_data_close(); | |
fclose( $fp ); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
fclose( $fp ); | |
return false; | |
} | |
$out = $this->_data_read( $mode, $fp ); | |
fclose( $fp ); | |
$this->_data_close(); | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return $out; | |
} | |
/** | |
* FPUT. | |
* | |
* @access public | |
* @param mixed $remotefile Remote File. | |
* @param mixed $fp FP. | |
* @param int $rest (default: 0) Rest. | |
*/ | |
public function fput( $remotefile, $fp, $rest = 0 ) { | |
if ( $this->_can_restore && 0 != $rest ) { | |
fseek( $fp, $rest ); | |
} | |
$pi = pathinfo( $remotefile ); | |
if ( FTP_ASCII == $this->_type || ( FTP_AUTOASCII == $this->_type && in_array( strtoupper( $pi['extension'] ), $this->AutoAsciiExt ) ) ) { | |
$mode = FTP_ASCII; | |
} else { | |
$mode = FTP_BINARY; | |
} | |
if ( ! $this->_data_prepare( $mode ) ) { | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
$this->restore( $rest ); | |
} | |
if ( ! $this->_exec( 'STOR ' . $remotefile, 'put' ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
$ret = $this->_data_write( $mode, $fp ); | |
$this->_data_close(); | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return $ret; | |
} | |
/** | |
* PUT. | |
* | |
* @access public | |
* @param mixed $localfile Local File. | |
* @param mixed $remotefile (default: null) Remote File. | |
* @param int $rest (default: 0) Reset. | |
*/ | |
public function put( $localfile, $remotefile = null, $rest = 0 ) { | |
if ( is_null( $remotefile ) ) { | |
$remotefile = $localfile; | |
} | |
if ( ! file_exists( $localfile ) ) { | |
$this->PushError( 'put', "can't open local file", 'No such file or directory "' . $localfile . '"' ); | |
return false; | |
} | |
$fp = @fopen( $localfile, 'r' ); | |
if ( ! $fp ) { | |
$this->PushError( 'put', "can't open local file", 'Cannot read file "' . $localfile . '"' ); | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
fseek( $fp, $rest ); | |
} | |
$pi = pathinfo( $localfile ); | |
if ( FTP_ASCII == $this->_type || ( FTP_AUTOASCII == $this->_type && in_array( strtoupper( $pi['extension'] ), $this->AutoAsciiExt ) ) ) { | |
$mode = FTP_ASCII; | |
} else { | |
$mode = FTP_BINARY; | |
} | |
if ( ! $this->_data_prepare( $mode ) ) { | |
fclose( $fp ); | |
return false; | |
} | |
if ( $this->_can_restore && 0 != $rest ) { | |
$this->restore( $rest ); | |
} | |
if ( ! $this->_exec( 'STOR ' . $remotefile, 'put' ) ) { | |
$this->_data_close(); | |
fclose( $fp ); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
fclose( $fp ); | |
return false; | |
} | |
$ret = $this->_data_write( $mode, $fp ); | |
fclose( $fp ); | |
$this->_data_close(); | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
return $ret; | |
} | |
/** | |
* MPUT. | |
* | |
* @access public | |
* @param string $local (default: '.') Local. | |
* @param mixed $remote (default: null) Remote. | |
* @param bool $continious (default: false) Continious. | |
*/ | |
public function mput( $local = '.', $remote = null, $continious = false ) { | |
$local = realpath( $local ); | |
if ( ! @file_exists( $local ) ) { | |
$this->PushError( 'mput', "can't open local folder", 'Cannot stat folder "' . $local . '"' ); | |
return false; | |
} | |
if ( ! is_dir( $local ) ) { | |
return $this->put( $local, $remote ); | |
} | |
if ( empty( $remote ) ) { | |
$remote = '.'; | |
} elseif ( ! $this->file_exists( $remote ) && ! $this->mkdir( $remote ) ) { | |
return false; | |
} | |
if ( $handle = opendir( $local ) ) { | |
$list = array(); | |
while ( false !== ( $file = readdir( $handle ) ) ) { | |
if ( '.' != $file && '..' != $file ) { | |
$list[] = $file; | |
} | |
} | |
closedir( $handle ); | |
} else { | |
$this->PushError( 'mput', "can't open local folder", 'Cannot read folder "' . $local . '"' ); | |
return false; | |
} | |
if ( empty( $list ) ) { | |
return true; | |
} | |
$ret = true; | |
foreach ( $list as $el ) { | |
if ( is_dir( $local . '/' . $el ) ) { | |
$t = $this->mput( $local . '/' . $el, $remote . '/' . $el ); | |
} else { | |
$t = $this->put( $local . '/' . $el, $remote . '/' . $el ); | |
} | |
if ( ! $t ) { | |
$ret = false; | |
if ( ! $continious ) { | |
break; | |
} | |
} | |
} | |
return $ret; | |
} | |
/** | |
* MGET. | |
* | |
* @access public | |
* @param mixed $remote Remote. | |
* @param string $local (default: '.') Local. | |
* @param bool $continious (default: false) Continious. | |
*/ | |
public function mget( $remote, $local = '.', $continious = false ) { | |
$list = $this->rawlist( $remote, '-lA' ); | |
if ( false === $list ) { | |
$this->PushError( 'mget', "can't read remote folder list", "Can't read remote folder \"" . $remote . '" contents' ); | |
return false; | |
} | |
if ( empty( $list ) ) { | |
return true; | |
} | |
if ( ! @file_exists( $local ) ) { | |
if ( ! @mkdir( $local ) ) { | |
$this->PushError( 'mget', "can't create local folder", 'Cannot create folder "' . $local . '"' ); | |
return false; | |
} | |
} | |
foreach ( $list as $k => $v ) { | |
$list[ $k ] = $this->parselisting( $v ); | |
if ( ! $list[ $k ] || '.' == $list[ $k ]['name'] || '..' == $list[ $k ]['name'] ) { | |
unset( $list[ $k ] ); | |
} | |
} | |
$ret = true; | |
foreach ( $list as $el ) { | |
if ( 'd' == $el['type'] ) { | |
if ( ! $this->mget( $remote . '/' . $el['name'], $local . '/' . $el['name'], $continious ) ) { | |
$this->PushError( 'mget', "can't copy folder", "Can't copy remote folder \"" . $remote . '/' . $el['name'] . '" to local "' . $local . '/' . $el['name'] . '"' ); | |
$ret = false; | |
if ( ! $continious ) { | |
break; | |
} | |
} | |
} else { | |
if ( ! $this->get( $remote . '/' . $el['name'], $local . '/' . $el['name'] ) ) { | |
$this->PushError( 'mget', "can't copy file", "Can't copy remote file \"" . $remote . '/' . $el['name'] . '" to local "' . $local . '/' . $el['name'] . '"' ); | |
$ret = false; | |
if ( ! $continious ) { | |
break; | |
} | |
} | |
} | |
@chmod( $local . '/' . $el['name'], $el['perms'] ); | |
$t = strtotime( $el['date'] ); | |
if ( -1 !== $t && false !== $t ) { | |
@touch( $local . '/' . $el['name'], $t ); | |
} | |
} | |
return $ret; | |
} | |
/** | |
* M Delete. | |
* | |
* @access public | |
* @param mixed $remote Remote. | |
* @param bool $continious (default: false) Continious. | |
*/ | |
public function mdel( $remote, $continious = false ) { | |
$list = $this->rawlist( $remote, '-la' ); | |
if ( false === $list ) { | |
$this->PushError( 'mdel', "can't read remote folder list", "Can't read remote folder \"" . $remote . '" contents' ); | |
return false; | |
} | |
foreach ( $list as $k => $v ) { | |
$list[ $k ] = $this->parselisting( $v ); | |
if ( ! $list[ $k ] || '.' == $list[ $k ]['name'] || '..' == $list[ $k ]['name'] ) { | |
unset( $list[ $k ] ); | |
} | |
} | |
$ret = true; | |
foreach ( $list as $el ) { | |
if ( empty( $el ) ) { | |
continue; | |
} | |
if ( 'd' == $el['type'] ) { | |
if ( ! $this->mdel( $remote . '/' . $el['name'], $continious ) ) { | |
$ret = false; | |
if ( ! $continious ) { | |
break; | |
} | |
} | |
} else { | |
if ( ! $this->delete( $remote . '/' . $el['name'] ) ) { | |
$this->PushError( 'mdel', "can't delete file", "Can't delete remote file \"" . $remote . '/' . $el['name'] . '"' ); | |
$ret = false; | |
if ( ! $continious ) { | |
break; | |
} | |
} | |
} | |
} | |
if ( ! $this->rmdir( $remote ) ) { | |
$this->PushError( 'mdel', "can't delete folder", "Can't delete remote folder \"" . $remote . '/' . $el['name'] . '"' ); | |
$ret = false; | |
} | |
return $ret; | |
} | |
/** | |
* Make Directory. | |
* | |
* @access public | |
* @param mixed $dir Directory. | |
* @param int $mode (default: 0777) Mode. | |
*/ | |
public function mmkdir( $dir, $mode = 0777 ) { | |
if ( empty( $dir ) ) { | |
return false; | |
} | |
if ( $this->is_exists( $dir ) || '/' == $dir ) { | |
return true; | |
} | |
if ( ! $this->mmkdir( dirname( $dir ), $mode ) ) { | |
return false; | |
} | |
$r = $this->mkdir( $dir, $mode ); | |
$this->chmod( $dir, $mode ); | |
return $r; | |
} | |
/** | |
* Glob. | |
* | |
* @access public | |
* @param mixed $pattern Pattern. | |
* @param mixed $handle (default: null) Handle. | |
*/ | |
public function glob( $pattern, $handle = null ) { | |
$path = $output = null; | |
if ( PHP_OS == 'WIN32' ) { | |
$slash = '\\'; | |
} else { | |
$slash = '/'; | |
} | |
$lastpos = strrpos( $pattern, $slash ); | |
if ( ! ( false == $lastpos ) ) { | |
$path = substr( $pattern, 0, -$lastpos - 1 ); | |
$pattern = substr( $pattern, $lastpos ); | |
} else { | |
$path = getcwd(); | |
} | |
if ( is_array( $handle ) && ! empty( $handle ) ) { | |
foreach ( $handle as $dir ) { | |
if ( $this->glob_pattern_match( $pattern, $dir ) ) { | |
$output[] = $dir; | |
} | |
} | |
} else { | |
$handle = @opendir( $path ); | |
if ( false === $handle ) { | |
return false; | |
} | |
while ( $dir = readdir( $handle ) ) { | |
if ( $this->glob_pattern_match( $pattern, $dir ) ) { | |
$output[] = $dir; | |
} | |
} | |
closedir( $handle ); | |
} | |
if ( is_array( $output ) ) { | |
return $output; | |
} | |
return false; | |
} | |
/** | |
* Glob Pattern Match. | |
* | |
* @access public | |
* @param mixed $pattern Pattern. | |
* @param mixed $string String. | |
*/ | |
public function glob_pattern_match( $pattern, $string ) { | |
$out = null; | |
$chunks = explode( ';', $pattern ); | |
foreach ( $chunks as $pattern ) { | |
$escape = array( '$', '^', '.', '{', '}', '(', ')', '[', ']', '|' ); | |
while ( strpos( $pattern, '**' ) !== false ) { | |
$pattern = str_replace( '**', '*', $pattern ); | |
} | |
foreach ( $escape as $probe ) { | |
$pattern = str_replace( $probe, "\\$probe", $pattern ); | |
} | |
$pattern = str_replace( | |
'?*', | |
'*', | |
str_replace( | |
'*?', | |
'*', | |
str_replace( | |
'*', | |
'.*', | |
str_replace( '?', '.{1,1}', $pattern ) | |
) | |
) | |
); | |
$out[] = $pattern; | |
} | |
if ( count( $out ) == 1 ) { | |
return( $this->glob_regexp( "^$out[0]$", $string ) ); | |
} else { | |
foreach ( $out as $tester ) { | |
if ( $this->my_regexp( "^$tester$", $string ) ) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* Glob Regex. | |
* | |
* @access public | |
* @param mixed $pattern Pattern. | |
* @param mixed $probe Probe. | |
*/ | |
public function glob_regexp( $pattern, $probe ) { | |
$sensitive = ( PHP_OS != 'WIN32' ); | |
return ( $sensitive ? | |
preg_match( '/' . preg_quote( $pattern, '/' ) . '/', $probe ) : | |
preg_match( '/' . preg_quote( $pattern, '/' ) . '/i', $probe ) | |
); | |
} | |
/** | |
* Directory List. | |
* | |
* @access public | |
* @param mixed $remote Remote. | |
*/ | |
public function dirlist( $remote ) { | |
$list = $this->rawlist( $remote, '-la' ); | |
if ( false === $list ) { | |
$this->PushError( 'dirlist', "can't read remote folder list", "Can't read remote folder \"" . $remote . '" contents' ); | |
return false; | |
} | |
$dirlist = array(); | |
foreach ( $list as $k => $v ) { | |
$entry = $this->parselisting( $v ); | |
if ( empty( $entry ) ) { | |
continue; | |
} | |
if ( '.' == $entry['name'] || '..' == $entry['name'] ) { | |
continue; | |
} | |
$dirlist[ $entry['name'] ] = $entry; | |
} | |
return $dirlist; | |
} | |
/** | |
* _checkCode. | |
* | |
* @access private | |
*/ | |
private function _checkCode() { | |
return ( $this->_code < 400 && $this->_code > 0 ); | |
} | |
/** | |
* _list. | |
* | |
* @access private | |
* @param string $arg (default: '') Arguments. | |
* @param string $cmd (default: 'LIST') CMD. | |
* @param string $fnction (default: '_list') FNCTION. | |
*/ | |
private function _list( $arg = '', $cmd = 'LIST', $fnction = '_list' ) { | |
if ( ! $this->_data_prepare() ) { | |
return false; | |
} | |
if ( ! $this->_exec( $cmd . $arg, $fnction ) ) { | |
$this->_data_close(); | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
$this->_data_close(); | |
return false; | |
} | |
$out = ''; | |
if ( $this->_code < 200 ) { | |
$out = $this->_data_read(); | |
$this->_data_close(); | |
if ( ! $this->_readmsg() ) { | |
return false; | |
} | |
if ( ! $this->_checkCode() ) { | |
return false; | |
} | |
if ( false === $out ) { | |
return false; | |
} | |
$out = preg_split( '/[' . CRLF . ']+/', $out, -1, PREG_SPLIT_NO_EMPTY ); | |
} | |
return $out; | |
} | |
/** | |
* Generates an error for external treatment class. | |
* | |
* @access public | |
* @param mixed $fctname FCTNAME. | |
* @param mixed $msg Message. | |
* @param bool $desc (default: false) Description. | |
*/ | |
public function PushError( $fctname, $msg, $desc = false ) { | |
$error = array(); | |
$error['time'] = time(); | |
$error['fctname'] = $fctname; | |
$error['msg'] = $msg; | |
$error['desc'] = $desc; | |
if ( $desc ) { | |
$tmp = ' (' . $desc . ')'; | |
} else { | |
$tmp = ''; | |
} | |
$this->SendMSG( $fctname . ': ' . $msg . $tmp ); | |
return( array_push( $this->_error_array, $error ) ); | |
} | |
/** | |
* Recover from External Error. | |
* | |
* @access public | |
*/ | |
public function PopError() { | |
if ( count( $this->_error_array ) ) { | |
return( array_pop( $this->_error_array ) ); | |
} else { | |
return( false ); | |
} | |
} | |
} | |
$mod_sockets = extension_loaded( 'sockets' ); | |
if ( ! $mod_sockets && function_exists( 'dl' ) && is_callable( 'dl' ) ) { | |
$prefix = ( 'dll' == PHP_SHLIB_SUFFIX ) ? 'php_' : ''; | |
@dl( $prefix . 'sockets.' . PHP_SHLIB_SUFFIX ); | |
$mod_sockets = extension_loaded( 'sockets' ); | |
} | |
require_once dirname( __FILE__ ) . '/class-ftp-' . ( $mod_sockets ? 'sockets' : 'pure' ) . '.php'; | |
if ( $mod_sockets ) { | |
/** | |
* FTP Class. | |
* | |
* @extends ftp_sockets | |
*/ | |
class ftp extends ftp_sockets {} | |
} else { | |
/** | |
* FTP Class. | |
* | |
* @extends ftp_pure | |
*/ | |
class ftp extends ftp_pure {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment