Files
server/usr/share/psa-pear/pear/php/Horde/Vfs/Smb.php
2026-01-07 20:52:11 +01:00

724 lines
23 KiB
PHP

<?php
/**
* Codebase copyright 2002 Paul Gareau <paul@xhawk.net>. Adapted with
* permission by Patrice Levesque <wayne@ptaff.ca> from phpsmb-0.8 code, and
* converted to the LGPL. Please do not taunt original author, contact
* Patrice Levesque or dev@lists.horde.org.
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Paul Gareau <paul@xhawk.net>
* @author Patrice Levesque <wayne@ptaff.ca>
* @package Vfs
* @todo Add driver for smbclient extension https://github.com/eduardok/libsmbclient-php
*/
/**
* Stateless VFS implementation for a SMB server, based on smbclient.
*
* Required values for $params:
* - username: (string) The username with which to connect to the SMB server.
* - password: (string) The password with which to connect to the SMB server.
* - hostspec: (string) The SMB server to connect to.
* - share: (string) The share to access on the SMB server. Any trailing
* paths will removed from the share and prepended to each path
* in further requests. Example: a share of 'myshare/basedir' and
* a request to 'dir/subdir' will result in a request to
* 'basedir/dir/subdir' on myshare.
* - smbclient: (string) The path to the 'smbclient' executable.
*
* Optional values for $params:
* - port: (integer) The SMB port number to connect to.
* - ipaddress: (string) The address of the server to connect to.
*
* Functions not implemented:
* - changePermissions(): The SMB permission style does not fit with the
* module.
*
* All paths need to use forward slashes!
*
* @author Paul Gareau <paul@xhawk.net>
* @author Patrice Levesque <wayne@ptaff.ca>
* @package Vfs
*/
class Horde_Vfs_Smb extends Horde_Vfs_Base
{
/**
* List of additional credentials required for this VFS backend.
*
* @var array
*/
protected $_credentials = array('username', 'password');
/**
* Prefix to use for every path.
*
* Passed as a path suffix to the share parameter.
*
* @var string
*/
protected $_prefix = '';
/**
* Has the vfsroot already been created?
*
* @var boolean
*/
protected $_rootCreated = false;
/**
* Constructor.
*
* @param array $params A hash containing connection parameters.
*/
public function __construct($params = array())
{
parent::__construct($params);
if (!isset($this->_params['share'])) {
return;
}
$share_parts = explode('/', $this->_params['share']);
$this->_params['share'] = array_shift($share_parts);
if ($share_parts) {
$this->_prefix = implode('/', $share_parts);
}
}
/**
* Retrieves the size of a file from the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return integer The file size.
* @throws Horde_Vfs_Exception
*/
public function size($path, $name)
{
$file = $this->readFile($path, $name);
return filesize($file);
}
/**
* Retrieves a file from the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return string The file data.
*/
public function read($path, $name)
{
$file = $this->readFile($path, $name);
$size = filesize($file);
return ($size === 0)
? ''
: file_get_contents($file);
}
/**
* Retrieves a file from the VFS as an on-disk local file.
*
* This function provides a file on local disk with the data of a VFS file
* in it. This file <em>cannot</em> be modified! The behavior if you do
* modify it is undefined. It will be removed at the end of the request.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return string A local filename.
*/
public function readFile($path, $name)
{
// Create a temporary file and register it for deletion at the
// end of this request.
if (!($localFile = Horde_Util::getTempFile('vfs'))) {
throw new Horde_Vfs_Exception('Unable to create temporary file.');
}
$this->_createRoot();
list($npath, $name) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
$cmd = array('get \"' . $name . '\" ' . $localFile);
$this->_command($npath, $cmd);
if (!file_exists($localFile)) {
throw new Horde_Vfs_Exception(sprintf('Unable to open VFS file "%s".', $this->_getPath($path, $name)));
}
clearstatcache();
return $localFile;
}
/**
* Open a stream to a file in the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return resource The stream.
*/
public function readStream($path, $name)
{
return fopen($this->readFile($path, $name),
substr(PHP_OS, 0, 3) == 'WIN' ? 'rb' : 'r');
}
/**
* Stores a file in the VFS.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string $tmpFile The temporary file containing the data to be
* stored.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function write($path, $name, $tmpFile, $autocreate = false)
{
$this->_createRoot();
// Double quotes not allowed in SMB filename.
$name = str_replace('"', "'", $name);
list($npath, $name) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
$cmd = array('put \"' . $tmpFile . '\" \"' . $name . '\"');
// do we need to first autocreate the directory?
if ($autocreate) {
$this->autocreatePath($path);
}
$this->_command($npath, $cmd);
}
/**
* Stores a file in the VFS from raw data.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string|resource $data The data as a string or stream resource.
* Resources allowed @since 2.4.0
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function writeData($path, $name, $data, $autocreate = false)
{
$tmpFile = Horde_Util::getTempFile('vfs');
if (is_resource($data)) {
rewind($data);
}
file_put_contents($tmpFile, $data);
try {
$this->write($path, $name, $tmpFile, $autocreate);
unlink($tmpFile);
} catch (Horde_Vfs_Exception $e) {
unlink($tmpFile);
throw $e;
}
}
/**
* Deletes a file from the VFS.
*
* @param string $path The path to delete the file from.
* @param string $name The filename to use.
*
* @throws Horde_Vfs_Exception
*/
public function deleteFile($path, $name)
{
$this->_createRoot();
list($path, $name) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
$cmd = array('del \"' . $name . '\"');
$this->_command($path, $cmd);
}
/**
* Checks if a given pathname is a folder.
*
* @param string $path The path to the folder.
* @param string $name The file or folder name.
*
* @return boolean True if it is a folder, false otherwise.
*/
public function isFolder($path, $name)
{
$this->_createRoot();
list($path, $name) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
try {
$this->_command($this->_getPath($path, $name), array('quit'));
return true;
} catch (Horde_Vfs_Exception $e) {
return false;
}
}
/**
* Deletes a folder from the VFS.
*
* @param string $path The path to delete the folder from.
* @param string $name The name of the folder to delete.
* @param boolean $recursive Force a recursive delete?
*
* @throws Horde_Vfs_Exception
*/
public function deleteFolder($path, $name, $recursive = false)
{
$this->_createRoot();
if (!$this->isFolder($path, $name)) {
throw new Horde_Vfs_Exception(sprintf('"%s" is not a directory.', $path . '/' . $name));
}
$file_list = $this->listFolder($this->_getPath($path, $name));
if ($file_list && !$recursive) {
throw new Horde_Vfs_Exception(sprintf('Unable to delete "%s", the directory is not empty.', $this->_getPath($path, $name)));
}
foreach ($file_list as $file) {
if ($file['type'] == '**dir') {
$this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
} else {
$this->deleteFile($this->_getPath($path, $name), $file['name']);
}
}
// Really delete the folder.
list($npath, $name) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
$cmd = array('rmdir \"' . $name . '\"');
try {
$this->_command($npath, $cmd);
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception(sprintf('Unable to delete VFS folder "%s".', $this->_getPath($path, $name)));
}
}
/**
* Renames a file in the VFS.
*
* @param string $oldpath The old path to the file.
* @param string $oldname The old filename.
* @param string $newpath The new path of the file.
* @param string $newname The new filename.
*
* @throws Horde_Vfs_Exception
*/
public function rename($oldpath, $oldname, $newpath, $newname)
{
$this->_createRoot();
$this->autocreatePath($newpath);
// Double quotes not allowed in SMB filename. The '/' character should
// also be removed from the beginning/end of the names.
$oldname = str_replace('"', "'", trim($oldname, '/'));
$newname = str_replace('"', "'", trim($newname, '/'));
if (empty($oldname)) {
throw new Horde_Vfs_Exception('Unable to rename VFS file to same name.');
}
/* If the path was not empty (i.e. the path is not the root path),
* then add the trailing '/' character to path. */
if (!empty($oldpath)) {
$oldpath .= '/';
}
if (!empty($newpath)) {
$newpath .= '/';
}
list($file, $name) = $this->_escapeShellCommand($oldname, $newname);
$cmd = array(
'rename \"'
. str_replace('/', '\\\\', $this->_getNativePath($oldpath))
. $file . '\" \"'
. str_replace('/', '\\\\', $this->_getNativePath($newpath))
. $name . '\"'
);
try {
$this->_command('', $cmd);
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception(sprintf('Unable to rename VFS file "%s".', $this->_getPath($oldpath, $oldname)));
}
}
/**
* Creates a folder on the VFS.
*
* @param string $path The path of directory to create folder.
* @param string $name The name of the new folder.
*
* @throws Horde_Vfs_Exception
*/
public function createFolder($path, $name)
{
$this->_createRoot();
// Double quotes not allowed in SMB filename.
$name = str_replace('"', "'", $name);
list($dir, $mkdir) = $this->_escapeShellCommand($this->_getNativePath($path), $name);
$cmd = array('mkdir \"' . $mkdir . '\"');
try {
$this->_command($dir, $cmd);
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception(sprintf('Unable to create VFS folder "%s".', $this->_getPath($path, $name)));
}
}
/**
* Returns a file list of the directory passed in.
*
* @param string $path The path of the directory.
* @param string|array $filter Regular expression(s) to filter
* file/directory name on.
* @param boolean $dotfiles Show dotfiles?
* @param boolean $dironly Show only directories?
* @param boolean $recursive Return all directory levels recursively?
*
* @return array File list.
* @throws Horde_Vfs_Exception
*/
public function listFolder($path = '', $filter = null, $dotfiles = true,
$dironly = false, $recursive = false)
{
$this->_createRoot();
list($path) = $this->_escapeShellCommand($this->_getNativePath($path));
return $this->parseListing($this->_command($path, array('ls')),
$filter,
$dotfiles,
$dironly);
}
/**
*/
public function parseListing($res, $filter, $dotfiles, $dironly)
{
$num_lines = count($res);
$files = array();
for ($r = 0; $r < $num_lines; $r++) {
// Match file listing.
if (!preg_match('/^ (.+?) +([A-Z]*) +(\d+) (\w\w\w \w\w\w [ \d]\d \d\d:\d\d:\d\d \d\d\d\d)$/', $res[$r], $match)) {
continue;
}
// If the file name isn't . or ..
if ($match[1] == '.' || $match[1] == '..') {
continue;
}
$my_name = $match[1];
// Filter out dotfiles if they aren't wanted.
if (!$dotfiles && substr($my_name, 0, 1) == '.') {
continue;
}
$my_size = $match[3];
$ext_name = explode('.', $my_name);
if ((strpos($match[2], 'D') !== false)) {
$my_type = '**dir';
$my_size = -1;
} else {
$my_type = Horde_String::lower($ext_name[count($ext_name) - 1]);
}
$my_date = strtotime($match[4]);
$filedata = array('owner' => '',
'group' => '',
'perms' => '',
'name' => $my_name,
'type' => $my_type,
'date' => $my_date,
'size' => $my_size);
// watch for filters and dironly
if ($this->_filterMatch($filter, $my_name)) {
unset($file);
continue;
}
if ($dironly && $my_type !== '**dir') {
unset($file);
continue;
}
$files[$filedata['name']] = $filedata;
}
return $files;
}
/**
* Copies a file through the backend.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string $dest The destination of the file.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function copy($path, $name, $dest, $autocreate = false)
{
$this->_checkDestination($path, $dest);
if ($autocreate) {
$this->autocreatePath($dest);
}
foreach ($this->listFolder($dest, null, true) as $file) {
if ($file['name'] == $name) {
throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
}
}
if ($this->isFolder($path, $name)) {
$this->_copyRecursive($path, $name, $dest);
} else {
try {
$this->write($dest, $name, $this->readFile($path, $name));
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception(sprintf('Copy failed: %s', $this->_getPath($dest, $name)));
}
}
}
/**
* Moves a file through the backend.
*
* @param string $path The path to store the file in.
* @param string $name The filename to use.
* @param string $dest The destination of the file.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function move($path, $name, $dest, $autocreate = false)
{
$orig = $this->_getPath($path, $name);
if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
throw new Horde_Vfs_Exception('Cannot copy file(s) - destination is within source.');
}
if ($autocreate) {
$this->autocreatePath($dest);
}
foreach ($this->listFolder($dest, null, true) as $file) {
if ($file['name'] == $name) {
throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
}
}
try {
$this->rename($path, $name, $dest, $name);
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception(sprintf('Failed to move to "%s".', $this->_getPath($dest, $name)));
}
}
/**
* Returns the full path of a directory.
*
* @param string $path The directory.
*
* @return string Full path to the directory.
*/
protected function _getNativePath($path)
{
$parts = array($path);
if (strlen($this->_prefix)) {
array_unshift($parts, $this->_prefix);
}
if (isset($this->_params['vfsroot']) &&
strlen($this->_params['vfsroot'])) {
array_unshift($parts, $this->_params['vfsroot']);
}
$path = implode('/', $parts);
// In some samba versions after samba-3.0.25-pre2, $path must
// end in a trailing slash.
if (substr($path, -1) != '/') {
$path .= '/';
}
return $path;
}
/**
* Replacement for escapeshellcmd(), variable length args, as we only want
* certain characters escaped.
*
* @param array $array Strings to escape.
*
* @return array TODO
*/
protected function _escapeShellCommand()
{
$ret = array();
$args = func_get_args();
foreach ($args as $arg) {
$ret[] = str_replace(array(';', '\\'), array('\;', '\\\\'), $arg);
}
return $ret;
}
/**
* Executes a command and returns output lines in array.
*
* @param string $cmd Command to be executed.
*
* @return array Array on success.
* @throws Horde_Vfs_Exception
*/
protected function _execute($cmd)
{
$cmd = str_replace('"-U%"', '-N', $cmd);
$proc = proc_open(
$cmd,
array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')),
$pipes);
if (!is_resource($proc)) {
// This should never happen.
throw new Horde_Vfs_Exception('Failed to call proc_open().');
}
$out = explode("\n", trim(stream_get_contents($pipes[1])));
$error = explode("\n", trim(stream_get_contents($pipes[2])));
$ret = proc_close($proc);
// In some cases, (like trying to delete a nonexistant file),
// smbclient will return success (at least on 2.2.7 version I'm
// testing on). So try to match error strings, even after success.
if ($ret != 0) {
$err = '';
foreach ($error as $line) {
if (strpos($line, 'Usage:') === 0) {
$err = 'Command syntax incorrect';
break;
}
if (strpos($line, 'ERRSRV') !== false ||
strpos($line, 'ERRDOS') !== false) {
$err = preg_replace('/.*\((.+)\).*/', '\\1', $line);
if (!$err) {
$err = $line;
}
break;
}
}
if (!$err) {
$err = $out ? $out[count($out) - 1] : $ret;
}
throw new Horde_Vfs_Exception($err);
}
// Check for errors even on success.
$err = '';
foreach ($out as $line) {
if (strpos($line, 'NT_STATUS_NO_SUCH_FILE') !== false ||
strpos($line, 'NT_STATUS_OBJECT_NAME_NOT_FOUND') !== false) {
$err = Horde_Vfs_Translation::t("No such file");
break;
} elseif (strpos($line, 'NT_STATUS_ACCESS_DENIED') !== false) {
$err = Horde_Vfs_Translation::t("Permission Denied");
break;
}
}
if ($err) {
throw new Horde_Vfs_Exception($err);
}
return $out;
}
/**
* Executes SMB commands - without authentication - and returns output
* lines in array.
*
* @param array $path Base path for command.
* @param array $cmd Commands to be executed.
*
* @return array Array on success.
* @throws Horde_Vfs_Exception
*/
protected function _command($path, $cmd)
{
list($share) = $this->_escapeShellCommand($this->_params['share']);
putenv('PASSWD=' . $this->_params['password']);
$port = isset($this->_params['port'])
? (' "-p' . $this->_params['port'] . '"')
: '';
$ipoption = isset($this->_params['ipaddress'])
? (' -I ' . $this->_params['ipaddress'])
: '';
$domain = isset($this->_params['domain'])
? (' -W ' . $this->_params['domain'])
: '';
$fullcmd = $this->_params['smbclient'] .
' "//' . $this->_params['hostspec'] . '/' . $share . '"' .
$port .
' "-U' . $this->_params['username'] . '"' .
' -D "' . $path . '"' .
$ipoption .
$domain .
' -c "';
foreach ($cmd as $c) {
$fullcmd .= $c . ";";
}
$fullcmd .= '"';
return $this->_execute($fullcmd);
}
/**
* Authenticates a user on the SMB server and share.
*
* @throws Horde_Vfs_Exception
*/
protected function _connect()
{
try {
$this->_command('', array('quit'));
} catch (Horde_Vfs_Exception $e) {
throw new Horde_Vfs_Exception('Authentication to the SMB server failed.');
}
}
/**
* Creates the vfsroot.
*/
protected function _createRoot()
{
if ($this->_rootCreated) {
return;
}
$root = trim($this->_params['vfsroot'] . '/' . $this->_prefix, '/');
$path = '';
foreach (explode('/', $root) as $dir) {
try {
$this->_command($path . '/' . $dir . '/', array());
} catch (Horde_Vfs_Exception $e) {
try {
$this->_command('/' . $path . '/', array('mkdir \"' . $dir . '\"'));
} catch (Horde_Vfs_Exception $e) {
echo $e;
throw new Horde_Vfs_Exception(sprintf('Unable to create VFS root directory "%s".', $this->_params['vfsroot']));
}
}
$path .= '/' . $dir;
}
$this->_rootCreated = true;
}
}