SFTPv4+ defines a 'newline' extension. SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com',
however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's
not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for
one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that
'newline@vandyke.com' would.
if (isset($this->extensions['newline@vandyke.com'])) {
$this->extensions['newline'] = $this->extensions['newline@vandyke.com'];
unset($this->extensions['newline@vandyke.com']);
A Note on SFTPv4/5/6 support:
<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.1> states the following:
"If the client wishes to interoperate with servers that support noncontiguous version
numbers it SHOULD send '3'"
Given that the server only sends its version number after the client has already done so, the above
seems to be suggesting that v3 should be the default version. This makes sense given that v3 is the
<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.5> states the following;
"If the server did not send the "versions" extension, or the version-from-list was not included, the
server MAY send a status response describing the failure, but MUST then close the channel without
processing any further requests."
So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and
a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4? If it only implements
v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed
in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what Net_SFTP would do is close the
channel and reopen it with a new and updated SSH_FXP_INIT packet.
switch ($this->version) {
$this->pwd = $this->_realpath('.');
$this->_update_stat_cache($this->pwd, array());
public function disableStatCache()
$this->use_stat_cache = false;
public function enableStatCache()
$this->use_stat_cache = true;
public function clearStatCache()
$this->stat_cache = array();
* Returns the current directory name
* @param String $response
* @param optional Integer $status
public function _logError($response, $status = -1)
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
$error = $this->status_codes[$status];
if ($this->version > 2) {
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$this->sftp_errors[] = $error.': '.$this->_string_shift($response, $length);
$this->sftp_errors[] = $error;
* Canonicalize the Server-Side Path Name
* SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it. Returns
* the absolute (canonicalized) path.
public function _realpath($path)
if ($this->pwd === false) {
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
if (!$this->_send_sftp_packet(NET_SFTP_REALPATH, pack('Na*', strlen($path), $path))) {
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
// although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following
// should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks
// at is the first part and that part is defined the same in SFTP versions 3 through 6.
$this->_string_shift($response, 4); // skip over the count - it should be 1, anyway
extract(unpack('Nlength', $this->_string_shift($response, 4)));
return $this->_string_shift($response, $length);
$this->_logError($response);
user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS');
$path = $this->pwd.'/'.$path;
$path = explode('/', $path);
foreach ($path as $dir) {
return '/'.implode('/', $new);
* Changes the current directory
public function chdir($dir)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
// assume current dir if $dir is empty
// suffix a slash if needed
} elseif ($dir[strlen($dir) - 1] != '/') {
$dir = $this->_realpath($dir);
// confirm that $dir is, in fact, a valid directory
if ($this->use_stat_cache && is_array($this->_query_stat_cache($dir))) {
// we could do a stat on the alleged $dir to see if it's a directory but that doesn't tell us
// the currently logged in user has the appropriate permissions or not. maybe you could see if
// the file's uid / gid match the currently logged in user's uid / gid but how there's no easy
// way to get those with SFTP
if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) {
// see Net_SFTP::nlist() for a more thorough explanation of the following
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
$handle = substr($response, 4);
$this->_logError($response);
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
if (!$this->_close_handle($handle)) {
$this->_update_stat_cache($dir, array());
* Returns a list of files in the given directory
* @param optional String $dir
* @param optional Boolean $recursive
public function nlist($dir = '.', $recursive = false)
return $this->_nlist_helper($dir, $recursive, '');
* Helper method for nlist
* @param Boolean $recursive
* @param String $relativeDir
public function _nlist_helper($dir, $recursive, $relativeDir)
$files = $this->_list($dir, false);
foreach ($files as $value) {
if ($value == '.' || $value == '..') {
if ($relativeDir == '') {
if (is_array($this->_query_stat_cache($this->_realpath($dir.'/'.$value)))) {
$temp = $this->_nlist_helper($dir.'/'.$value, true, $relativeDir.$value.'/');
$result = array_merge($result, $temp);
$result[] = $relativeDir.$value;
* Returns a detailed list of files in the given directory
* @param optional String $dir
* @param optional Boolean $recursive
public function rawlist($dir = '.', $recursive = false)
$files = $this->_list($dir, true);
if (!$recursive || $files === false) {
foreach ($files as $key => $value) {
if ($depth != 0 && $key == '..') {
if ($key != '.' && $key != '..' && is_array($this->_query_stat_cache($this->_realpath($dir.'/'.$key)))) {
$files[$key] = $this->rawlist($dir.'/'.$key, true);
$files[$key] = (object) $value;
* Reads a list, be it detailed or not, of files in the given directory
* @param optional Boolean $raw
public function _list($dir, $raw = true)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$dir = $this->_realpath($dir.'/');
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.2
if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) {
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.2
// since 'handle' is the last field in the SSH_FXP_HANDLE packet, we'll just remove the first four bytes that
// represent the length of the string and leave it at that
$handle = substr($response, 4);
// presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
$this->_logError($response);
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
$this->_update_stat_cache($dir, array());
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.2
// why multiple SSH_FXP_READDIR packets would be sent when the response to a single one can span arbitrarily many
// SSH_MSG_CHANNEL_DATA messages is not known to me.
if (!$this->_send_sftp_packet(NET_SFTP_READDIR, pack('Na*', strlen($handle), $handle))) {
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
extract(unpack('Ncount', $this->_string_shift($response, 4)));
for ($i = 0; $i < $count; $i++) {
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$shortname = $this->_string_shift($response, $length);
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$longname = $this->_string_shift($response, $length);
$attributes = $this->_parseAttributes($response);
if (!isset($attributes['type'])) {
$fileType = $this->_parseLongname($longname);
$attributes['type'] = $fileType;
$contents[$shortname] = $attributes + array('filename' => $shortname);
if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) {
$this->_update_stat_cache($dir.'/'.$shortname, array());
if ($shortname == '..') {
$temp = $this->_realpath($dir.'/..').'/.';
$temp = $dir.'/'.$shortname;
$this->_update_stat_cache($temp, (object) $attributes);
// SFTPv6 has an optional boolean end-of-list field, but we'll ignore that, since the
// final SSH_FXP_STATUS packet should tell us that, already.
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_EOF) {
$this->_logError($response, $status);
user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS');
if (!$this->_close_handle($handle)) {
if (count($this->sortOptions)) {
uasort($contents, array(&$this, '_comparator'));
return $raw ? $contents : array_keys($contents);
* Compares two rawlist entries using parameters set by setListOrder()
* Intended for use with uasort()
public function _comparator($a, $b)
case $a['filename'] === '.' || $b['filename'] === '.':
if ($a['filename'] === $b['filename']) {
return $a['filename'] === '.' ? -1 : 1;
case $a['filename'] === '..' || $b['filename'] === '..':
if ($a['filename'] === $b['filename']) {
return $a['filename'] === '..' ? -1 : 1;
case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY:
if (!isset($b['type'])) {
if ($b['type'] !== $a['type']) {
case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY:
foreach ($this->sortOptions as $sort => $order) {
if (!isset($a[$sort]) || !isset($b[$sort])) {