,
// "if the owner or group is specified as -1, then that ID is not changed"
$attr = pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1);
return $this->_setstat($filename, $attr, $recursive);
}
/**
* Changes file or directory group
*
* Returns true on success or false on error.
*
* @param string $filename
* @param int $gid
* @param bool $recursive
* @return bool
* @access public
*/
function chgrp($filename, $gid, $recursive = false)
{
$attr = pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid);
return $this->_setstat($filename, $attr, $recursive);
}
/**
* Set permissions on a file.
*
* Returns the new file permissions on success or false on error.
* If $recursive is true than this just returns true or false.
*
* @param int $mode
* @param string $filename
* @param bool $recursive
* @return mixed
* @access public
*/
function chmod($mode, $filename, $recursive = false)
{
if (is_string($mode) && is_int($filename)) {
$temp = $mode;
$mode = $filename;
$filename = $temp;
}
$attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777);
if (!$this->_setstat($filename, $attr, $recursive)) {
return false;
}
if ($recursive) {
return true;
}
$filename = $this->realpath($filename);
// rather than return what the permissions *should* be, we'll return what they actually are. this will also
// tell us if the file actually exists.
// incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following:
$packet = pack('Na*', strlen($filename), $filename);
if (!$this->_send_sftp_packet(NET_SFTP_STAT, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_ATTRS:
$attrs = $this->_parseAttributes($response);
return $attrs['permissions'];
case NET_SFTP_STATUS:
$this->_logError($response);
return false;
}
user_error('Expected SSH_FXP_ATTRS or SSH_FXP_STATUS');
return false;
}
/**
* Sets information about a file
*
* @param string $filename
* @param string $attr
* @param bool $recursive
* @return bool
* @access private
*/
function _setstat($filename, $attr, $recursive)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$filename = $this->_realpath($filename);
if ($filename === false) {
return false;
}
$this->_remove_from_stat_cache($filename);
if ($recursive) {
$i = 0;
$result = $this->_setstat_recursive($filename, $attr, $i);
$this->_read_put_responses($i);
return $result;
}
// SFTPv4+ has an additional byte field - type - that would need to be sent, as well. setting it to
// SSH_FILEXFER_TYPE_UNKNOWN might work. if not, we'd have to do an SSH_FXP_STAT before doing an SSH_FXP_SETSTAT.
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($filename), $filename, $attr))) {
return false;
}
/*
"Because some systems must use separate system calls to set various attributes, it is possible that a failure
response will be returned, but yet some of the attributes may be have been successfully modified. If possible,
servers SHOULD avoid this situation; however, clients MUST be aware that this is possible."
-- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6
*/
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
return false;
}
return true;
}
/**
* Recursively sets information on directories on the SFTP server
*
* Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
*
* @param string $path
* @param string $attr
* @param int $i
* @return bool
* @access private
*/
function _setstat_recursive($path, $attr, &$i)
{
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
$entries = $this->_list($path, true);
if ($entries === false) {
return $this->_setstat($path, $attr, false);
}
// normally $entries would have at least . and .. but it might not if the directories
// permissions didn't allow reading
if (empty($entries)) {
return false;
}
unset($entries['.'], $entries['..']);
foreach ($entries as $filename => $props) {
if (!isset($props['type'])) {
return false;
}
$temp = $path . '/' . $filename;
if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
if (!$this->_setstat_recursive($temp, $attr, $i)) {
return false;
}
} else {
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($temp), $temp, $attr))) {
return false;
}
$i++;
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
}
}
}
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($path), $path, $attr))) {
return false;
}
$i++;
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
}
return true;
}
/**
* Return the target of a symbolic link
*
* @param string $link
* @return mixed
* @access public
*/
function readlink($link)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$link = $this->_realpath($link);
if (!$this->_send_sftp_packet(NET_SFTP_READLINK, pack('Na*', strlen($link), $link))) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_NAME:
break;
case NET_SFTP_STATUS:
$this->_logError($response);
return false;
default:
user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Ncount', $this->_string_shift($response, 4)));
// the file isn't a symlink
if (!$count) {
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nlength', $this->_string_shift($response, 4)));
return $this->_string_shift($response, $length);
}
/**
* Create a symlink
*
* symlink() creates a symbolic link to the existing target with the specified name link.
*
* @param string $target
* @param string $link
* @return bool
* @access public
*/
function symlink($target, $link)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
//$target = $this->_realpath($target);
$link = $this->_realpath($link);
$packet = pack('Na*Na*', strlen($target), $target, strlen($link), $link);
if (!$this->_send_sftp_packet(NET_SFTP_SYMLINK, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
return false;
}
return true;
}
/**
* Creates a directory.
*
* @param string $dir
* @return bool
* @access public
*/
function mkdir($dir, $mode = -1, $recursive = false)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$dir = $this->_realpath($dir);
if ($recursive) {
$dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir));
if (empty($dirs[0])) {
array_shift($dirs);
$dirs[0] = '/' . $dirs[0];
}
for ($i = 0; $i < count($dirs); $i++) {
$temp = array_slice($dirs, 0, $i + 1);
$temp = implode('/', $temp);
$result = $this->_mkdir_helper($temp, $mode);
}
return $result;
}
return $this->_mkdir_helper($dir, $mode);
}
/**
* Helper function for directory creation
*
* @param string $dir
* @return bool
* @access private
*/
function _mkdir_helper($dir, $mode)
{
// send SSH_FXP_MKDIR without any attributes (that's what the \0\0\0\0 is doing)
if (!$this->_send_sftp_packet(NET_SFTP_MKDIR, pack('Na*a*', strlen($dir), $dir, "\0\0\0\0"))) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
return false;
}
if ($mode !== -1) {
$this->chmod($mode, $dir);
}
return true;
}
/**
* Removes a directory.
*
* @param string $dir
* @return bool
* @access public
*/
function rmdir($dir)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$dir = $this->_realpath($dir);
if ($dir === false) {
return false;
}
if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($dir), $dir))) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
// presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED?
$this->_logError($response, $status);
return false;
}
$this->_remove_from_stat_cache($dir);
// the following will do a soft delete, which would be useful if you deleted a file
// and then tried to do a stat on the deleted file. the above, in contrast, does
// a hard delete
//$this->_update_stat_cache($dir, false);
return true;
}
/**
* Uploads a file to the SFTP server.
*
* By default, \phpseclib\Net\SFTP::put() does not read from the local filesystem. $data is dumped directly into $remote_file.
* So, for example, if you set $data to 'filename.ext' and then do \phpseclib\Net\SFTP::get(), you will get a file, twelve bytes
* long, containing 'filename.ext' as its contents.
*
* Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior. With self::SOURCE_LOCAL_FILE, $remote_file will
* contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how
* large $remote_file will be, as well.
*
* Setting $mode to self::SOURCE_CALLBACK will use $data as callback function, which gets only one parameter -- number of bytes to return, and returns a string if there is some data or null if there is no more data
*
* If $data is a resource then it'll be used as a resource instead.
*
* Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take
* care of that, yourself.
*
* $mode can take an additional two parameters - self::RESUME and self::RESUME_START. These are bitwise AND'd with
* $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following:
*
* self::SOURCE_LOCAL_FILE | self::RESUME
*
* If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace
* self::RESUME with self::RESUME_START.
*
* If $mode & (self::RESUME | self::RESUME_START) then self::RESUME_START will be assumed.
*
* $start and $local_start give you more fine grained control over this process and take precident over self::RESUME
* when they're non-negative. ie. $start could let you write at the end of a file (like self::RESUME) or in the middle
* of one. $local_start could let you start your reading from the end of a file (like self::RESUME_START) or in the
* middle of one.
*
* Setting $local_start to > 0 or $mode | self::RESUME_START doesn't do anything unless $mode | self::SOURCE_LOCAL_FILE.
*
* @param string $remote_file
* @param string|resource $data
* @param int $mode
* @param int $start
* @param int $local_start
* @param callable|null $progressCallback
* @return bool
* @access public
* @internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib\Net\SFTP::setMode().
*/
function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$remote_file = $this->_realpath($remote_file);
if ($remote_file === false) {
return false;
}
$this->_remove_from_stat_cache($remote_file);
$flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE;
// according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file."
// in practice, it doesn't seem to do that.
//$flags|= ($mode & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE;
if ($start >= 0) {
$offset = $start;
} elseif ($mode & self::RESUME) {
// if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called
$size = $this->size($remote_file);
$offset = $size !== false ? $size : 0;
} else {
$offset = 0;
$flags|= NET_SFTP_OPEN_TRUNCATE;
}
$packet = pack('Na*N2', strlen($remote_file), $remote_file, $flags, 0);
if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_HANDLE:
$handle = substr($response, 4);
break;
case NET_SFTP_STATUS:
$this->_logError($response);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3
$dataCallback = false;
switch (true) {
case $mode & self::SOURCE_CALLBACK:
if (!is_callable($data)) {
user_error("\$data should be is_callable() if you specify SOURCE_CALLBACK flag");
}
$dataCallback = $data;
// do nothing
break;
case is_resource($data):
$mode = $mode & ~self::SOURCE_LOCAL_FILE;
$info = stream_get_meta_data($data);
if ($info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') {
$fp = fopen('php://memory', 'w+');
stream_copy_to_stream($data, $fp);
rewind($fp);
} else {
$fp = $data;
}
break;
case $mode & self::SOURCE_LOCAL_FILE:
if (!is_file($data)) {
user_error("$data is not a valid file");
return false;
}
$fp = @fopen($data, 'rb');
if (!$fp) {
return false;
}
}
if (isset($fp)) {
$stat = fstat($fp);
$size = !empty($stat) ? $stat['size'] : 0;
if ($local_start >= 0) {
fseek($fp, $local_start);
$size-= $local_start;
}
} elseif ($dataCallback) {
$size = 0;
} else {
$size = strlen($data);
}
$sent = 0;
$size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
$sftp_packet_size = 4096; // PuTTY uses 4096
// make the SFTP packet be exactly 4096 bytes by including the bytes in the NET_SFTP_WRITE packets "header"
$sftp_packet_size-= strlen($handle) + 25;
$i = $j = 0;
while ($dataCallback || ($size === 0 || $sent < $size)) {
if ($dataCallback) {
$temp = call_user_func($dataCallback, $sftp_packet_size);
if (is_null($temp)) {
break;
}
} else {
$temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size);
if ($temp === false || $temp === '') {
break;
}
}
$subtemp = $offset + $sent;
$packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp);
if (!$this->_send_sftp_packet(NET_SFTP_WRITE, $packet, $j)) {
if ($mode & self::SOURCE_LOCAL_FILE) {
fclose($fp);
}
return false;
}
$sent+= strlen($temp);
if (is_callable($progressCallback)) {
call_user_func($progressCallback, $sent);
}
$i++;
$j++;
if ($i == NET_SFTP_UPLOAD_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
$i = 0;
break;
}
$i = 0;
}
}
if (!$this->_read_put_responses($i)) {
if ($mode & self::SOURCE_LOCAL_FILE) {
fclose($fp);
}
$this->_close_handle($handle);
return false;
}
if ($mode & self::SOURCE_LOCAL_FILE) {
fclose($fp);
}
return $this->_close_handle($handle);
}
/**
* Reads multiple successive SSH_FXP_WRITE responses
*
* Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i
* SSH_FXP_WRITEs, in succession, and then reading $i responses.
*
* @param int $i
* @return bool
* @access private
*/
function _read_put_responses($i)
{
while ($i--) {
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
break;
}
}
return $i < 0;
}
/**
* Close handle
*
* @param string $handle
* @return bool
* @access private
*/
function _close_handle($handle)
{
if (!$this->_send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle))) {
return false;
}
// "The client MUST release all resources associated with the handle regardless of the status."
// -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
return false;
}
return true;
}
/**
* Downloads a file from the SFTP server.
*
* Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if
* the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the
* operation.
*
* $offset and $length can be used to download files in chunks.
*
* @param string $remote_file
* @param string $local_file
* @param int $offset
* @param int $length
* @param callable|null $progressCallback
* @return mixed
* @access public
*/
function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$remote_file = $this->_realpath($remote_file);
if ($remote_file === false) {
return false;
}
$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_HANDLE:
$handle = substr($response, 4);
break;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
$this->_logError($response);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
if (is_resource($local_file)) {
$fp = $local_file;
$stat = fstat($fp);
$res_offset = $stat['size'];
} else {
$res_offset = 0;
if ($local_file !== false) {
$fp = fopen($local_file, 'wb');
if (!$fp) {
return false;
}
} else {
$content = '';
}
}
$fclose_check = $local_file !== false && !is_resource($local_file);
$start = $offset;
$read = 0;
while (true) {
$i = 0;
while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) {
$tempoffset = $start + $read;
$packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet;
$packet = pack('Na*N3', strlen($handle), $handle, $tempoffset / 4294967296, $tempoffset, $packet_size);
if (!$this->_send_sftp_packet(NET_SFTP_READ, $packet, $i)) {
if ($fclose_check) {
fclose($fp);
}
return false;
}
$packet = null;
$read+= $packet_size;
if (is_callable($progressCallback)) {
call_user_func($progressCallback, $read);
}
$i++;
}
if (!$i) {
break;
}
$packets_sent = $i - 1;
$clear_responses = false;
while ($i > 0) {
$i--;
if ($clear_responses) {
$this->_get_sftp_packet($packets_sent - $i);
continue;
} else {
$response = $this->_get_sftp_packet($packets_sent - $i);
}
switch ($this->packet_type) {
case NET_SFTP_DATA:
$temp = substr($response, 4);
$offset+= strlen($temp);
if ($local_file === false) {
$content.= $temp;
} else {
fputs($fp, $temp);
}
$temp = null;
break;
case NET_SFTP_STATUS:
// could, in theory, return false if !strlen($content) but we'll hold off for the time being
$this->_logError($response);
$clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses
break;
default:
if ($fclose_check) {
fclose($fp);
}
user_error('Expected SSH_FX_DATA or SSH_FXP_STATUS');
}
$response = null;
}
if ($clear_responses) {
break;
}
}
if ($length > 0 && $length <= $offset - $start) {
if ($local_file === false) {
$content = substr($content, 0, $length);
} else {
ftruncate($fp, $length + $res_offset);
}
}
if ($fclose_check) {
fclose($fp);
}
if (!$this->_close_handle($handle)) {
return false;
}
// if $content isn't set that means a file was written to
return isset($content) ? $content : true;
}
/**
* Deletes a file on the SFTP server.
*
* @param string $path
* @param bool $recursive
* @return bool
* @access public
*/
function delete($path, $recursive = true)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
if (is_object($path)) {
// It's an object. Cast it as string before we check anything else.
$path = (string) $path;
}
if (!is_string($path) || $path == '') {
return false;
}
$path = $this->_realpath($path);
if ($path === false) {
return false;
}
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path))) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
// if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
if (!$recursive) {
return false;
}
$i = 0;
$result = $this->_delete_recursive($path, $i);
$this->_read_put_responses($i);
return $result;
}
$this->_remove_from_stat_cache($path);
return true;
}
/**
* Recursively deletes directories on the SFTP server
*
* Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
*
* @param string $path
* @param int $i
* @return bool
* @access private
*/
function _delete_recursive($path, &$i)
{
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
$entries = $this->_list($path, true);
// normally $entries would have at least . and .. but it might not if the directories
// permissions didn't allow reading
if (empty($entries)) {
return false;
}
unset($entries['.'], $entries['..']);
foreach ($entries as $filename => $props) {
if (!isset($props['type'])) {
return false;
}
$temp = $path . '/' . $filename;
if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
if (!$this->_delete_recursive($temp, $i)) {
return false;
}
} else {
if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($temp), $temp))) {
return false;
}
$this->_remove_from_stat_cache($temp);
$i++;
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
}
}
}
if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($path), $path))) {
return false;
}
$this->_remove_from_stat_cache($path);
$i++;
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
return false;
}
$i = 0;
}
return true;
}
/**
* Checks whether a file or directory exists
*
* @param string $path
* @return bool
* @access public
*/
function file_exists($path)
{
if ($this->use_stat_cache) {
$path = $this->_realpath($path);
$result = $this->_query_stat_cache($path);
if (isset($result)) {
// return true if $result is an array or if it's an stdClass object
return $result !== false;
}
}
return $this->stat($path) !== false;
}
/**
* Tells whether the filename is a directory
*
* @param string $path
* @return bool
* @access public
*/
function is_dir($path)
{
$result = $this->_get_stat_cache_prop($path, 'type');
if ($result === false) {
return false;
}
return $result === NET_SFTP_TYPE_DIRECTORY;
}
/**
* Tells whether the filename is a regular file
*
* @param string $path
* @return bool
* @access public
*/
function is_file($path)
{
$result = $this->_get_stat_cache_prop($path, 'type');
if ($result === false) {
return false;
}
return $result === NET_SFTP_TYPE_REGULAR;
}
/**
* Tells whether the filename is a symbolic link
*
* @param string $path
* @return bool
* @access public
*/
function is_link($path)
{
$result = $this->_get_lstat_cache_prop($path, 'type');
if ($result === false) {
return false;
}
return $result === NET_SFTP_TYPE_SYMLINK;
}
/**
* Tells whether a file exists and is readable
*
* @param string $path
* @return bool
* @access public
*/
function is_readable($path)
{
$path = $this->_realpath($path);
$packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_READ, 0);
if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_HANDLE:
return true;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
}
/**
* Tells whether the filename is writable
*
* @param string $path
* @return bool
* @access public
*/
function is_writable($path)
{
$path = $this->_realpath($path);
$packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_WRITE, 0);
if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_HANDLE:
return true;
case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
return false;
}
}
/**
* Tells whether the filename is writeable
*
* Alias of is_writable
*
* @param string $path
* @return bool
* @access public
*/
function is_writeable($path)
{
return $this->is_writable($path);
}
/**
* Gets last access time of file
*
* @param string $path
* @return mixed
* @access public
*/
function fileatime($path)
{
return $this->_get_stat_cache_prop($path, 'atime');
}
/**
* Gets file modification time
*
* @param string $path
* @return mixed
* @access public
*/
function filemtime($path)
{
return $this->_get_stat_cache_prop($path, 'mtime');
}
/**
* Gets file permissions
*
* @param string $path
* @return mixed
* @access public
*/
function fileperms($path)
{
return $this->_get_stat_cache_prop($path, 'permissions');
}
/**
* Gets file owner
*
* @param string $path
* @return mixed
* @access public
*/
function fileowner($path)
{
return $this->_get_stat_cache_prop($path, 'uid');
}
/**
* Gets file group
*
* @param string $path
* @return mixed
* @access public
*/
function filegroup($path)
{
return $this->_get_stat_cache_prop($path, 'gid');
}
/**
* Gets file size
*
* @param string $path
* @return mixed
* @access public
*/
function filesize($path)
{
return $this->_get_stat_cache_prop($path, 'size');
}
/**
* Gets file type
*
* @param string $path
* @return mixed
* @access public
*/
function filetype($path)
{
$type = $this->_get_stat_cache_prop($path, 'type');
if ($type === false) {
return false;
}
switch ($type) {
case NET_SFTP_TYPE_BLOCK_DEVICE:
return 'block';
case NET_SFTP_TYPE_CHAR_DEVICE:
return 'char';
case NET_SFTP_TYPE_DIRECTORY:
return 'dir';
case NET_SFTP_TYPE_FIFO:
return 'fifo';
case NET_SFTP_TYPE_REGULAR:
return 'file';
case NET_SFTP_TYPE_SYMLINK:
return 'link';
default:
return false;
}
}
/**
* Return a stat properity
*
* Uses cache if appropriate.
*
* @param string $path
* @param string $prop
* @return mixed
* @access private
*/
function _get_stat_cache_prop($path, $prop)
{
return $this->_get_xstat_cache_prop($path, $prop, 'stat');
}
/**
* Return an lstat properity
*
* Uses cache if appropriate.
*
* @param string $path
* @param string $prop
* @return mixed
* @access private
*/
function _get_lstat_cache_prop($path, $prop)
{
return $this->_get_xstat_cache_prop($path, $prop, 'lstat');
}
/**
* Return a stat or lstat properity
*
* Uses cache if appropriate.
*
* @param string $path
* @param string $prop
* @return mixed
* @access private
*/
function _get_xstat_cache_prop($path, $prop, $type)
{
if ($this->use_stat_cache) {
$path = $this->_realpath($path);
$result = $this->_query_stat_cache($path);
if (is_object($result) && isset($result->$type)) {
return $result->{$type}[$prop];
}
}
$result = $this->$type($path);
if ($result === false || !isset($result[$prop])) {
return false;
}
return $result[$prop];
}
/**
* Renames a file or a directory on the SFTP server
*
* @param string $oldname
* @param string $newname
* @return bool
* @access public
*/
function rename($oldname, $newname)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
return false;
}
$oldname = $this->_realpath($oldname);
$newname = $this->_realpath($newname);
if ($oldname === false || $newname === false) {
return false;
}
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
$packet = pack('Na*Na*', strlen($oldname), $oldname, strlen($newname), $newname);
if (!$this->_send_sftp_packet(NET_SFTP_RENAME, $packet)) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
return false;
}
// if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
if (strlen($response) < 4) {
return false;
}
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
return false;
}
// don't move the stat cache entry over since this operation could very well change the
// atime and mtime attributes
//$this->_update_stat_cache($newname, $this->_query_stat_cache($oldname));
$this->_remove_from_stat_cache($oldname);
$this->_remove_from_stat_cache($newname);
return true;
}
/**
* Parse Attributes
*
* See '7. File Attributes' of draft-ietf-secsh-filexfer-13 for more info.
*
* @param string $response
* @return array
* @access private
*/
function _parseAttributes(&$response)
{
$attr = array();
if (strlen($response) < 4) {
user_error('Malformed file attributes');
return array();
}
extract(unpack('Nflags', $this->_string_shift($response, 4)));
// SFTPv4+ have a type field (a byte) that follows the above flag field
foreach ($this->attributes as $key => $value) {
switch ($flags & $key) {
case NET_SFTP_ATTR_SIZE: // 0x00000001
// The size attribute is defined as an unsigned 64-bit integer.
// The following will use floats on 32-bit platforms, if necessary.
// As can be seen in the BigInteger class, floats are generally
// IEEE 754 binary64 "double precision" on such platforms and
// as such can represent integers of at least 2^50 without loss
// of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB.
$attr['size'] = hexdec(bin2hex($this->_string_shift($response, 8)));
break;
case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only)
if (strlen($response) < 8) {
user_error('Malformed file attributes');
return $attr;
}
$attr+= unpack('Nuid/Ngid', $this->_string_shift($response, 8));
break;
case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004
if (strlen($response) < 4) {
user_error('Malformed file attributes');
return $attr;
}
$attr+= unpack('Npermissions', $this->_string_shift($response, 4));
// mode == permissions; permissions was the original array key and is retained for bc purposes.
// mode was added because that's the more industry standard terminology
$attr+= array('mode' => $attr['permissions']);
$fileType = $this->_parseMode($attr['permissions']);
if ($fileType !== false) {
$attr+= array('type' => $fileType);
}
break;
case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008
if (strlen($response) < 8) {
user_error('Malformed file attributes');
return $attr;
}
$attr+= unpack('Natime/Nmtime', $this->_string_shift($response, 8));
break;
case NET_SFTP_ATTR_EXTENDED: // 0x80000000
if (strlen($response) < 4) {
user_error('Malformed file attributes');
return $attr;
}
extract(unpack('Ncount', $this->_string_shift($response, 4)));
for ($i = 0; $i < $count; $i++) {
if (strlen($response) < 4) {
user_error('Malformed file attributes');
return $attr;
}
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$key = $this->_string_shift($response, $length);
if (strlen($response) < 4) {
user_error('Malformed file attributes');
return $attr;
}
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$attr[$key] = $this->_string_shift($response, $length);
}
}
}
return $attr;
}
/**
* Attempt to identify the file type
*
* Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway
*
* @param int $mode
* @return int
* @access private
*/
function _parseMode($mode)
{
// values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12
// see, also, http://linux.die.net/man/2/stat
switch ($mode & 0170000) {// ie. 1111 0000 0000 0000
case 0000000: // no file type specified - figure out the file type using alternative means
return false;
case 0040000:
return NET_SFTP_TYPE_DIRECTORY;
case 0100000:
return NET_SFTP_TYPE_REGULAR;
case 0120000:
return NET_SFTP_TYPE_SYMLINK;
// new types introduced in SFTPv5+
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
case 0010000: // named pipe (fifo)
return NET_SFTP_TYPE_FIFO;
case 0020000: // character special
return NET_SFTP_TYPE_CHAR_DEVICE;
case 0060000: // block special
return NET_SFTP_TYPE_BLOCK_DEVICE;
case 0140000: // socket
return NET_SFTP_TYPE_SOCKET;
case 0160000: // whiteout
// "SPECIAL should be used for files that are of
// a known type which cannot be expressed in the protocol"
return NET_SFTP_TYPE_SPECIAL;
default:
return NET_SFTP_TYPE_UNKNOWN;
}
}
/**
* Parse Longname
*
* SFTPv3 doesn't provide any easy way of identifying a file type. You could try to open
* a file as a directory and see if an error is returned or you could try to parse the
* SFTPv3-specific longname field of the SSH_FXP_NAME packet. That's what this function does.
* The result is returned using the
* {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}.
*
* If the longname is in an unrecognized format bool(false) is returned.
*
* @param string $longname
* @return mixed
* @access private
*/
function _parseLongname($longname)
{
// http://en.wikipedia.org/wiki/Unix_file_types
// http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions
if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) {
switch ($longname[0]) {
case '-':
return NET_SFTP_TYPE_REGULAR;
case 'd':
return NET_SFTP_TYPE_DIRECTORY;
case 'l':
return NET_SFTP_TYPE_SYMLINK;
default:
return NET_SFTP_TYPE_SPECIAL;
}
}
return false;
}
/**
* Sends SFTP Packets
*
* See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
*
* @param int $type
* @param string $data
* @see self::_get_sftp_packet()
* @see self::_send_channel_packet()
* @return bool
* @access private
*/
function _send_sftp_packet($type, $data, $request_id = 1)
{
$packet = $this->use_request_id ?
pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) :
pack('NCa*', strlen($data) + 1, $type, $data);
$start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
$result = $this->_send_channel_packet(self::CHANNEL, $packet);
$stop = strtok(microtime(), ' ') + strtok('');
if (defined('NET_SFTP_LOGGING')) {
$packet_type = '-> ' . $this->packet_types[$type] .
' (' . round($stop - $start, 4) . 's)';
if (NET_SFTP_LOGGING == self::LOG_REALTIME) {
echo "\r\n" . $this->_format_log(array($data), array($packet_type)) . "\r\n
\r\n";
flush();
ob_flush();
} else {
$this->packet_type_log[] = $packet_type;
if (NET_SFTP_LOGGING == self::LOG_COMPLEX) {
$this->packet_log[] = $data;
}
}
}
return $result;
}
/**
* Resets a connection for re-use
*
* @param int $reason
* @access private
*/
function _reset_connection($reason)
{
parent::_reset_connection($reason);
$this->use_request_id = false;
$this->pwd = false;
$this->requestBuffer = array();
}
/**
* Receives SFTP Packets
*
* See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
*
* Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present.
* There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA
* messages containing one SFTP packet.
*
* @see self::_send_sftp_packet()
* @return string
* @access private
*/
function _get_sftp_packet($request_id = null)
{
if (isset($request_id) && isset($this->requestBuffer[$request_id])) {
$this->packet_type = $this->requestBuffer[$request_id]['packet_type'];
$temp = $this->requestBuffer[$request_id]['packet'];
unset($this->requestBuffer[$request_id]);
return $temp;
}
// in SSH2.php the timeout is cumulative per function call. eg. exec() will
// timeout after 10s. but for SFTP.php it's cumulative per packet
$this->curTimeout = $this->timeout;
$start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
// SFTP packet length
while (strlen($this->packet_buffer) < 4) {
$temp = $this->_get_channel_packet(self::CHANNEL, true);
if (is_bool($temp)) {
$this->packet_type = false;
$this->packet_buffer = '';
return false;
}
$this->packet_buffer.= $temp;
}
if (strlen($this->packet_buffer) < 4) {
return false;
}
extract(unpack('Nlength', $this->_string_shift($this->packet_buffer, 4)));
$tempLength = $length;
$tempLength-= strlen($this->packet_buffer);
// 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h
if ($tempLength > 256 * 1024) {
user_error('Invalid SFTP packet size');
return false;
}
// SFTP packet type and data payload
while ($tempLength > 0) {
$temp = $this->_get_channel_packet(self::CHANNEL, true);
if (is_bool($temp)) {
$this->packet_type = false;
$this->packet_buffer = '';
return false;
}
$this->packet_buffer.= $temp;
$tempLength-= strlen($temp);
}
$stop = strtok(microtime(), ' ') + strtok('');
$this->packet_type = ord($this->_string_shift($this->packet_buffer));
if ($this->use_request_id) {
extract(unpack('Npacket_id', $this->_string_shift($this->packet_buffer, 4))); // remove the request id
$length-= 5; // account for the request id and the packet type
} else {
$length-= 1; // account for the packet type
}
$packet = $this->_string_shift($this->packet_buffer, $length);
if (defined('NET_SFTP_LOGGING')) {
$packet_type = '<- ' . $this->packet_types[$this->packet_type] .
' (' . round($stop - $start, 4) . 's)';
if (NET_SFTP_LOGGING == self::LOG_REALTIME) {
echo "\r\n" . $this->_format_log(array($packet), array($packet_type)) . "\r\n
\r\n";
flush();
ob_flush();
} else {
$this->packet_type_log[] = $packet_type;
if (NET_SFTP_LOGGING == self::LOG_COMPLEX) {
$this->packet_log[] = $packet;
}
}
}
if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) {
$this->requestBuffer[$packet_id] = array(
'packet_type' => $this->packet_type,
'packet' => $packet
);
return $this->_get_sftp_packet($request_id);
}
return $packet;
}
/**
* Returns a log of the packets that have been sent and received.
*
* Returns a string if NET_SFTP_LOGGING == NET_SFTP_LOG_COMPLEX, an array if NET_SFTP_LOGGING == NET_SFTP_LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING')
*
* @access public
* @return string or Array
*/
function getSFTPLog()
{
if (!defined('NET_SFTP_LOGGING')) {
return false;
}
switch (NET_SFTP_LOGGING) {
case self::LOG_COMPLEX:
return $this->_format_log($this->packet_log, $this->packet_type_log);
break;
//case self::LOG_SIMPLE:
default:
return $this->packet_type_log;
}
}
/**
* Returns all errors
*
* @return array
* @access public
*/
function getSFTPErrors()
{
return $this->sftp_errors;
}
/**
* Returns the last error
*
* @return string
* @access public
*/
function getLastSFTPError()
{
return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : '';
}
/**
* Get supported SFTP versions
*
* @return array
* @access public
*/
function getSupportedVersions()
{
$temp = array('version' => $this->version);
if (isset($this->extensions['versions'])) {
$temp['extensions'] = $this->extensions['versions'];
}
return $temp;
}
/**
* Disconnect
*
* @param int $reason
* @return bool
* @access private
*/
function _disconnect($reason)
{
$this->pwd = false;
parent::_disconnect($reason);
}
}