move backends into their own namespace and add support for multiple auth methods

This commit is contained in:
Robin Appelman 2018-03-13 16:18:37 +01:00
commit 29bdebad42
33 changed files with 752 additions and 377 deletions

117
src/Wrapped/Connection.php Normal file
View file

@ -0,0 +1,117 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\NoLoginServerException;
class Connection extends RawConnection {
const DELIMITER = 'smb:';
const DELIMITER_LENGTH = 4;
/** @var Parser */
private $parser;
public function __construct($command, Parser $parser, $env = array()) {
parent::__construct($command, $env);
$this->parser = $parser;
}
/**
* send input to smbclient
*
* @param string $input
*/
public function write($input) {
parent::write($input . PHP_EOL);
}
public function clearTillPrompt() {
$this->write('');
do {
$promptLine = $this->readLine();
} while (!$this->isPrompt($promptLine));
$this->write('');
$this->readLine();
}
/**
* get all unprocessed output from smbclient until the next prompt
*
* @param callable $callback (optional) callback to call for every line read
* @return string[]
* @throws AuthenticationException
* @throws ConnectException
* @throws ConnectionException
* @throws InvalidHostException
* @throws NoLoginServerException
*/
public function read(callable $callback = null) {
if (!$this->isValid()) {
throw new ConnectionException('Connection not valid');
}
$promptLine = $this->readLine(); //first line is prompt
$this->parser->checkConnectionError($promptLine);
$output = array();
$line = $this->readLine();
if ($line === false) {
$this->unknownError($promptLine);
}
while (!$this->isPrompt($line)) { //next prompt functions as delimiter
if (is_callable($callback)) {
$result = $callback($line);
if ($result === false) { // allow the callback to close the connection for infinite running commands
$this->close(true);
break;
}
} else {
$output[] .= $line;
}
$line = $this->readLine();
}
return $output;
}
/**
* Check
*
* @param $line
* @return bool
*/
private function isPrompt($line) {
return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER || $line === false;
}
/**
* @param string $promptLine (optional) prompt line that might contain some info about the error
* @throws ConnectException
*/
private function unknownError($promptLine = '') {
if ($promptLine) { //maybe we have some error we missed on the previous line
throw new ConnectException('Unknown error (' . $promptLine . ')');
} else {
$error = $this->readError(); // maybe something on stderr
if ($error) {
throw new ConnectException('Unknown error (' . $error . ')');
} else {
throw new ConnectException('Unknown error');
}
}
}
public function close($terminate = true) {
if (is_resource($this->getInputStream())) {
$this->write('close' . PHP_EOL);
}
parent::close($terminate);
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
class ErrorCodes {
/**
* connection errors
*/
const LogonFailure = 'NT_STATUS_LOGON_FAILURE';
const BadHostName = 'NT_STATUS_BAD_NETWORK_NAME';
const Unsuccessful = 'NT_STATUS_UNSUCCESSFUL';
const ConnectionRefused = 'NT_STATUS_CONNECTION_REFUSED';
const NoLogonServers = 'NT_STATUS_NO_LOGON_SERVERS';
const PathNotFound = 'NT_STATUS_OBJECT_PATH_NOT_FOUND';
const NoSuchFile = 'NT_STATUS_NO_SUCH_FILE';
const ObjectNotFound = 'NT_STATUS_OBJECT_NAME_NOT_FOUND';
const NameCollision = 'NT_STATUS_OBJECT_NAME_COLLISION';
const AccessDenied = 'NT_STATUS_ACCESS_DENIED';
const DirectoryNotEmpty = 'NT_STATUS_DIRECTORY_NOT_EMPTY';
const FileIsADirectory = 'NT_STATUS_FILE_IS_A_DIRECTORY';
const NotADirectory = 'NT_STATUS_NOT_A_DIRECTORY';
const SharingViolation = 'NT_STATUS_SHARING_VIOLATION';
const InvalidParameter = 'NT_STATUS_INVALID_PARAMETER';
const RevisionMismatch = 'NT_STATUS_REVISION_MISMATCH';
}

115
src/Wrapped/FileInfo.php Normal file
View file

@ -0,0 +1,115 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\IFileInfo;
class FileInfo implements IFileInfo {
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $name;
/**
* @var int
*/
protected $size;
/**
* @var int
*/
protected $time;
/**
* @var int
*/
protected $mode;
/**
* @param string $path
* @param string $name
* @param int $size
* @param int $time
* @param int $mode
*/
public function __construct($path, $name, $size, $time, $mode) {
$this->path = $path;
$this->name = $name;
$this->size = $size;
$this->time = $time;
$this->mode = $mode;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @return int
*/
public function getSize() {
return $this->size;
}
/**
* @return int
*/
public function getMTime() {
return $this->time;
}
/**
* @return bool
*/
public function isDirectory() {
return (bool)($this->mode & IFileInfo::MODE_DIRECTORY);
}
/**
* @return bool
*/
public function isReadOnly() {
return (bool)($this->mode & IFileInfo::MODE_READONLY);
}
/**
* @return bool
*/
public function isHidden() {
return (bool)($this->mode & IFileInfo::MODE_HIDDEN);
}
/**
* @return bool
*/
public function isSystem() {
return (bool)($this->mode & IFileInfo::MODE_SYSTEM);
}
/**
* @return bool
*/
public function isArchived() {
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE);
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Change;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\RevisionMismatchException;
use Icewind\SMB\INotifyHandler;
class NotifyHandler implements INotifyHandler {
/**
* @var Connection
*/
private $connection;
/**
* @var string
*/
private $path;
private $listening = true;
// see error.h
const EXCEPTION_MAP = [
ErrorCodes::RevisionMismatch => RevisionMismatchException::class,
];
/**
* @param Connection $connection
* @param string $path
*/
public function __construct(Connection $connection, $path) {
$this->connection = $connection;
$this->path = $path;
}
/**
* Get all changes detected since the start of the notify process or the last call to getChanges
*
* @return Change[]
*/
public function getChanges() {
if (!$this->listening) {
return [];
}
stream_set_blocking($this->connection->getOutputStream(), 0);
$lines = [];
while (($line = $this->connection->readLine())) {
$this->checkForError($line);
$lines[] = $line;
}
stream_set_blocking($this->connection->getOutputStream(), 1);
return array_values(array_filter(array_map([$this, 'parseChangeLine'], $lines)));
}
/**
* Listen actively to all incoming changes
*
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated
*
* @param callable $callback
*/
public function listen($callback) {
if ($this->listening) {
$this->connection->read(function ($line) use ($callback) {
$this->checkForError($line);
$change = $this->parseChangeLine($line);
if ($change) {
return $callback($change);
}
});
}
}
private function parseChangeLine($line) {
$code = (int)substr($line, 0, 4);
if ($code === 0) {
return null;
}
$subPath = str_replace('\\', '/', substr($line, 5));
if ($this->path === '') {
return new Change($code, $subPath);
} else {
return new Change($code, $this->path . '/' . $subPath);
}
}
private function checkForError($line) {
if (substr($line, 0, 16) === 'notify returned ') {
$error = substr($line, 16);
throw Exception::fromMap(array_merge(self::EXCEPTION_MAP, Parser::EXCEPTION_MAP), $error, 'Notify is not supported with the used smb version');
}
}
public function stop() {
$this->listening = false;
$this->connection->close();
}
public function __destruct() {
$this->stop();
}
}

173
src/Wrapped/Parser.php Normal file
View file

@ -0,0 +1,173 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\AuthenticationException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidHostException;
use Icewind\SMB\Exception\InvalidParameterException;
use Icewind\SMB\Exception\InvalidResourceException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NoLoginServerException;
use Icewind\SMB\Exception\NotEmptyException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\TimeZoneProvider;
class Parser {
const MSG_NOT_FOUND = 'Error opening local file ';
/**
* @var \Icewind\SMB\TimeZoneProvider
*/
protected $timeZoneProvider;
// todo replace with static once <5.6 support is dropped
// see error.h
const EXCEPTION_MAP = [
ErrorCodes::LogonFailure => AuthenticationException::class,
ErrorCodes::PathNotFound => NotFoundException::class,
ErrorCodes::ObjectNotFound => NotFoundException::class,
ErrorCodes::NoSuchFile => NotFoundException::class,
ErrorCodes::NameCollision => AlreadyExistsException::class,
ErrorCodes::AccessDenied => AccessDeniedException::class,
ErrorCodes::DirectoryNotEmpty => NotEmptyException::class,
ErrorCodes::FileIsADirectory => InvalidTypeException::class,
ErrorCodes::NotADirectory => InvalidTypeException::class,
ErrorCodes::SharingViolation => FileInUseException::class,
ErrorCodes::InvalidParameter => InvalidParameterException::class
];
/**
* @param TimeZoneProvider $timeZoneProvider
*/
public function __construct(TimeZoneProvider $timeZoneProvider) {
$this->timeZoneProvider = $timeZoneProvider;
}
private function getErrorCode($line) {
$parts = explode(' ', $line);
foreach ($parts as $part) {
if (substr($part, 0, 9) === 'NT_STATUS') {
return $part;
}
}
return false;
}
public function checkForError($output, $path) {
if (strpos($output[0], 'does not exist')) {
throw new NotFoundException($path);
}
$error = $this->getErrorCode($output[0]);
if (substr($output[0], 0, strlen(self::MSG_NOT_FOUND)) === self::MSG_NOT_FOUND) {
$localPath = substr($output[0], strlen(self::MSG_NOT_FOUND));
throw new InvalidResourceException('Failed opening local file "' . $localPath . '" for writing');
}
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path);
}
/**
* check if the first line holds a connection failure
*
* @param $line
* @throws AuthenticationException
* @throws InvalidHostException
* @throws NoLoginServerException
*/
public function checkConnectionError($line) {
$line = rtrim($line, ')');
if (substr($line, -23) === ErrorCodes::LogonFailure) {
throw new AuthenticationException('Invalid login');
}
if (substr($line, -26) === ErrorCodes::BadHostName) {
throw new InvalidHostException('Invalid hostname');
}
if (substr($line, -22) === ErrorCodes::Unsuccessful) {
throw new InvalidHostException('Connection unsuccessful');
}
if (substr($line, -28) === ErrorCodes::ConnectionRefused) {
throw new InvalidHostException('Connection refused');
}
if (substr($line, -26) === ErrorCodes::NoLogonServers) {
throw new NoLoginServerException('No login server');
}
}
public function parseMode($mode) {
$result = 0;
$modeStrings = array(
'R' => FileInfo::MODE_READONLY,
'H' => FileInfo::MODE_HIDDEN,
'S' => FileInfo::MODE_SYSTEM,
'D' => FileInfo::MODE_DIRECTORY,
'A' => FileInfo::MODE_ARCHIVE,
'N' => FileInfo::MODE_NORMAL
);
foreach ($modeStrings as $char => $val) {
if (strpos($mode, $char) !== false) {
$result |= $val;
}
}
return $result;
}
public function parseStat($output) {
$data = [];
foreach ($output as $line) {
// A line = explode statement may not fill all array elements
// properly. May happen when accessing non Windows Fileservers
$words = explode(':', $line, 2);
$name = isset($words[0]) ? $words[0] : '';
$value = isset($words[1]) ? $words[1] : '';
$value = trim($value);
$data[$name] = $value;
}
return [
'mtime' => strtotime($data['write_time']),
'mode' => hexdec(substr($data['attributes'], strpos($data['attributes'], '('), -1)),
'size' => isset($data['stream']) ? intval(explode(' ', $data['stream'])[1]) : 0
];
}
public function parseDir($output, $basePath) {
//last line is used space
array_pop($output);
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
//2 spaces, filename, optional type, size, date
$content = array();
foreach ($output as $line) {
if (preg_match($regex, $line, $matches)) {
list(, $name, $mode, $size, $time) = $matches;
if ($name !== '.' and $name !== '..') {
$mode = $this->parseMode($mode);
$time = strtotime($time . ' ' . $this->timeZoneProvider->get());
$content[] = new FileInfo($basePath . '/' . $name, $name, $size, $time, $mode);
}
}
}
return $content;
}
public function parseListShares($output) {
$shareNames = array();
foreach ($output as $line) {
if (strpos($line, '|')) {
list($type, $name, $description) = explode('|', $line);
if (strtolower($type) === 'disk') {
$shareNames[$name] = $description;
}
}
}
return $shareNames;
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\ConnectionException;
class RawConnection {
/**
* @var string
*/
private $command;
/**
* @var string[]
*/
private $env;
/**
* @var resource[] $pipes
*
* $pipes[0] holds STDIN for smbclient
* $pipes[1] holds STDOUT for smbclient
* $pipes[3] holds the authfile for smbclient
* $pipes[4] holds the stream for writing files
* $pipes[5] holds the stream for reading files
*/
private $pipes;
/**
* @var resource $process
*/
private $process;
/**
* @var resource|null $authStream
*/
private $authStream = null;
private $connected = false;
public function __construct($command, array $env = []) {
$this->command = $command;
$this->env = $env;
}
public function connect() {
if (is_null($this->getAuthStream())) {
throw new ConnectException('Authentication not set before connecting');
}
$descriptorSpec = [
0 => ['pipe', 'r'], // child reads from stdin
1 => ['pipe', 'w'], // child writes to stdout
2 => ['pipe', 'w'], // child writes to stderr
3 => $this->getAuthStream(), // child reads from fd#3
4 => ['pipe', 'r'], // child reads from fd#4
5 => ['pipe', 'w'] // child writes to fd#5
];
setlocale(LC_ALL, Server::LOCALE);
$env = array_merge($this->env, [
'CLI_FORCE_INTERACTIVE' => 'y', // Needed or the prompt isn't displayed!!
'LC_ALL' => Server::LOCALE,
'LANG' => Server::LOCALE,
'COLUMNS' => 8192 // prevent smbclient from line-wrapping it's output
]);
$this->process = proc_open($this->command, $descriptorSpec, $this->pipes, '/', $env);
if (!$this->isValid()) {
throw new ConnectionException();
}
$this->connected = true;
}
/**
* check if the connection is still active
*
* @return bool
*/
public function isValid() {
if (is_resource($this->process)) {
$status = proc_get_status($this->process);
return $status['running'];
} else {
return false;
}
}
/**
* send input to the process
*
* @param string $input
*/
public function write($input) {
fwrite($this->getInputStream(), $input);
fflush($this->getInputStream());
}
/**
* read a line of output
*
* @return string|false
*/
public function readLine() {
return stream_get_line($this->getOutputStream(), 4086, "\n");
}
/**
* read a line of output
*
* @return string
*/
public function readError() {
return trim(stream_get_line($this->getErrorStream(), 4086));
}
/**
* get all output until the process closes
*
* @return array
*/
public function readAll() {
$output = array();
while ($line = $this->readLine()) {
$output[] = $line;
}
return $output;
}
public function getInputStream() {
return $this->pipes[0];
}
public function getOutputStream() {
return $this->pipes[1];
}
public function getErrorStream() {
return $this->pipes[2];
}
public function getAuthStream() {
return $this->authStream;
}
public function getFileInputStream() {
return $this->pipes[4];
}
public function getFileOutputStream() {
return $this->pipes[5];
}
public function writeAuthentication($user, $password) {
$auth = ($password === false)
? "username=$user"
: "username=$user\npassword=$password\n";
$this->authStream = fopen('php://temp', 'w+');
fwrite($this->getAuthStream(), $auth);
}
public function close($terminate = true) {
if (!is_resource($this->process)) {
return;
}
if ($terminate) {
// if for case that posix_ functions are not available
if (function_exists('posix_kill')) {
$status = proc_get_status($this->process);
$ppid = $status['pid'];
$pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid $ppid`);
foreach ($pids as $pid) {
if (is_numeric($pid)) {
//9 is the SIGKILL signal
posix_kill($pid, 9);
}
}
}
proc_terminate($this->process);
}
proc_close($this->process);
}
public function reconnect() {
$this->close();
$this->connect();
}
public function __destruct() {
$this->close();
}
}

70
src/Wrapped/Server.php Normal file
View file

@ -0,0 +1,70 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\AbstractServer;
use Icewind\SMB\System;
class Server extends AbstractServer {
/**
* Check if the smbclient php extension is available
*
* @return bool
*/
public static function available(System $system) {
return $system->getSmbclientPath();
}
private function getAuthFileArgument() {
if ($this->getAuth()->getUsername()) {
return '--authentication-file=' . System::getFD(3);
} else {
return '';
}
}
/**
* @return \Icewind\SMB\IShare[]
*
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
*/
public function listShares() {
$command = sprintf('%s %s %s -L %s',
$this->system->getSmbclientPath(),
$this->getAuthFileArgument(),
$this->getAuth()->getExtraCommandLineArguments(),
escapeshellarg('//' . $this->getHost())
);
$connection = new RawConnection($command);
$connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword());
$connection->connect();
$output = $connection->readAll();
$parser = new Parser($this->timezoneProvider);
if (isset($output[0])) {
$parser->checkConnectionError($output[0]);
}
$shareNames = $parser->parseListShares($output);
$shares = array();
foreach ($shareNames as $name => $description) {
$shares[] = $this->getShare($name);
}
return $shares;
}
/**
* @param string $name
* @return \Icewind\SMB\IShare
*/
public function getShare($name) {
return new Share($this, $name, $this->system);
}
}

443
src/Wrapped/Share.php Normal file
View file

@ -0,0 +1,443 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB\Wrapped;
use Icewind\SMB\AbstractShare;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\DependencyException;
use Icewind\SMB\Exception\FileInUseException;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\INotifyHandler;
use Icewind\SMB\IServer;
use Icewind\SMB\System;
use Icewind\SMB\TimeZoneProvider;
use Icewind\Streams\CallbackWrapper;
class Share extends AbstractShare {
/**
* @var IServer $server
*/
private $server;
/**
* @var string $name
*/
private $name;
/**
* @var Connection $connection
*/
public $connection;
/**
* @var Parser
*/
protected $parser;
/**
* @var System
*/
private $system;
/**
* @param IServer $server
* @param string $name
* @param System $system
*/
public function __construct(IServer $server, $name, System $system = null) {
parent::__construct();
$this->server = $server;
$this->name = $name;
$this->system = (!is_null($system)) ? $system : new System();
$this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system));
}
private function getAuthFileArgument() {
if ($this->server->getAuth()->getUsername()) {
return '--authentication-file=' . System::getFD(3);
} else {
return '';
}
}
protected function getConnection() {
$command = sprintf('%s%s %s %s %s',
$this->system->hasStdBuf() ? 'stdbuf -o0 ' : '',
$this->system->getSmbclientPath(),
$this->getAuthFileArgument(),
$this->server->getAuth()->getExtraCommandLineArguments(),
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
);
$connection = new Connection($command, $this->parser);
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
$connection->connect();
if (!$connection->isValid()) {
throw new ConnectionException($connection->readLine());
}
// some versions of smbclient add a help message in first of the first prompt
$connection->clearTillPrompt();
return $connection;
}
/**
* @throws \Icewind\SMB\Exception\ConnectionException
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
*/
protected function connect() {
if ($this->connection and $this->connection->isValid()) {
return;
}
$this->connection = $this->getConnection();
}
protected function reconnect() {
$this->connection->reconnect();
if (!$this->connection->isValid()) {
throw new ConnectionException();
}
}
/**
* Get the name of the share
*
* @return string
*/
public function getName() {
return $this->name;
}
protected function simpleCommand($command, $path) {
$escapedPath = $this->escapePath($path);
$cmd = $command . ' ' . $escapedPath;
$output = $this->execute($cmd);
return $this->parseOutput($output, $path);
}
/**
* List the content of a remote folder
*
* @param $path
* @return \Icewind\SMB\IFileInfo[]
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function dir($path) {
$escapedPath = $this->escapePath($path);
$output = $this->execute('cd ' . $escapedPath);
//check output for errors
$this->parseOutput($output, $path);
$output = $this->execute('dir');
$this->execute('cd /');
return $this->parser->parseDir($output, $path);
}
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo
*/
public function stat($path) {
$escapedPath = $this->escapePath($path);
$output = $this->execute('allinfo ' . $escapedPath);
// Windows and non Windows Fileserver may respond different
// to the allinfo command for directories. If the result is a single
// line = error line, redo it with a different allinfo parameter
if ($escapedPath == '""' && count($output) < 2) {
$output = $this->execute('allinfo ' . '"."');
}
if (count($output) < 3) {
$this->parseOutput($output, $path);
}
$stat = $this->parser->parseStat($output);
return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']);
}
/**
* Create a folder on the share
*
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
*/
public function mkdir($path) {
return $this->simpleCommand('mkdir', $path);
}
/**
* Remove a folder on the share
*
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function rmdir($path) {
return $this->simpleCommand('rmdir', $path);
}
/**
* Delete a file on the share
*
* @param string $path
* @param bool $secondTry
* @return bool
* @throws InvalidTypeException
* @throws NotFoundException
* @throws \Exception
*/
public function del($path, $secondTry = false) {
//del return a file not found error when trying to delete a folder
//we catch it so we can check if $path doesn't exist or is of invalid type
try {
return $this->simpleCommand('del', $path);
} catch (NotFoundException $e) {
//no need to do anything with the result, we just check if this throws the not found error
try {
$this->simpleCommand('ls', $path);
} catch (NotFoundException $e2) {
throw $e;
} catch (\Exception $e2) {
throw new InvalidTypeException($path);
}
throw $e;
} catch (FileInUseException $e) {
if ($secondTry) {
throw $e;
}
$this->reconnect();
return $this->del($path, true);
}
}
/**
* Rename a remote file
*
* @param string $from
* @param string $to
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
*/
public function rename($from, $to) {
$path1 = $this->escapePath($from);
$path2 = $this->escapePath($to);
$output = $this->execute('rename ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $to);
}
/**
* Upload a local file
*
* @param string $source local file
* @param string $target remove file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function put($source, $target) {
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
$path2 = $this->escapePath($target);
$output = $this->execute('put ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $target);
}
/**
* Download a remote file
*
* @param string $source remove file
* @param string $target local file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function get($source, $target) {
$path1 = $this->escapePath($source);
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
$output = $this->execute('get ' . $path1 . ' ' . $path2);
return $this->parseOutput($output, $source);
}
/**
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function read($source) {
$source = $this->escapePath($source);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
$connection = $this->getConnection();
$connection->write('get ' . $source . ' ' . System::getFD(5));
$connection->write('exit');
$fh = $connection->getFileOutputStream();
stream_context_set_option($fh, 'file', 'connection', $connection);
return $fh;
}
/**
* Open a writable stream to a remote file
*
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function write($target) {
$target = $this->escapePath($target);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
$connection = $this->getConnection();
$fh = $connection->getFileInputStream();
$connection->write('put ' . System::getFD(4) . ' ' . $target);
$connection->write('exit');
// use a close callback to ensure the upload is finished before continuing
// this also serves as a way to keep the connection in scope
return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
$connection->close(false); // dont terminate, give the upload some time
});
}
/**
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode($path, $mode) {
$modeString = '';
$modeMap = array(
FileInfo::MODE_READONLY => 'r',
FileInfo::MODE_HIDDEN => 'h',
FileInfo::MODE_ARCHIVE => 'a',
FileInfo::MODE_SYSTEM => 's'
);
foreach ($modeMap as $modeByte => $string) {
if ($mode & $modeByte) {
$modeString .= $string;
}
}
$path = $this->escapePath($path);
// first reset the mode to normal
$cmd = 'setmode ' . $path . ' -rsha';
$output = $this->execute($cmd);
$this->parseOutput($output, $path);
if ($mode !== FileInfo::MODE_NORMAL) {
// then set the modes we want
$cmd = 'setmode ' . $path . ' ' . $modeString;
$output = $this->execute($cmd);
return $this->parseOutput($output, $path);
} else {
return true;
}
}
/**
* @param string $path
* @return INotifyHandler
* @throws ConnectionException
* @throws DependencyException
*/
public function notify($path) {
if (!$this->system->hasStdBuf()) { //stdbuf is required to disable smbclient's output buffering
throw new DependencyException('stdbuf is required for usage of the notify command');
}
$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
$command = 'notify ' . $this->escapePath($path);
$connection->write($command . PHP_EOL);
return new NotifyHandler($connection, $path);
}
/**
* @param string $command
* @return array
*/
protected function execute($command) {
$this->connect();
$this->connection->write($command . PHP_EOL);
$output = $this->connection->read();
return $output;
}
/**
* check output for errors
*
* @param string[] $lines
* @param string $path
*
* @throws NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws \Icewind\SMB\Exception\AccessDeniedException
* @throws \Icewind\SMB\Exception\NotEmptyException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws \Icewind\SMB\Exception\Exception
* @return bool
*/
protected function parseOutput($lines, $path = '') {
if (count($lines) === 0) {
return true;
} else {
$this->parser->checkForError($lines, $path);
return false;
}
}
/**
* @param string $string
* @return string
*/
protected function escape($string) {
return escapeshellarg($string);
}
/**
* @param string $path
* @return string
*/
protected function escapePath($path) {
$this->verifyPath($path);
if ($path === '/') {
$path = '';
}
$path = str_replace('/', '\\', $path);
$path = str_replace('"', '^"', $path);
$path = ltrim($path, '\\');
return '"' . $path . '"';
}
/**
* @param string $path
* @return string
*/
protected function escapeLocalPath($path) {
$path = str_replace('"', '\"', $path);
return '"' . $path . '"';
}
public function __destruct() {
unset($this->connection);
}
}