mirror of
https://codeberg.org/icewind/SMB.git
synced 2026-06-04 01:34:07 +02:00
move backends into their own namespace and add support for multiple auth methods
This commit is contained in:
parent
2280570d28
commit
29bdebad42
33 changed files with 752 additions and 377 deletions
117
src/Wrapped/Connection.php
Normal file
117
src/Wrapped/Connection.php
Normal 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);
|
||||
}
|
||||
}
|
||||
31
src/Wrapped/ErrorCodes.php
Normal file
31
src/Wrapped/ErrorCodes.php
Normal 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
115
src/Wrapped/FileInfo.php
Normal 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);
|
||||
}
|
||||
}
|
||||
110
src/Wrapped/NotifyHandler.php
Normal file
110
src/Wrapped/NotifyHandler.php
Normal 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
173
src/Wrapped/Parser.php
Normal 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;
|
||||
}
|
||||
}
|
||||
198
src/Wrapped/RawConnection.php
Normal file
198
src/Wrapped/RawConnection.php
Normal 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
70
src/Wrapped/Server.php
Normal 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
443
src/Wrapped/Share.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue