Signed-off-by: Robin Appelman <robin@icewind.nl>tags/v22.0.0beta1
@@ -6,3 +6,6 @@ icewind/smb/Makefile | |||
icewind/smb/.travis.yml | |||
icewind/smb/.scrutinizer.yml | |||
icewind/streams/tests | |||
.github | |||
.php_cs* | |||
psalm.xml |
@@ -8,7 +8,7 @@ | |||
"classmap-authoritative": true | |||
}, | |||
"require": { | |||
"icewind/streams": "0.7.1", | |||
"icewind/smb": "3.2.7" | |||
"icewind/streams": "0.7.3", | |||
"icewind/smb": "3.4.0" | |||
} | |||
} |
@@ -4,29 +4,31 @@ | |||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | |||
"This file is @generated automatically" | |||
], | |||
"content-hash": "6181c23a5c03b00fbdc659d87c1ad67d", | |||
"content-hash": "9905ed45527f669a4165a8b83b6e4141", | |||
"packages": [ | |||
{ | |||
"name": "icewind/smb", | |||
"version": "v3.2.7", | |||
"version": "v3.4.0", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/SMB.git", | |||
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6" | |||
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/743a7bf35317f1b76cf8e8b804e54a6c5faacad6", | |||
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6", | |||
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/b5c6921f2e91229c9f71556a4713b4fac91fd394", | |||
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"icewind/streams": ">=0.2.0", | |||
"php": ">=7.1" | |||
"icewind/streams": ">=0.7.3", | |||
"php": ">=7.2" | |||
}, | |||
"require-dev": { | |||
"friendsofphp/php-cs-fixer": "^2.13", | |||
"phpunit/phpunit": "^7.0" | |||
"friendsofphp/php-cs-fixer": "^2.16", | |||
"phpstan/phpstan": "^0.12.57", | |||
"phpunit/phpunit": "^8.5|^9.3.8", | |||
"psalm/phar": "^4.3" | |||
}, | |||
"type": "library", | |||
"autoload": { | |||
@@ -45,33 +47,37 @@ | |||
} | |||
], | |||
"description": "php wrapper for smbclient and libsmbclient-php", | |||
"time": "2020-09-03T13:00:22+00:00" | |||
"support": { | |||
"issues": "https://github.com/icewind1991/SMB/issues", | |||
"source": "https://github.com/icewind1991/SMB/tree/v3.4.0" | |||
}, | |||
"time": "2021-03-10T14:00:37+00:00" | |||
}, | |||
{ | |||
"name": "icewind/streams", | |||
"version": "v0.7.1", | |||
"version": "v0.7.3", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/Streams.git", | |||
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121" | |||
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/4db3ed6c366e90b958d00e1d4c6360a9b39b2121", | |||
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121", | |||
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/22ef9fc5b50d645dbc202206a656cc4dde28f95c", | |||
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"php": ">=5.3" | |||
"php": ">=7.1" | |||
}, | |||
"require-dev": { | |||
"phpunit/phpunit": "^4.8", | |||
"satooshi/php-coveralls": "v1.0.0" | |||
"friendsofphp/php-cs-fixer": "^2", | |||
"phpstan/phpstan": "^0.12", | |||
"phpunit/phpunit": "^9" | |||
}, | |||
"type": "library", | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\Streams\\Tests\\": "tests/", | |||
"Icewind\\Streams\\": "src/" | |||
} | |||
}, | |||
@@ -86,7 +92,11 @@ | |||
} | |||
], | |||
"description": "A set of generic stream wrappers", | |||
"time": "2019-02-15T12:57:29+00:00" | |||
"support": { | |||
"issues": "https://github.com/icewind1991/Streams/issues", | |||
"source": "https://github.com/icewind1991/Streams/tree/v0.7.3" | |||
}, | |||
"time": "2021-03-02T19:33:35+00:00" | |||
} | |||
], | |||
"packages-dev": [], | |||
@@ -97,5 +107,5 @@ | |||
"prefer-lowest": false, | |||
"platform": [], | |||
"platform-dev": [], | |||
"plugin-api-version": "1.1.0" | |||
"plugin-api-version": "2.0.0" | |||
} |
@@ -37,11 +37,13 @@ namespace Composer\Autoload; | |||
* | |||
* @author Fabien Potencier <fabien@symfony.com> | |||
* @author Jordi Boggiano <j.boggiano@seld.be> | |||
* @see http://www.php-fig.org/psr/psr-0/ | |||
* @see http://www.php-fig.org/psr/psr-4/ | |||
* @see https://www.php-fig.org/psr/psr-0/ | |||
* @see https://www.php-fig.org/psr/psr-4/ | |||
*/ | |||
class ClassLoader | |||
{ | |||
private $vendorDir; | |||
// PSR-4 | |||
private $prefixLengthsPsr4 = array(); | |||
private $prefixDirsPsr4 = array(); | |||
@@ -57,10 +59,17 @@ class ClassLoader | |||
private $missingClasses = array(); | |||
private $apcuPrefix; | |||
private static $registeredLoaders = array(); | |||
public function __construct($vendorDir = null) | |||
{ | |||
$this->vendorDir = $vendorDir; | |||
} | |||
public function getPrefixes() | |||
{ | |||
if (!empty($this->prefixesPsr0)) { | |||
return call_user_func_array('array_merge', $this->prefixesPsr0); | |||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); | |||
} | |||
return array(); | |||
@@ -300,6 +309,17 @@ class ClassLoader | |||
public function register($prepend = false) | |||
{ | |||
spl_autoload_register(array($this, 'loadClass'), true, $prepend); | |||
if (null === $this->vendorDir) { | |||
return; | |||
} | |||
if ($prepend) { | |||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; | |||
} else { | |||
unset(self::$registeredLoaders[$this->vendorDir]); | |||
self::$registeredLoaders[$this->vendorDir] = $this; | |||
} | |||
} | |||
/** | |||
@@ -308,6 +328,10 @@ class ClassLoader | |||
public function unregister() | |||
{ | |||
spl_autoload_unregister(array($this, 'loadClass')); | |||
if (null !== $this->vendorDir) { | |||
unset(self::$registeredLoaders[$this->vendorDir]); | |||
} | |||
} | |||
/** | |||
@@ -367,6 +391,16 @@ class ClassLoader | |||
return $file; | |||
} | |||
/** | |||
* Returns the currently registered loaders indexed by their corresponding vendor directories. | |||
* | |||
* @return self[] | |||
*/ | |||
public static function getRegisteredLoaders() | |||
{ | |||
return self::$registeredLoaders; | |||
} | |||
private function findFileWithExtension($class, $ext) | |||
{ | |||
// PSR-4 lookup |
@@ -0,0 +1,301 @@ | |||
<?php | |||
namespace Composer; | |||
use Composer\Autoload\ClassLoader; | |||
use Composer\Semver\VersionParser; | |||
class InstalledVersions | |||
{ | |||
private static $installed = array ( | |||
'root' => | |||
array ( | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96', | |||
'name' => 'files_external/3rdparty', | |||
), | |||
'versions' => | |||
array ( | |||
'files_external/3rdparty' => | |||
array ( | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96', | |||
), | |||
'icewind/smb' => | |||
array ( | |||
'pretty_version' => 'v3.4.0', | |||
'version' => '3.4.0.0', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => 'b5c6921f2e91229c9f71556a4713b4fac91fd394', | |||
), | |||
'icewind/streams' => | |||
array ( | |||
'pretty_version' => 'v0.7.3', | |||
'version' => '0.7.3.0', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '22ef9fc5b50d645dbc202206a656cc4dde28f95c', | |||
), | |||
), | |||
); | |||
private static $canGetVendors; | |||
private static $installedByVendor = array(); | |||
public static function getInstalledPackages() | |||
{ | |||
$packages = array(); | |||
foreach (self::getInstalled() as $installed) { | |||
$packages[] = array_keys($installed['versions']); | |||
} | |||
if (1 === \count($packages)) { | |||
return $packages[0]; | |||
} | |||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); | |||
} | |||
public static function isInstalled($packageName) | |||
{ | |||
foreach (self::getInstalled() as $installed) { | |||
if (isset($installed['versions'][$packageName])) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
public static function satisfies(VersionParser $parser, $packageName, $constraint) | |||
{ | |||
$constraint = $parser->parseConstraints($constraint); | |||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); | |||
return $provided->matches($constraint); | |||
} | |||
public static function getVersionRanges($packageName) | |||
{ | |||
foreach (self::getInstalled() as $installed) { | |||
if (!isset($installed['versions'][$packageName])) { | |||
continue; | |||
} | |||
$ranges = array(); | |||
if (isset($installed['versions'][$packageName]['pretty_version'])) { | |||
$ranges[] = $installed['versions'][$packageName]['pretty_version']; | |||
} | |||
if (array_key_exists('aliases', $installed['versions'][$packageName])) { | |||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); | |||
} | |||
if (array_key_exists('replaced', $installed['versions'][$packageName])) { | |||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); | |||
} | |||
if (array_key_exists('provided', $installed['versions'][$packageName])) { | |||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); | |||
} | |||
return implode(' || ', $ranges); | |||
} | |||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
} | |||
public static function getVersion($packageName) | |||
{ | |||
foreach (self::getInstalled() as $installed) { | |||
if (!isset($installed['versions'][$packageName])) { | |||
continue; | |||
} | |||
if (!isset($installed['versions'][$packageName]['version'])) { | |||
return null; | |||
} | |||
return $installed['versions'][$packageName]['version']; | |||
} | |||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
} | |||
public static function getPrettyVersion($packageName) | |||
{ | |||
foreach (self::getInstalled() as $installed) { | |||
if (!isset($installed['versions'][$packageName])) { | |||
continue; | |||
} | |||
if (!isset($installed['versions'][$packageName]['pretty_version'])) { | |||
return null; | |||
} | |||
return $installed['versions'][$packageName]['pretty_version']; | |||
} | |||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
} | |||
public static function getReference($packageName) | |||
{ | |||
foreach (self::getInstalled() as $installed) { | |||
if (!isset($installed['versions'][$packageName])) { | |||
continue; | |||
} | |||
if (!isset($installed['versions'][$packageName]['reference'])) { | |||
return null; | |||
} | |||
return $installed['versions'][$packageName]['reference']; | |||
} | |||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
} | |||
public static function getRootPackage() | |||
{ | |||
$installed = self::getInstalled(); | |||
return $installed[0]['root']; | |||
} | |||
public static function getRawData() | |||
{ | |||
return self::$installed; | |||
} | |||
public static function reload($data) | |||
{ | |||
self::$installed = $data; | |||
self::$installedByVendor = array(); | |||
} | |||
private static function getInstalled() | |||
{ | |||
if (null === self::$canGetVendors) { | |||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); | |||
} | |||
$installed = array(); | |||
if (self::$canGetVendors) { | |||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { | |||
if (isset(self::$installedByVendor[$vendorDir])) { | |||
$installed[] = self::$installedByVendor[$vendorDir]; | |||
} elseif (is_file($vendorDir.'/composer/installed.php')) { | |||
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; | |||
} | |||
} | |||
} | |||
$installed[] = self::$installed; | |||
return $installed; | |||
} | |||
} |
@@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__)); | |||
$baseDir = $vendorDir; | |||
return array( | |||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', | |||
'Icewind\\SMB\\ACL' => $vendorDir . '/icewind/smb/src/ACL.php', | |||
'Icewind\\SMB\\AbstractServer' => $vendorDir . '/icewind/smb/src/AbstractServer.php', | |||
'Icewind\\SMB\\AbstractShare' => $vendorDir . '/icewind/smb/src/AbstractShare.php', | |||
@@ -57,6 +58,7 @@ return array( | |||
'Icewind\\SMB\\Native\\NativeWriteStream' => $vendorDir . '/icewind/smb/src/Native/NativeWriteStream.php', | |||
'Icewind\\SMB\\Options' => $vendorDir . '/icewind/smb/src/Options.php', | |||
'Icewind\\SMB\\ServerFactory' => $vendorDir . '/icewind/smb/src/ServerFactory.php', | |||
'Icewind\\SMB\\StringBuffer' => $vendorDir . '/icewind/smb/src/StringBuffer.php', | |||
'Icewind\\SMB\\System' => $vendorDir . '/icewind/smb/src/System.php', | |||
'Icewind\\SMB\\TimeZoneProvider' => $vendorDir . '/icewind/smb/src/TimeZoneProvider.php', | |||
'Icewind\\SMB\\Wrapped\\Connection' => $vendorDir . '/icewind/smb/src/Wrapped/Connection.php', | |||
@@ -73,13 +75,17 @@ return array( | |||
'Icewind\\Streams\\DirectoryFilter' => $vendorDir . '/icewind/streams/src/DirectoryFilter.php', | |||
'Icewind\\Streams\\DirectoryWrapper' => $vendorDir . '/icewind/streams/src/DirectoryWrapper.php', | |||
'Icewind\\Streams\\File' => $vendorDir . '/icewind/streams/src/File.php', | |||
'Icewind\\Streams\\HashWrapper' => $vendorDir . '/icewind/streams/src/HashWrapper.php', | |||
'Icewind\\Streams\\IteratorDirectory' => $vendorDir . '/icewind/streams/src/IteratorDirectory.php', | |||
'Icewind\\Streams\\NullWrapper' => $vendorDir . '/icewind/streams/src/NullWrapper.php', | |||
'Icewind\\Streams\\Path' => $vendorDir . '/icewind/streams/src/Path.php', | |||
'Icewind\\Streams\\PathWrapper' => $vendorDir . '/icewind/streams/src/PathWrapper.php', | |||
'Icewind\\Streams\\ReadHashWrapper' => $vendorDir . '/icewind/streams/src/ReadHashWrapper.php', | |||
'Icewind\\Streams\\RetryWrapper' => $vendorDir . '/icewind/streams/src/RetryWrapper.php', | |||
'Icewind\\Streams\\SeekableWrapper' => $vendorDir . '/icewind/streams/src/SeekableWrapper.php', | |||
'Icewind\\Streams\\Url' => $vendorDir . '/icewind/streams/src/Url.php', | |||
'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallBack.php', | |||
'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallback.php', | |||
'Icewind\\Streams\\Wrapper' => $vendorDir . '/icewind/streams/src/Wrapper.php', | |||
'Icewind\\Streams\\WrapperHandler' => $vendorDir . '/icewind/streams/src/WrapperHandler.php', | |||
'Icewind\\Streams\\WriteHashWrapper' => $vendorDir . '/icewind/streams/src/WriteHashWrapper.php', | |||
); |
@@ -6,7 +6,6 @@ $vendorDir = dirname(dirname(__FILE__)); | |||
$baseDir = $vendorDir; | |||
return array( | |||
'Icewind\\Streams\\Tests\\' => array($vendorDir . '/icewind/streams/tests'), | |||
'Icewind\\Streams\\' => array($vendorDir . '/icewind/streams/src'), | |||
'Icewind\\SMB\\' => array($vendorDir . '/icewind/smb/src'), | |||
); |
@@ -22,13 +22,15 @@ class ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3 | |||
return self::$loader; | |||
} | |||
require __DIR__ . '/platform_check.php'; | |||
spl_autoload_register(array('ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3', 'loadClassLoader'), true, true); | |||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(); | |||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); | |||
spl_autoload_unregister(array('ComposerAutoloaderInit98fe9b281934250b3a93f69a5ce843b3', 'loadClassLoader')); | |||
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); | |||
if ($useStaticLoader) { | |||
require_once __DIR__ . '/autoload_static.php'; | |||
require __DIR__ . '/autoload_static.php'; | |||
call_user_func(\Composer\Autoload\ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3::getInitializer($loader)); | |||
} else { |
@@ -9,17 +9,12 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 | |||
public static $prefixLengthsPsr4 = array ( | |||
'I' => | |||
array ( | |||
'Icewind\\Streams\\Tests\\' => 22, | |||
'Icewind\\Streams\\' => 16, | |||
'Icewind\\SMB\\' => 12, | |||
), | |||
); | |||
public static $prefixDirsPsr4 = array ( | |||
'Icewind\\Streams\\Tests\\' => | |||
array ( | |||
0 => __DIR__ . '/..' . '/icewind/streams/tests', | |||
), | |||
'Icewind\\Streams\\' => | |||
array ( | |||
0 => __DIR__ . '/..' . '/icewind/streams/src', | |||
@@ -31,6 +26,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 | |||
); | |||
public static $classMap = array ( | |||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', | |||
'Icewind\\SMB\\ACL' => __DIR__ . '/..' . '/icewind/smb/src/ACL.php', | |||
'Icewind\\SMB\\AbstractServer' => __DIR__ . '/..' . '/icewind/smb/src/AbstractServer.php', | |||
'Icewind\\SMB\\AbstractShare' => __DIR__ . '/..' . '/icewind/smb/src/AbstractShare.php', | |||
@@ -82,6 +78,7 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 | |||
'Icewind\\SMB\\Native\\NativeWriteStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeWriteStream.php', | |||
'Icewind\\SMB\\Options' => __DIR__ . '/..' . '/icewind/smb/src/Options.php', | |||
'Icewind\\SMB\\ServerFactory' => __DIR__ . '/..' . '/icewind/smb/src/ServerFactory.php', | |||
'Icewind\\SMB\\StringBuffer' => __DIR__ . '/..' . '/icewind/smb/src/StringBuffer.php', | |||
'Icewind\\SMB\\System' => __DIR__ . '/..' . '/icewind/smb/src/System.php', | |||
'Icewind\\SMB\\TimeZoneProvider' => __DIR__ . '/..' . '/icewind/smb/src/TimeZoneProvider.php', | |||
'Icewind\\SMB\\Wrapped\\Connection' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Connection.php', | |||
@@ -98,15 +95,19 @@ class ComposerStaticInit98fe9b281934250b3a93f69a5ce843b3 | |||
'Icewind\\Streams\\DirectoryFilter' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryFilter.php', | |||
'Icewind\\Streams\\DirectoryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryWrapper.php', | |||
'Icewind\\Streams\\File' => __DIR__ . '/..' . '/icewind/streams/src/File.php', | |||
'Icewind\\Streams\\HashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/HashWrapper.php', | |||
'Icewind\\Streams\\IteratorDirectory' => __DIR__ . '/..' . '/icewind/streams/src/IteratorDirectory.php', | |||
'Icewind\\Streams\\NullWrapper' => __DIR__ . '/..' . '/icewind/streams/src/NullWrapper.php', | |||
'Icewind\\Streams\\Path' => __DIR__ . '/..' . '/icewind/streams/src/Path.php', | |||
'Icewind\\Streams\\PathWrapper' => __DIR__ . '/..' . '/icewind/streams/src/PathWrapper.php', | |||
'Icewind\\Streams\\ReadHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/ReadHashWrapper.php', | |||
'Icewind\\Streams\\RetryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/RetryWrapper.php', | |||
'Icewind\\Streams\\SeekableWrapper' => __DIR__ . '/..' . '/icewind/streams/src/SeekableWrapper.php', | |||
'Icewind\\Streams\\Url' => __DIR__ . '/..' . '/icewind/streams/src/Url.php', | |||
'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallBack.php', | |||
'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallback.php', | |||
'Icewind\\Streams\\Wrapper' => __DIR__ . '/..' . '/icewind/streams/src/Wrapper.php', | |||
'Icewind\\Streams\\WrapperHandler' => __DIR__ . '/..' . '/icewind/streams/src/WrapperHandler.php', | |||
'Icewind\\Streams\\WriteHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/WriteHashWrapper.php', | |||
); | |||
public static function getInitializer(ClassLoader $loader) |
@@ -1,88 +1,104 @@ | |||
[ | |||
{ | |||
"name": "icewind/smb", | |||
"version": "v3.2.7", | |||
"version_normalized": "3.2.7.0", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/SMB.git", | |||
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6" | |||
{ | |||
"packages": [ | |||
{ | |||
"name": "icewind/smb", | |||
"version": "v3.4.0", | |||
"version_normalized": "3.4.0.0", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/SMB.git", | |||
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/b5c6921f2e91229c9f71556a4713b4fac91fd394", | |||
"reference": "b5c6921f2e91229c9f71556a4713b4fac91fd394", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"icewind/streams": ">=0.7.3", | |||
"php": ">=7.2" | |||
}, | |||
"require-dev": { | |||
"friendsofphp/php-cs-fixer": "^2.16", | |||
"phpstan/phpstan": "^0.12.57", | |||
"phpunit/phpunit": "^8.5|^9.3.8", | |||
"psalm/phar": "^4.3" | |||
}, | |||
"time": "2021-03-10T14:00:37+00:00", | |||
"type": "library", | |||
"installation-source": "dist", | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\SMB\\": "src/" | |||
} | |||
}, | |||
"notification-url": "https://packagist.org/downloads/", | |||
"license": [ | |||
"MIT" | |||
], | |||
"authors": [ | |||
{ | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"description": "php wrapper for smbclient and libsmbclient-php", | |||
"support": { | |||
"issues": "https://github.com/icewind1991/SMB/issues", | |||
"source": "https://github.com/icewind1991/SMB/tree/v3.4.0" | |||
}, | |||
"install-path": "../icewind/smb" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/SMB/zipball/743a7bf35317f1b76cf8e8b804e54a6c5faacad6", | |||
"reference": "743a7bf35317f1b76cf8e8b804e54a6c5faacad6", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"icewind/streams": ">=0.2.0", | |||
"php": ">=7.1" | |||
}, | |||
"require-dev": { | |||
"friendsofphp/php-cs-fixer": "^2.13", | |||
"phpunit/phpunit": "^7.0" | |||
}, | |||
"time": "2020-09-03T13:00:22+00:00", | |||
"type": "library", | |||
"installation-source": "dist", | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\SMB\\": "src/" | |||
} | |||
}, | |||
"notification-url": "https://packagist.org/downloads/", | |||
"license": [ | |||
"MIT" | |||
], | |||
"authors": [ | |||
{ | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"description": "php wrapper for smbclient and libsmbclient-php" | |||
}, | |||
{ | |||
"name": "icewind/streams", | |||
"version": "v0.7.1", | |||
"version_normalized": "0.7.1.0", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/Streams.git", | |||
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/4db3ed6c366e90b958d00e1d4c6360a9b39b2121", | |||
"reference": "4db3ed6c366e90b958d00e1d4c6360a9b39b2121", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"php": ">=5.3" | |||
}, | |||
"require-dev": { | |||
"phpunit/phpunit": "^4.8", | |||
"satooshi/php-coveralls": "v1.0.0" | |||
}, | |||
"time": "2019-02-15T12:57:29+00:00", | |||
"type": "library", | |||
"installation-source": "dist", | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\Streams\\Tests\\": "tests/", | |||
"Icewind\\Streams\\": "src/" | |||
} | |||
}, | |||
"notification-url": "https://packagist.org/downloads/", | |||
"license": [ | |||
"MIT" | |||
], | |||
"authors": [ | |||
{ | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"description": "A set of generic stream wrappers" | |||
} | |||
] | |||
{ | |||
"name": "icewind/streams", | |||
"version": "v0.7.3", | |||
"version_normalized": "0.7.3.0", | |||
"source": { | |||
"type": "git", | |||
"url": "https://github.com/icewind1991/Streams.git", | |||
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c" | |||
}, | |||
"dist": { | |||
"type": "zip", | |||
"url": "https://api.github.com/repos/icewind1991/Streams/zipball/22ef9fc5b50d645dbc202206a656cc4dde28f95c", | |||
"reference": "22ef9fc5b50d645dbc202206a656cc4dde28f95c", | |||
"shasum": "" | |||
}, | |||
"require": { | |||
"php": ">=7.1" | |||
}, | |||
"require-dev": { | |||
"friendsofphp/php-cs-fixer": "^2", | |||
"phpstan/phpstan": "^0.12", | |||
"phpunit/phpunit": "^9" | |||
}, | |||
"time": "2021-03-02T19:33:35+00:00", | |||
"type": "library", | |||
"installation-source": "dist", | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\Streams\\": "src/" | |||
} | |||
}, | |||
"notification-url": "https://packagist.org/downloads/", | |||
"license": [ | |||
"MIT" | |||
], | |||
"authors": [ | |||
{ | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"description": "A set of generic stream wrappers", | |||
"support": { | |||
"issues": "https://github.com/icewind1991/Streams/issues", | |||
"source": "https://github.com/icewind1991/Streams/tree/v0.7.3" | |||
}, | |||
"install-path": "../icewind/streams" | |||
} | |||
], | |||
"dev": true, | |||
"dev-package-names": [] | |||
} |
@@ -0,0 +1,42 @@ | |||
<?php return array ( | |||
'root' => | |||
array ( | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96', | |||
'name' => 'files_external/3rdparty', | |||
), | |||
'versions' => | |||
array ( | |||
'files_external/3rdparty' => | |||
array ( | |||
'pretty_version' => 'dev-master', | |||
'version' => 'dev-master', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '62929cc646134fbd409cfb4eacb7039d15763b96', | |||
), | |||
'icewind/smb' => | |||
array ( | |||
'pretty_version' => 'v3.4.0', | |||
'version' => '3.4.0.0', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => 'b5c6921f2e91229c9f71556a4713b4fac91fd394', | |||
), | |||
'icewind/streams' => | |||
array ( | |||
'pretty_version' => 'v0.7.3', | |||
'version' => '0.7.3.0', | |||
'aliases' => | |||
array ( | |||
), | |||
'reference' => '22ef9fc5b50d645dbc202206a656cc4dde28f95c', | |||
), | |||
), | |||
); |
@@ -0,0 +1,26 @@ | |||
<?php | |||
// platform_check.php @generated by Composer | |||
$issues = array(); | |||
if (!(PHP_VERSION_ID >= 70200)) { | |||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; | |||
} | |||
if ($issues) { | |||
if (!headers_sent()) { | |||
header('HTTP/1.1 500 Internal Server Error'); | |||
} | |||
if (!ini_get('display_errors')) { | |||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { | |||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); | |||
} elseif (!headers_sent()) { | |||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; | |||
} | |||
} | |||
trigger_error( | |||
'Composer detected issues in your platform: ' . implode(' ', $issues), | |||
E_USER_ERROR | |||
); | |||
} |
@@ -4,3 +4,4 @@ composer.lock | |||
.php_cs.cache | |||
listen.php | |||
test.php | |||
*.cache |
@@ -1,9 +1,8 @@ | |||
SMB | |||
=== | |||
[![Code Coverage](https://scrutinizer-ci.com/g/icewind1991/SMB/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/SMB/?branch=master) | |||
[![Build Status](https://travis-ci.org/icewind1991/SMB.svg?branch=master)](https://travis-ci.org/icewind1991/SMB) | |||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/icewind1991/SMB/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/SMB/?branch=master) | |||
[![CI](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/SMB/actions/workflows/ci.yaml) | |||
[![codecov](https://codecov.io/gh/icewind1991/SMB/branch/master/graph/badge.svg?token=eTg0P466k6)](https://codecov.io/gh/icewind1991/SMB) | |||
PHP wrapper for `smbclient` and [`libsmbclient-php`](https://github.com/eduardok/libsmbclient-php) | |||
@@ -103,7 +102,7 @@ fclose($fh); | |||
``` | |||
**Note**: write() will truncate your file to 0bytes. You may open a writeable stream with append() which will point | |||
the cursor to the end of the file or create it if it does not exists yet. (append() is only compatible with libsmbclient-php) | |||
the cursor to the end of the file or create it if it does not exist yet. (append() is only compatible with libsmbclient-php) | |||
```php | |||
$fh = $share->append('test.txt'); | |||
fwrite($fh, 'bar'); | |||
@@ -127,11 +126,22 @@ $options->setTimeout(5); | |||
$serverFactory = new ServerFactory($options); | |||
``` | |||
### Setting protocol version | |||
```php | |||
$options = new Options(); | |||
$options->setMinProtocol(IOptions::PROTOCOL_SMB2); | |||
$options->setMaxProtocol(IOptions::PROTOCOL_SMB3); | |||
$serverFactory = new ServerFactory($options); | |||
``` | |||
Note, setting the protocol version is not supported with php-smbclient version 1.0.1 or lower. | |||
### Customizing system integration | |||
The `smbclient` backend needs to get various information about the system it's running on to function | |||
such as the paths of various binaries or the system timezone. | |||
While the default logic for getting this information should work on most systems, it possible to customize this behaviour. | |||
While the default logic for getting this information should work on most systems, it is possible to customize this behaviour. | |||
In order to customize the integration you provide a custom implementation of `ITimezoneProvider` and/or `ISystem` and pass them as arguments to the `ServerFactory`. | |||
@@ -1,29 +1,37 @@ | |||
{ | |||
"name" : "icewind/smb", | |||
"description" : "php wrapper for smbclient and libsmbclient-php", | |||
"license" : "MIT", | |||
"authors" : [ | |||
{ | |||
"name" : "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"require" : { | |||
"php": ">=7.1", | |||
"icewind/streams": ">=0.2.0" | |||
}, | |||
"require-dev": { | |||
"phpunit/phpunit": "^7.0", | |||
"friendsofphp/php-cs-fixer": "^2.13" | |||
}, | |||
"autoload" : { | |||
"psr-4": { | |||
"Icewind\\SMB\\": "src/" | |||
} | |||
}, | |||
"autoload-dev" : { | |||
"psr-4": { | |||
"Icewind\\SMB\\Test\\": "tests/" | |||
} | |||
} | |||
"name": "icewind/smb", | |||
"description": "php wrapper for smbclient and libsmbclient-php", | |||
"license": "MIT", | |||
"authors": [ | |||
{ | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"require": { | |||
"php": ">=7.2", | |||
"icewind/streams": ">=0.7.3" | |||
}, | |||
"require-dev": { | |||
"phpunit/phpunit": "^8.5|^9.3.8", | |||
"friendsofphp/php-cs-fixer": "^2.16", | |||
"phpstan/phpstan": "^0.12.57", | |||
"psalm/phar": "^4.3" | |||
}, | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\SMB\\": "src/" | |||
} | |||
}, | |||
"autoload-dev": { | |||
"psr-4": { | |||
"Icewind\\SMB\\Test\\": "tests/" | |||
} | |||
}, | |||
"scripts": { | |||
"lint": "parallel-lint --exclude src --exclude vendor --exclude target --exclude build .", | |||
"cs:check": "php-cs-fixer fix --dry-run --diff", | |||
"cs:fix": "php-cs-fixer fix", | |||
"psalm": "psalm.phar" | |||
} | |||
} |
@@ -33,8 +33,11 @@ class ACL { | |||
const FLAG_OBJECT_INHERIT = 0x1; | |||
const FLAG_CONTAINER_INHERIT = 0x2; | |||
/** @var int */ | |||
private $type; | |||
/** @var int */ | |||
private $flags; | |||
/** @var int */ | |||
private $mask; | |||
public function __construct(int $type, int $flags, int $mask) { |
@@ -24,24 +24,16 @@ namespace Icewind\SMB; | |||
abstract class AbstractServer implements IServer { | |||
const LOCALE = 'en_US.UTF-8'; | |||
/** | |||
* @var string $host | |||
*/ | |||
/** @var string */ | |||
protected $host; | |||
/** | |||
* @var IAuth $user | |||
*/ | |||
/** @var IAuth */ | |||
protected $auth; | |||
/** | |||
* @var ISystem | |||
*/ | |||
/** @var ISystem */ | |||
protected $system; | |||
/** | |||
* @var TimeZoneProvider | |||
*/ | |||
/** @var ITimeZoneProvider */ | |||
protected $timezoneProvider; | |||
/** @var IOptions */ | |||
@@ -51,10 +43,10 @@ abstract class AbstractServer implements IServer { | |||
* @param string $host | |||
* @param IAuth $auth | |||
* @param ISystem $system | |||
* @param TimeZoneProvider $timeZoneProvider | |||
* @param ITimeZoneProvider $timeZoneProvider | |||
* @param IOptions $options | |||
*/ | |||
public function __construct($host, IAuth $auth, ISystem $system, TimeZoneProvider $timeZoneProvider, IOptions $options) { | |||
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) { | |||
$this->host = $host; | |||
$this->auth = $auth; | |||
$this->system = $system; | |||
@@ -62,23 +54,23 @@ abstract class AbstractServer implements IServer { | |||
$this->options = $options; | |||
} | |||
public function getAuth() { | |||
public function getAuth(): IAuth { | |||
return $this->auth; | |||
} | |||
public function getHost() { | |||
public function getHost(): string { | |||
return $this->host; | |||
} | |||
public function getTimeZone() { | |||
public function getTimeZone(): string { | |||
return $this->timezoneProvider->get($this->host); | |||
} | |||
public function getSystem() { | |||
public function getSystem(): ISystem { | |||
return $this->system; | |||
} | |||
public function getOptions() { | |||
public function getOptions(): IOptions { | |||
return $this->options; | |||
} | |||
} |
@@ -10,13 +10,18 @@ namespace Icewind\SMB; | |||
use Icewind\SMB\Exception\InvalidPathException; | |||
abstract class AbstractShare implements IShare { | |||
/** @var string[] */ | |||
private $forbiddenCharacters; | |||
public function __construct() { | |||
$this->forbiddenCharacters = ['?', '<', '>', ':', '*', '|', '"', chr(0), "\n", "\r"]; | |||
} | |||
protected function verifyPath($path) { | |||
/** | |||
* @param string $path | |||
* @throws InvalidPathException | |||
*/ | |||
protected function verifyPath(string $path): void { | |||
foreach ($this->forbiddenCharacters as $char) { | |||
if (strpos($path, $char) !== false) { | |||
throw new InvalidPathException('Invalid path, "' . $char . '" is not allowed'); | |||
@@ -24,7 +29,10 @@ abstract class AbstractShare implements IShare { | |||
} | |||
} | |||
public function setForbiddenChars(array $charList) { | |||
/** | |||
* @param string[] $charList | |||
*/ | |||
public function setForbiddenChars(array $charList): void { | |||
$this->forbiddenCharacters = $charList; | |||
} | |||
} |
@@ -22,23 +22,23 @@ | |||
namespace Icewind\SMB; | |||
class AnonymousAuth implements IAuth { | |||
public function getUsername() { | |||
public function getUsername(): ?string { | |||
return null; | |||
} | |||
public function getWorkgroup() { | |||
public function getWorkgroup(): ?string { | |||
return 'dummy'; | |||
} | |||
public function getPassword() { | |||
public function getPassword(): ?string { | |||
return null; | |||
} | |||
public function getExtraCommandLineArguments() { | |||
public function getExtraCommandLineArguments(): string { | |||
return '-N'; | |||
} | |||
public function setExtraSmbClientOptions($smbClientState) { | |||
public function setExtraSmbClientOptions($smbClientState): void { | |||
smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true); | |||
} | |||
} |
@@ -24,41 +24,34 @@ namespace Icewind\SMB; | |||
class BasicAuth implements IAuth { | |||
/** @var string */ | |||
private $username; | |||
/** @var string */ | |||
/** @var string|null */ | |||
private $workgroup; | |||
/** @var string */ | |||
private $password; | |||
/** | |||
* BasicAuth constructor. | |||
* | |||
* @param string $username | |||
* @param string $workgroup | |||
* @param string $password | |||
*/ | |||
public function __construct($username, $workgroup, $password) { | |||
public function __construct(string $username, ?string $workgroup, string $password) { | |||
$this->username = $username; | |||
$this->workgroup = $workgroup; | |||
$this->password = $password; | |||
} | |||
public function getUsername() { | |||
public function getUsername(): ?string { | |||
return $this->username; | |||
} | |||
public function getWorkgroup() { | |||
public function getWorkgroup(): ?string { | |||
return $this->workgroup; | |||
} | |||
public function getPassword() { | |||
public function getPassword(): ?string { | |||
return $this->password; | |||
} | |||
public function getExtraCommandLineArguments() { | |||
public function getExtraCommandLineArguments(): string { | |||
return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : ''; | |||
} | |||
public function setExtraSmbClientOptions($smbClientState) { | |||
public function setExtraSmbClientOptions($smbClientState): void { | |||
// noop | |||
} | |||
} |
@@ -9,32 +9,21 @@ | |||
namespace Icewind\SMB; | |||
class Change { | |||
/** @var int */ | |||
private $code; | |||
/** @var string */ | |||
private $path; | |||
/** | |||
* Change constructor. | |||
* | |||
* @param $code | |||
* @param $path | |||
*/ | |||
public function __construct($code, $path) { | |||
public function __construct(int $code, string $path) { | |||
$this->code = $code; | |||
$this->path = $path; | |||
} | |||
/** | |||
* @return integer | |||
*/ | |||
public function getCode() { | |||
public function getCode(): int { | |||
return $this->code; | |||
} | |||
/** | |||
* @return string | |||
*/ | |||
public function getPath() { | |||
public function getPath(): string { | |||
return $this->path; | |||
} | |||
} |
@@ -7,23 +7,37 @@ | |||
namespace Icewind\SMB\Exception; | |||
use Throwable; | |||
/** | |||
* @psalm-consistent-constructor | |||
*/ | |||
class Exception extends \Exception { | |||
public static function unknown($path, $error) { | |||
$message = 'Unknown error (' . $error . ')'; | |||
public function __construct(string $message = "", int $code = 0, Throwable $previous = null) { | |||
parent::__construct($message, $code, $previous); | |||
} | |||
/** | |||
* @param string|null $path | |||
* @param string|int|null $error | |||
* @return Exception | |||
*/ | |||
public static function unknown(?string $path, $error): Exception { | |||
$message = 'Unknown error (' . (string)$error . ')'; | |||
if ($path) { | |||
$message .= ' for ' . $path; | |||
} | |||
return new Exception($message, is_string($error) ? 0 : $error); | |||
return new Exception($message, is_int($error) ? $error : 0); | |||
} | |||
/** | |||
* @param array $exceptionMap | |||
* @param mixed $error | |||
* @param string $path | |||
* @param array<int|string, class-string<Exception>> $exceptionMap | |||
* @param string|int|null $error | |||
* @param string|null $path | |||
* @return Exception | |||
*/ | |||
public static function fromMap(array $exceptionMap, $error, $path) { | |||
public static function fromMap(array $exceptionMap, $error, ?string $path): Exception { | |||
if (isset($exceptionMap[$error])) { | |||
$exceptionClass = $exceptionMap[$error]; | |||
if (is_numeric($error)) { |
@@ -13,15 +13,11 @@ class InvalidRequestException extends Exception { | |||
*/ | |||
protected $path; | |||
/** | |||
* @param string $path | |||
* @param int $code | |||
*/ | |||
public function __construct($path, $code = 0) { | |||
public function __construct(string $path = "", int $code = 0, \Throwable $previous = null) { | |||
$class = get_class($this); | |||
$parts = explode('\\', $class); | |||
$baseName = array_pop($parts); | |||
parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code); | |||
parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code, $previous); | |||
$this->path = $path; | |||
} | |||
@@ -10,7 +10,7 @@ namespace Icewind\SMB\Exception; | |||
use Throwable; | |||
class RevisionMismatchException extends Exception { | |||
public function __construct($message = 'Protocol version mismatch', $code = 0, Throwable $previous = null) { | |||
public function __construct(string $message = 'Protocol version mismatch', int $code = 0, Throwable $previous = null) { | |||
parent::__construct($message, $code, $previous); | |||
} | |||
} |
@@ -22,32 +22,23 @@ | |||
namespace Icewind\SMB; | |||
interface IAuth { | |||
/** | |||
* @return string|null | |||
*/ | |||
public function getUsername(); | |||
public function getUsername(): ?string; | |||
/** | |||
* @return string|null | |||
*/ | |||
public function getWorkgroup(); | |||
public function getWorkgroup(): ?string; | |||
/** | |||
* @return string|null | |||
*/ | |||
public function getPassword(); | |||
public function getPassword(): ?string; | |||
/** | |||
* Any extra command line option for smbclient that are required | |||
* | |||
* @return string | |||
*/ | |||
public function getExtraCommandLineArguments(); | |||
public function getExtraCommandLineArguments(): string; | |||
/** | |||
* Set any extra options for libsmbclient that are required | |||
* | |||
* @param resource $smbClientState | |||
*/ | |||
public function setExtraSmbClientOptions($smbClientState); | |||
public function setExtraSmbClientOptions($smbClientState): void; | |||
} |
@@ -21,50 +21,23 @@ interface IFileInfo { | |||
const MODE_ARCHIVE = 0x20; | |||
const MODE_NORMAL = 0x80; | |||
/** | |||
* @return string | |||
*/ | |||
public function getPath(); | |||
public function getPath(): string; | |||
/** | |||
* @return string | |||
*/ | |||
public function getName(); | |||
public function getName(): string; | |||
/** | |||
* @return int | |||
*/ | |||
public function getSize(); | |||
public function getSize(): int; | |||
/** | |||
* @return int | |||
*/ | |||
public function getMTime(); | |||
public function getMTime(): int; | |||
/** | |||
* @return bool | |||
*/ | |||
public function isDirectory(); | |||
public function isDirectory(): bool; | |||
/** | |||
* @return bool | |||
*/ | |||
public function isReadOnly(); | |||
public function isReadOnly(): bool; | |||
/** | |||
* @return bool | |||
*/ | |||
public function isHidden(); | |||
public function isHidden(): bool; | |||
/** | |||
* @return bool | |||
*/ | |||
public function isSystem(); | |||
public function isSystem(): bool; | |||
/** | |||
* @return bool | |||
*/ | |||
public function isArchived(); | |||
public function isArchived(): bool; | |||
/** | |||
* @return ACL[] |
@@ -25,21 +25,21 @@ interface INotifyHandler { | |||
* | |||
* @return Change[] | |||
*/ | |||
public function getChanges(); | |||
public function getChanges(): array; | |||
/** | |||
* 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 | |||
* @param callable(Change):?bool $callback | |||
*/ | |||
public function listen($callback); | |||
public function listen(callable $callback): void; | |||
/** | |||
* Stop listening for changes | |||
* | |||
* Note that any pending changes will be discarded | |||
*/ | |||
public function stop(); | |||
public function stop(): void; | |||
} |
@@ -22,8 +22,20 @@ | |||
namespace Icewind\SMB; | |||
interface IOptions { | |||
/** | |||
* @return int | |||
*/ | |||
public function getTimeout(); | |||
const PROTOCOL_NT1 = 'NT1'; | |||
const PROTOCOL_SMB2 = 'SMB2'; | |||
const PROTOCOL_SMB2_02 = 'SMB2_02'; | |||
const PROTOCOL_SMB2_22 = 'SMB2_22'; | |||
const PROTOCOL_SMB2_24 = 'SMB2_24'; | |||
const PROTOCOL_SMB3 = 'SMB3'; | |||
const PROTOCOL_SMB3_00 = 'SMB3_00'; | |||
const PROTOCOL_SMB3_02 = 'SMB3_02'; | |||
const PROTOCOL_SMB3_10 = 'SMB3_10'; | |||
const PROTOCOL_SMB3_11 = 'SMB3_11'; | |||
public function getTimeout(): int; | |||
public function getMinProtocol(): ?string; | |||
public function getMaxProtocol(): ?string; | |||
} |
@@ -22,15 +22,9 @@ | |||
namespace Icewind\SMB; | |||
interface IServer { | |||
/** | |||
* @return IAuth | |||
*/ | |||
public function getAuth(); | |||
public function getAuth(): IAuth; | |||
/** | |||
* @return string | |||
*/ | |||
public function getHost(); | |||
public function getHost(): string; | |||
/** | |||
* @return \Icewind\SMB\IShare[] | |||
@@ -38,32 +32,15 @@ interface IServer { | |||
* @throws \Icewind\SMB\Exception\AuthenticationException | |||
* @throws \Icewind\SMB\Exception\InvalidHostException | |||
*/ | |||
public function listShares(); | |||
public function listShares(): array; | |||
/** | |||
* @param string $name | |||
* @return \Icewind\SMB\IShare | |||
*/ | |||
public function getShare($name); | |||
public function getShare(string $name): IShare; | |||
/** | |||
* @return string | |||
*/ | |||
public function getTimeZone(); | |||
public function getTimeZone(): string; | |||
/** | |||
* @return ISystem | |||
*/ | |||
public function getSystem(); | |||
public function getSystem(): ISystem; | |||
/** | |||
* @return IOptions | |||
*/ | |||
public function getOptions(); | |||
public function getOptions(): IOptions; | |||
/** | |||
* @param ISystem $system | |||
* @return bool | |||
*/ | |||
public static function available(ISystem $system); | |||
public static function available(ISystem $system): bool; | |||
} |
@@ -7,13 +7,18 @@ | |||
namespace Icewind\SMB; | |||
use Icewind\SMB\Exception\AlreadyExistsException; | |||
use Icewind\SMB\Exception\InvalidRequestException; | |||
use Icewind\SMB\Exception\InvalidTypeException; | |||
use Icewind\SMB\Exception\NotFoundException; | |||
interface IShare { | |||
/** | |||
* Get the name of the share | |||
* | |||
* @return string | |||
*/ | |||
public function getName(); | |||
public function getName(): string; | |||
/** | |||
* Download a remote file | |||
@@ -22,10 +27,10 @@ interface IShare { | |||
* @param string $target local file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function get($source, $target); | |||
public function get(string $source, string $target): bool; | |||
/** | |||
* Upload a local file | |||
@@ -34,10 +39,10 @@ interface IShare { | |||
* @param string $target remove file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function put($source, $target); | |||
public function put(string $source, string $target): bool; | |||
/** | |||
* Open a readable stream top a remote file | |||
@@ -45,10 +50,10 @@ interface IShare { | |||
* @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 | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function read($source); | |||
public function read(string $source); | |||
/** | |||
* Open a writable stream to a remote file | |||
@@ -57,10 +62,10 @@ interface IShare { | |||
* @param string $target | |||
* @return resource a write only stream to upload a remote file | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function write($target); | |||
public function write(string $target); | |||
/** | |||
* Open a writable stream to a remote file and set the cursor to the end of the file | |||
@@ -68,11 +73,11 @@ interface IShare { | |||
* @param string $target | |||
* @return resource a write only stream to upload a remote file | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws \Icewind\SMB\Exception\InvalidRequestException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
* @throws InvalidRequestException | |||
*/ | |||
public function append($target); | |||
public function append(string $target); | |||
/** | |||
* Rename a remote file | |||
@@ -81,10 +86,10 @@ interface IShare { | |||
* @param string $to | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function rename($from, $to); | |||
public function rename(string $from, string $to): bool; | |||
/** | |||
* Delete a file on the share | |||
@@ -92,29 +97,29 @@ interface IShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function del($path); | |||
public function del(string $path): bool; | |||
/** | |||
* List the content of a remote folder | |||
* | |||
* @param $path | |||
* @return \Icewind\SMB\IFileInfo[] | |||
* @param string $path | |||
* @return IFileInfo[] | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function dir($path); | |||
public function dir(string $path): array; | |||
/** | |||
* @param string $path | |||
* @return \Icewind\SMB\IFileInfo | |||
* @return IFileInfo | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws NotFoundException | |||
*/ | |||
public function stat($path); | |||
public function stat(string $path): IFileInfo; | |||
/** | |||
* Create a folder on the share | |||
@@ -122,10 +127,10 @@ interface IShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function mkdir($path); | |||
public function mkdir(string $path): bool; | |||
/** | |||
* Remove a folder on the share | |||
@@ -133,23 +138,23 @@ interface IShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function rmdir($path); | |||
public function rmdir(string $path): bool; | |||
/** | |||
* @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); | |||
public function setMode(string $path, int $mode); | |||
/** | |||
* @param string $path | |||
* @return INotifyHandler | |||
*/ | |||
public function notify($path); | |||
public function notify(string $path); | |||
/** | |||
* Get the IServer instance for this share |
@@ -32,47 +32,47 @@ interface ISystem { | |||
* @param int $num the file descriptor id | |||
* @return string | |||
*/ | |||
public function getFD($num); | |||
public function getFD(int $num): string; | |||
/** | |||
* Get the full path to the `smbclient` binary of false if the binary is not available | |||
* Get the full path to the `smbclient` binary of null if the binary is not available | |||
* | |||
* @return string|bool | |||
* @return string|null | |||
*/ | |||
public function getSmbclientPath(); | |||
public function getSmbclientPath(): ?string; | |||
/** | |||
* Get the full path to the `net` binary of false if the binary is not available | |||
* Get the full path to the `net` binary of null if the binary is not available | |||
* | |||
* @return string|bool | |||
* @return string|null | |||
*/ | |||
public function getNetPath(); | |||
public function getNetPath(): ?string; | |||
/** | |||
* Get the full path to the `smbcacls` binary of false if the binary is not available | |||
* Get the full path to the `smbcacls` binary of null if the binary is not available | |||
* | |||
* @return string|bool | |||
* @return string|null | |||
*/ | |||
public function getSmbcAclsPath(); | |||
public function getSmbcAclsPath(): ?string; | |||
/** | |||
* Get the full path to the `stdbuf` binary of false if the binary is not available | |||
* Get the full path to the `stdbuf` binary of null if the binary is not available | |||
* | |||
* @return string|bool | |||
* @return string|null | |||
*/ | |||
public function getStdBufPath(); | |||
public function getStdBufPath(): ?string; | |||
/** | |||
* Get the full path to the `date` binary of false if the binary is not available | |||
* Get the full path to the `date` binary of null if the binary is not available | |||
* | |||
* @return string|bool | |||
* @return string|null | |||
*/ | |||
public function getDatePath(); | |||
public function getDatePath(): ?string; | |||
/** | |||
* Whether or not the smbclient php extension is enabled | |||
* | |||
* @return bool | |||
*/ | |||
public function libSmbclientAvailable(); | |||
public function libSmbclientAvailable(): bool; | |||
} |
@@ -28,5 +28,5 @@ interface ITimeZoneProvider { | |||
* @param string $host | |||
* @return string | |||
*/ | |||
public function get($host); | |||
public function get(string $host): string; | |||
} |
@@ -25,23 +25,23 @@ namespace Icewind\SMB; | |||
* Use existing kerberos ticket to authenticate | |||
*/ | |||
class KerberosAuth implements IAuth { | |||
public function getUsername() { | |||
public function getUsername(): ?string { | |||
return 'dummy'; | |||
} | |||
public function getWorkgroup() { | |||
public function getWorkgroup(): ?string { | |||
return 'dummy'; | |||
} | |||
public function getPassword() { | |||
public function getPassword(): ?string { | |||
return null; | |||
} | |||
public function getExtraCommandLineArguments() { | |||
public function getExtraCommandLineArguments(): string { | |||
return '-k'; | |||
} | |||
public function setExtraSmbClientOptions($smbClientState) { | |||
public function setExtraSmbClientOptions($smbClientState): void { | |||
smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true); | |||
smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false); | |||
} |
@@ -8,88 +8,71 @@ | |||
namespace Icewind\SMB\Native; | |||
use Icewind\SMB\ACL; | |||
use Icewind\SMB\Exception\Exception; | |||
use Icewind\SMB\IFileInfo; | |||
class NativeFileInfo implements IFileInfo { | |||
/** | |||
* @var string | |||
*/ | |||
/** @var string */ | |||
protected $path; | |||
/** | |||
* @var string | |||
*/ | |||
/** @var string */ | |||
protected $name; | |||
/** | |||
* @var NativeShare | |||
*/ | |||
/** @var NativeShare */ | |||
protected $share; | |||
/** | |||
* @var array|null | |||
*/ | |||
/** @var array{"mode": int, "size": int, "write_time": int}|null */ | |||
protected $attributeCache = null; | |||
/** | |||
* @param NativeShare $share | |||
* @param string $path | |||
* @param string $name | |||
*/ | |||
public function __construct($share, $path, $name) { | |||
public function __construct(NativeShare $share, string $path, string $name) { | |||
$this->share = $share; | |||
$this->path = $path; | |||
$this->name = $name; | |||
} | |||
/** | |||
* @return string | |||
*/ | |||
public function getPath() { | |||
public function getPath(): string { | |||
return $this->path; | |||
} | |||
/** | |||
* @return string | |||
*/ | |||
public function getName() { | |||
public function getName(): string { | |||
return $this->name; | |||
} | |||
/** | |||
* @return array | |||
* @return array{"mode": int, "size": int, "write_time": int} | |||
*/ | |||
protected function stat() { | |||
protected function stat(): array { | |||
if (is_null($this->attributeCache)) { | |||
$rawAttributes = explode(',', $this->share->getAttribute($this->path, 'system.dos_attr.*')); | |||
$this->attributeCache = []; | |||
$attributes = []; | |||
foreach ($rawAttributes as $rawAttribute) { | |||
list($name, $value) = explode(':', $rawAttribute); | |||
$name = strtolower($name); | |||
if ($name == 'mode') { | |||
$this->attributeCache[$name] = (int)hexdec(substr($value, 2)); | |||
$attributes[$name] = (int)hexdec(substr($value, 2)); | |||
} else { | |||
$this->attributeCache[$name] = (int)$value; | |||
$attributes[$name] = (int)$value; | |||
} | |||
} | |||
if (!isset($attributes['mode'])) { | |||
throw new Exception("Invalid attribute response"); | |||
} | |||
if (!isset($attributes['size'])) { | |||
throw new Exception("Invalid attribute response"); | |||
} | |||
if (!isset($attributes['write_time'])) { | |||
throw new Exception("Invalid attribute response"); | |||
} | |||
$this->attributeCache = $attributes; | |||
} | |||
return $this->attributeCache; | |||
} | |||
/** | |||
* @return int | |||
*/ | |||
public function getSize() { | |||
public function getSize(): int { | |||
$stat = $this->stat(); | |||
return $stat['size']; | |||
} | |||
/** | |||
* @return int | |||
*/ | |||
public function getMTime() { | |||
public function getMTime(): int { | |||
$stat = $this->stat(); | |||
return $stat['change_time']; | |||
return $stat['write_time']; | |||
} | |||
/** | |||
@@ -104,22 +87,16 @@ class NativeFileInfo implements IFileInfo { | |||
* as false (except for `hidden` where we use the unix dotfile convention) | |||
*/ | |||
/** | |||
* @return int | |||
*/ | |||
protected function getMode() { | |||
protected function getMode(): int { | |||
$mode = $this->stat()['mode']; | |||
// Let us ignore the ATTR_NOT_CONTENT_INDEXED for now | |||
$mode &= ~0x00002000; | |||
return $mode; | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isDirectory() { | |||
public function isDirectory(): bool { | |||
$mode = $this->getMode(); | |||
if ($mode > 0x1000) { | |||
return (bool)($mode & 0x4000); // 0x4000: unix directory flag | |||
@@ -128,10 +105,7 @@ class NativeFileInfo implements IFileInfo { | |||
} | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isReadOnly() { | |||
public function isReadOnly(): bool { | |||
$mode = $this->getMode(); | |||
if ($mode > 0x1000) { | |||
return !(bool)($mode & 0x80); // 0x80: owner write permissions | |||
@@ -140,10 +114,7 @@ class NativeFileInfo implements IFileInfo { | |||
} | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isHidden() { | |||
public function isHidden(): bool { | |||
$mode = $this->getMode(); | |||
if ($mode > 0x1000) { | |||
return strlen($this->name) > 0 && $this->name[0] === '.'; | |||
@@ -152,10 +123,7 @@ class NativeFileInfo implements IFileInfo { | |||
} | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isSystem() { | |||
public function isSystem(): bool { | |||
$mode = $this->getMode(); | |||
if ($mode > 0x1000) { | |||
return false; | |||
@@ -164,10 +132,7 @@ class NativeFileInfo implements IFileInfo { | |||
} | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isArchived() { | |||
public function isArchived(): bool { | |||
$mode = $this->getMode(); | |||
if ($mode > 0x1000) { | |||
return false; | |||
@@ -185,10 +150,11 @@ class NativeFileInfo implements IFileInfo { | |||
foreach (explode(',', $attribute) as $acl) { | |||
list($user, $permissions) = explode(':', $acl, 2); | |||
$user = trim($user, '\\'); | |||
list($type, $flags, $mask) = explode('/', $permissions); | |||
$mask = hexdec($mask); | |||
$acls[$user] = new ACL($type, $flags, $mask); | |||
$acls[$user] = new ACL((int)$type, (int)$flags, (int)$mask); | |||
} | |||
return $acls; |
@@ -7,64 +7,54 @@ | |||
namespace Icewind\SMB\Native; | |||
use Icewind\SMB\StringBuffer; | |||
/** | |||
* Stream optimized for read only usage | |||
*/ | |||
class NativeReadStream extends NativeStream { | |||
const CHUNK_SIZE = 1048576; // 1MB chunks | |||
/** | |||
* @var resource | |||
*/ | |||
private $readBuffer = null; | |||
private $bufferSize = 0; | |||
/** @var StringBuffer */ | |||
private $readBuffer; | |||
public function __construct() { | |||
$this->readBuffer = new StringBuffer(); | |||
} | |||
/** @var int */ | |||
private $pos = 0; | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$this->readBuffer = fopen('php://memory', 'r+'); | |||
return parent::stream_open($path, $mode, $options, $opened_path); | |||
} | |||
/** | |||
* Wrap a stream from libsmbclient-php into a regular php stream | |||
* | |||
* @param \Icewind\SMB\NativeState $state | |||
* @param NativeState $state | |||
* @param resource $smbStream | |||
* @param string $mode | |||
* @param string $url | |||
* @return resource | |||
*/ | |||
public static function wrap($state, $smbStream, $mode, $url) { | |||
stream_wrapper_register('nativesmb', NativeReadStream::class); | |||
$context = stream_context_create([ | |||
'nativesmb' => [ | |||
'state' => $state, | |||
'handle' => $smbStream, | |||
'url' => $url | |||
] | |||
]); | |||
$fh = fopen('nativesmb://', $mode, false, $context); | |||
stream_wrapper_unregister('nativesmb'); | |||
return $fh; | |||
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) { | |||
return parent::wrapClass($state, $smbStream, $mode, $url, NativeReadStream::class); | |||
} | |||
public function stream_read($count) { | |||
// php reads 8192 bytes at once | |||
// however due to network latency etc, it's faster to read in larger chunks | |||
// and buffer the result | |||
if (!parent::stream_eof() && $this->bufferSize < $count) { | |||
$remaining = $this->readBuffer; | |||
$this->readBuffer = fopen('php://memory', 'r+'); | |||
$this->bufferSize = 0; | |||
stream_copy_to_stream($remaining, $this->readBuffer); | |||
$this->bufferSize += fwrite($this->readBuffer, parent::stream_read(self::CHUNK_SIZE)); | |||
fseek($this->readBuffer, 0); | |||
if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) { | |||
$chunk = parent::stream_read(self::CHUNK_SIZE); | |||
if ($chunk === false) { | |||
return false; | |||
} | |||
$this->readBuffer->push($chunk); | |||
} | |||
$result = fread($this->readBuffer, $count); | |||
$this->bufferSize -= $count; | |||
$result = $this->readBuffer->read($count); | |||
$read = strlen($result); | |||
$this->pos += $read; | |||
@@ -75,15 +65,18 @@ class NativeReadStream extends NativeStream { | |||
public function stream_seek($offset, $whence = SEEK_SET) { | |||
$result = parent::stream_seek($offset, $whence); | |||
if ($result) { | |||
$this->readBuffer = fopen('php://memory', 'r+'); | |||
$this->bufferSize = 0; | |||
$this->pos = parent::stream_tell(); | |||
$this->readBuffer->clear(); | |||
$pos = parent::stream_tell(); | |||
if ($pos === false) { | |||
return false; | |||
} | |||
$this->pos = $pos; | |||
} | |||
return $result; | |||
} | |||
public function stream_eof() { | |||
return $this->bufferSize <= 0 && parent::stream_eof(); | |||
return $this->readBuffer->remaining() <= 0 && parent::stream_eof(); | |||
} | |||
public function stream_tell() { |
@@ -8,10 +8,13 @@ | |||
namespace Icewind\SMB\Native; | |||
use Icewind\SMB\AbstractServer; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
use Icewind\SMB\Exception\InvalidHostException; | |||
use Icewind\SMB\IAuth; | |||
use Icewind\SMB\IOptions; | |||
use Icewind\SMB\IShare; | |||
use Icewind\SMB\ISystem; | |||
use Icewind\SMB\TimeZoneProvider; | |||
use Icewind\SMB\ITimeZoneProvider; | |||
class NativeServer extends AbstractServer { | |||
/** | |||
@@ -19,38 +22,34 @@ class NativeServer extends AbstractServer { | |||
*/ | |||
protected $state; | |||
public function __construct($host, IAuth $auth, ISystem $system, TimeZoneProvider $timeZoneProvider, IOptions $options) { | |||
public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) { | |||
parent::__construct($host, $auth, $system, $timeZoneProvider, $options); | |||
$this->state = new NativeState(); | |||
} | |||
protected function connect() { | |||
protected function connect(): void { | |||
$this->state->init($this->getAuth(), $this->getOptions()); | |||
} | |||
/** | |||
* @return \Icewind\SMB\IShare[] | |||
* @throws \Icewind\SMB\Exception\AuthenticationException | |||
* @throws \Icewind\SMB\Exception\InvalidHostException | |||
* @return IShare[] | |||
* @throws AuthenticationException | |||
* @throws InvalidHostException | |||
*/ | |||
public function listShares() { | |||
public function listShares(): array { | |||
$this->connect(); | |||
$shares = []; | |||
$dh = $this->state->opendir('smb://' . $this->getHost()); | |||
while ($share = $this->state->readdir($dh)) { | |||
while ($share = $this->state->readdir($dh, '')) { | |||
if ($share['type'] === 'file share') { | |||
$shares[] = $this->getShare($share['name']); | |||
} | |||
} | |||
$this->state->closedir($dh); | |||
$this->state->closedir($dh, ''); | |||
return $shares; | |||
} | |||
/** | |||
* @param string $name | |||
* @return \Icewind\SMB\IShare | |||
*/ | |||
public function getShare($name) { | |||
public function getShare(string $name): IShare { | |||
return new NativeShare($this, $name); | |||
} | |||
@@ -60,7 +59,7 @@ class NativeServer extends AbstractServer { | |||
* @param ISystem $system | |||
* @return bool | |||
*/ | |||
public static function available(ISystem $system) { | |||
public static function available(ISystem $system): bool { | |||
return $system->libSmbclientAvailable(); | |||
} | |||
} |
@@ -8,9 +8,16 @@ | |||
namespace Icewind\SMB\Native; | |||
use Icewind\SMB\AbstractShare; | |||
use Icewind\SMB\Exception\AlreadyExistsException; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
use Icewind\SMB\Exception\ConnectionException; | |||
use Icewind\SMB\Exception\DependencyException; | |||
use Icewind\SMB\Exception\InvalidHostException; | |||
use Icewind\SMB\Exception\InvalidPathException; | |||
use Icewind\SMB\Exception\InvalidResourceException; | |||
use Icewind\SMB\Exception\InvalidTypeException; | |||
use Icewind\SMB\Exception\NotFoundException; | |||
use Icewind\SMB\IFileInfo; | |||
use Icewind\SMB\INotifyHandler; | |||
use Icewind\SMB\IServer; | |||
use Icewind\SMB\Wrapped\Server; | |||
@@ -27,28 +34,22 @@ class NativeShare extends AbstractShare { | |||
*/ | |||
private $name; | |||
/** | |||
* @var NativeState $state | |||
*/ | |||
private $state; | |||
/** @var NativeState|null $state */ | |||
private $state = null; | |||
/** | |||
* @param IServer $server | |||
* @param string $name | |||
*/ | |||
public function __construct($server, $name) { | |||
public function __construct(IServer $server, string $name) { | |||
parent::__construct(); | |||
$this->server = $server; | |||
$this->name = $name; | |||
} | |||
/** | |||
* @throws \Icewind\SMB\Exception\ConnectionException | |||
* @throws \Icewind\SMB\Exception\AuthenticationException | |||
* @throws \Icewind\SMB\Exception\InvalidHostException | |||
* @throws ConnectionException | |||
* @throws AuthenticationException | |||
* @throws InvalidHostException | |||
*/ | |||
protected function getState() { | |||
if ($this->state and $this->state instanceof NativeState) { | |||
protected function getState(): NativeState { | |||
if ($this->state) { | |||
return $this->state; | |||
} | |||
@@ -62,11 +63,11 @@ class NativeShare extends AbstractShare { | |||
* | |||
* @return string | |||
*/ | |||
public function getName() { | |||
public function getName(): string { | |||
return $this->name; | |||
} | |||
private function buildUrl($path) { | |||
private function buildUrl(string $path): string { | |||
$this->verifyPath($path); | |||
$url = sprintf('smb://%s/%s', $this->server->getHost(), $this->name); | |||
if ($path) { | |||
@@ -81,16 +82,16 @@ class NativeShare extends AbstractShare { | |||
* List the content of a remote folder | |||
* | |||
* @param string $path | |||
* @return \Icewind\SMB\IFileInfo[] | |||
* @return IFileInfo[] | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function dir($path) { | |||
public function dir(string $path): array { | |||
$files = []; | |||
$dh = $this->getState()->opendir($this->buildUrl($path)); | |||
while ($file = $this->getState()->readdir($dh)) { | |||
while ($file = $this->getState()->readdir($dh, $path)) { | |||
$name = $file['name']; | |||
if ($name !== '.' and $name !== '..') { | |||
$fullPath = $path . '/' . $name; | |||
@@ -98,15 +99,15 @@ class NativeShare extends AbstractShare { | |||
} | |||
} | |||
$this->getState()->closedir($dh); | |||
$this->getState()->closedir($dh, $path); | |||
return $files; | |||
} | |||
/** | |||
* @param string $path | |||
* @return \Icewind\SMB\IFileInfo | |||
* @return IFileInfo | |||
*/ | |||
public function stat($path) { | |||
public function stat(string $path): IFileInfo { | |||
$info = new NativeFileInfo($this, $path, self::mb_basename($path)); | |||
// trigger attribute loading | |||
@@ -119,10 +120,10 @@ class NativeShare extends AbstractShare { | |||
* Multibyte unicode safe version of basename() | |||
* | |||
* @param string $path | |||
* @link https://www.php.net/manual/en/function.basename.php#121405 | |||
* @link http://php.net/manual/en/function.basename.php#121405 | |||
* @return string | |||
*/ | |||
protected static function mb_basename($path) { | |||
protected static function mb_basename(string $path): string { | |||
if (preg_match('@^.*[\\\\/]([^\\\\/]+)$@s', $path, $matches)) { | |||
return $matches[1]; | |||
} elseif (preg_match('@^([^\\\\/]+)$@s', $path, $matches)) { | |||
@@ -138,10 +139,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function mkdir($path) { | |||
public function mkdir(string $path): bool { | |||
return $this->getState()->mkdir($this->buildUrl($path)); | |||
} | |||
@@ -151,10 +152,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function rmdir($path) { | |||
public function rmdir(string $path): bool { | |||
return $this->getState()->rmdir($this->buildUrl($path)); | |||
} | |||
@@ -164,10 +165,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function del($path) { | |||
public function del(string $path): bool { | |||
return $this->getState()->unlink($this->buildUrl($path)); | |||
} | |||
@@ -178,10 +179,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $to | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function rename($from, $to) { | |||
public function rename(string $from, string $to): bool { | |||
return $this->getState()->rename($this->buildUrl($from), $this->buildUrl($to)); | |||
} | |||
@@ -192,10 +193,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $target remove file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function put($source, $target) { | |||
public function put(string $source, string $target): bool { | |||
$sourceHandle = fopen($source, 'rb'); | |||
$targetUrl = $this->buildUrl($target); | |||
@@ -215,20 +216,18 @@ class NativeShare extends AbstractShare { | |||
* @param string $target local file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws \Icewind\SMB\Exception\InvalidPathException | |||
* @throws \Icewind\SMB\Exception\InvalidResourceException | |||
* @throws AuthenticationException | |||
* @throws ConnectionException | |||
* @throws InvalidHostException | |||
* @throws InvalidPathException | |||
* @throws InvalidResourceException | |||
*/ | |||
public function get($source, $target) { | |||
public function get(string $source, string $target): bool { | |||
if (!$target) { | |||
throw new InvalidPathException('Invalid target path: Filename cannot be empty'); | |||
} | |||
$sourceHandle = $this->getState()->open($this->buildUrl($source), 'r'); | |||
if (!$sourceHandle) { | |||
throw new InvalidResourceException('Failed opening remote file "' . $source . '" for reading'); | |||
} | |||
$targetHandle = @fopen($target, 'wb'); | |||
if (!$targetHandle) { | |||
@@ -242,7 +241,7 @@ class NativeShare extends AbstractShare { | |||
throw new InvalidResourceException('Failed opening local file "' . $target . '" for writing: ' . $reason); | |||
} | |||
while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE)) { | |||
while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE, $source)) { | |||
fwrite($targetHandle, $data); | |||
} | |||
$this->getState()->close($sourceHandle, $this->buildUrl($source)); | |||
@@ -255,10 +254,10 @@ class NativeShare extends AbstractShare { | |||
* @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 | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function read($source) { | |||
public function read(string $source) { | |||
$url = $this->buildUrl($source); | |||
$handle = $this->getState()->open($url, 'r'); | |||
return NativeReadStream::wrap($this->getState(), $handle, 'r', $url); | |||
@@ -271,10 +270,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $source | |||
* @return resource a writeable stream | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function write($source) { | |||
public function write(string $source) { | |||
$url = $this->buildUrl($source); | |||
$handle = $this->getState()->create($url); | |||
return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url); | |||
@@ -286,10 +285,10 @@ class NativeShare extends AbstractShare { | |||
* @param string $source | |||
* @return resource a writeable stream | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function append($source) { | |||
public function append(string $source) { | |||
$url = $this->buildUrl($source); | |||
$handle = $this->getState()->open($url, "a+"); | |||
return NativeWriteStream::wrap($this->getState(), $handle, "a", $url); | |||
@@ -302,7 +301,7 @@ class NativeShare extends AbstractShare { | |||
* @param string $attribute attribute to get the info | |||
* @return string the attribute value | |||
*/ | |||
public function getAttribute($path, $attribute) { | |||
public function getAttribute(string $path, string $attribute): string { | |||
return $this->getState()->getxattr($this->buildUrl($path), $attribute); | |||
} | |||
@@ -314,9 +313,13 @@ class NativeShare extends AbstractShare { | |||
* @param string|int $value | |||
* @return mixed the attribute value | |||
*/ | |||
public function setAttribute($path, $attribute, $value) { | |||
if ($attribute === 'system.dos_attr.mode' and is_int($value)) { | |||
$value = '0x' . dechex($value); | |||
public function setAttribute(string $path, string $attribute, $value) { | |||
if (is_int($value)) { | |||
if ($attribute === 'system.dos_attr.mode') { | |||
$value = '0x' . dechex($value); | |||
} else { | |||
throw new \InvalidArgumentException("Invalid value for attribute"); | |||
} | |||
} | |||
return $this->getState()->setxattr($this->buildUrl($path), $attribute, $value); | |||
@@ -329,7 +332,7 @@ class NativeShare extends AbstractShare { | |||
* @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) { | |||
public function setMode(string $path, int $mode) { | |||
return $this->setAttribute($path, 'system.dos_attr.mode', $mode); | |||
} | |||
@@ -340,7 +343,7 @@ class NativeShare extends AbstractShare { | |||
* @param string $path | |||
* @return INotifyHandler | |||
*/ | |||
public function notify($path) { | |||
public function notify(string $path): INotifyHandler { | |||
// php-smbclient does not support notify (https://github.com/eduardok/libsmbclient-php/issues/29) | |||
// so we use the smbclient based backend for this | |||
if (!Server::available($this->server->getSystem())) { |
@@ -29,13 +29,13 @@ use Icewind\SMB\IOptions; | |||
* Low level wrapper for libsmbclient-php with error handling | |||
*/ | |||
class NativeState { | |||
/** | |||
* @var resource | |||
*/ | |||
protected $state; | |||
/** @var resource|null */ | |||
protected $state = null; | |||
/** @var bool */ | |||
protected $handlerSet = false; | |||
/** @var bool */ | |||
protected $connected = false; | |||
// see error.h | |||
@@ -58,7 +58,8 @@ class NativeState { | |||
113 => NoRouteToHostException::class | |||
]; | |||
protected function handleError($path) { | |||
protected function handleError(?string $path): void { | |||
/** @var int $error */ | |||
$error = smbclient_state_errno($this->state); | |||
if ($error === 0) { | |||
return; | |||
@@ -66,14 +67,19 @@ class NativeState { | |||
throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path); | |||
} | |||
protected function testResult($result, $uri) { | |||
/** | |||
* @param mixed $result | |||
* @param string|null $uri | |||
* @throws Exception | |||
*/ | |||
protected function testResult($result, ?string $uri): void { | |||
if ($result === false or $result === null) { | |||
// smb://host/share/path | |||
if (is_string($uri) && count(explode('/', $uri, 5)) > 4) { | |||
list(, , , , $path) = explode('/', $uri, 5); | |||
$path = '/' . $path; | |||
} else { | |||
$path = null; | |||
$path = $uri; | |||
} | |||
$this->handleError($path); | |||
} | |||
@@ -88,10 +94,21 @@ class NativeState { | |||
if ($this->connected) { | |||
return true; | |||
} | |||
$this->state = smbclient_state_new(); | |||
/** @var resource $state */ | |||
$state = smbclient_state_new(); | |||
$this->state = $state; | |||
smbclient_option_set($this->state, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, false); | |||
smbclient_option_set($this->state, SMBCLIENT_OPT_TIMEOUT, $options->getTimeout() * 1000); | |||
if (function_exists('smbclient_client_protocols')) { | |||
$maxProtocol = $options->getMaxProtocol(); | |||
$minProtocol = $options->getMinProtocol(); | |||
smbclient_client_protocols($this->state, $minProtocol, $maxProtocol); | |||
} | |||
$auth->setExtraSmbClientOptions($this->state); | |||
/** @var bool $result */ | |||
$result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword()); | |||
$this->testResult($result, ''); | |||
@@ -103,7 +120,8 @@ class NativeState { | |||
* @param string $uri | |||
* @return resource | |||
*/ | |||
public function opendir($uri) { | |||
public function opendir(string $uri) { | |||
/** @var resource $result */ | |||
$result = @smbclient_opendir($this->state, $uri); | |||
$this->testResult($result, $uri); | |||
@@ -112,23 +130,27 @@ class NativeState { | |||
/** | |||
* @param resource $dir | |||
* @return array | |||
* @param string $path | |||
* @return array{"type": string, "comment": string, "name": string}|false | |||
*/ | |||
public function readdir($dir) { | |||
public function readdir($dir, string $path) { | |||
/** @var array{"type": string, "comment": string, "name": string}|false $result */ | |||
$result = @smbclient_readdir($this->state, $dir); | |||
$this->testResult($result, $dir); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
/** | |||
* @param $dir | |||
* @param resource $dir | |||
* @param string $path | |||
* @return bool | |||
*/ | |||
public function closedir($dir) { | |||
public function closedir($dir, string $path): bool { | |||
/** @var bool $result */ | |||
$result = smbclient_closedir($this->state, $dir); | |||
$this->testResult($result, $dir); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
@@ -137,7 +159,8 @@ class NativeState { | |||
* @param string $new | |||
* @return bool | |||
*/ | |||
public function rename($old, $new) { | |||
public function rename(string $old, string $new): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_rename($this->state, $old, $this->state, $new); | |||
$this->testResult($result, $new); | |||
@@ -148,7 +171,8 @@ class NativeState { | |||
* @param string $uri | |||
* @return bool | |||
*/ | |||
public function unlink($uri) { | |||
public function unlink(string $uri): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_unlink($this->state, $uri); | |||
$this->testResult($result, $uri); | |||
@@ -160,7 +184,8 @@ class NativeState { | |||
* @param int $mask | |||
* @return bool | |||
*/ | |||
public function mkdir($uri, $mask = 0777) { | |||
public function mkdir(string $uri, int $mask = 0777): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_mkdir($this->state, $uri, $mask); | |||
$this->testResult($result, $uri); | |||
@@ -171,7 +196,8 @@ class NativeState { | |||
* @param string $uri | |||
* @return bool | |||
*/ | |||
public function rmdir($uri) { | |||
public function rmdir(string $uri): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_rmdir($this->state, $uri); | |||
$this->testResult($result, $uri); | |||
@@ -180,9 +206,10 @@ class NativeState { | |||
/** | |||
* @param string $uri | |||
* @return array | |||
* @return array{"mtime": int, "size": int, "mode": int} | |||
*/ | |||
public function stat($uri) { | |||
public function stat(string $uri): array { | |||
/** @var array{"mtime": int, "size": int, "mode": int} $result */ | |||
$result = @smbclient_stat($this->state, $uri); | |||
$this->testResult($result, $uri); | |||
@@ -191,12 +218,14 @@ class NativeState { | |||
/** | |||
* @param resource $file | |||
* @return array | |||
* @param string $path | |||
* @return array{"mtime": int, "size": int, "mode": int} | |||
*/ | |||
public function fstat($file) { | |||
public function fstat($file, string $path): array { | |||
/** @var array{"mtime": int, "size": int, "mode": int} $result */ | |||
$result = @smbclient_fstat($this->state, $file); | |||
$this->testResult($result, $file); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
@@ -206,7 +235,8 @@ class NativeState { | |||
* @param int $mask | |||
* @return resource | |||
*/ | |||
public function open($uri, $mode, $mask = 0666) { | |||
public function open(string $uri, string $mode, int $mask = 0666) { | |||
/** @var resource $result */ | |||
$result = @smbclient_open($this->state, $uri, $mode, $mask); | |||
$this->testResult($result, $uri); | |||
@@ -218,7 +248,8 @@ class NativeState { | |||
* @param int $mask | |||
* @return resource | |||
*/ | |||
public function create($uri, $mask = 0666) { | |||
public function create(string $uri, int $mask = 0666) { | |||
/** @var resource $result */ | |||
$result = @smbclient_creat($this->state, $uri, $mask); | |||
$this->testResult($result, $uri); | |||
@@ -228,12 +259,14 @@ class NativeState { | |||
/** | |||
* @param resource $file | |||
* @param int $bytes | |||
* @param string $path | |||
* @return string | |||
*/ | |||
public function read($file, $bytes) { | |||
public function read($file, int $bytes, string $path): string { | |||
/** @var string $result */ | |||
$result = @smbclient_read($this->state, $file, $bytes); | |||
$this->testResult($result, $file); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
@@ -241,10 +274,11 @@ class NativeState { | |||
* @param resource $file | |||
* @param string $data | |||
* @param string $path | |||
* @param int $length | |||
* @param int|null $length | |||
* @return int | |||
*/ | |||
public function write($file, $data, $path, $length = null) { | |||
public function write($file, string $data, string $path, ?int $length = null): int { | |||
/** @var int $result */ | |||
$result = @smbclient_write($this->state, $file, $data, $length); | |||
$this->testResult($result, $path); | |||
@@ -255,28 +289,38 @@ class NativeState { | |||
* @param resource $file | |||
* @param int $offset | |||
* @param int $whence SEEK_SET | SEEK_CUR | SEEK_END | |||
* @return int|bool new file offset as measured from the start of the file on success, false on failure. | |||
* @param string|null $path | |||
* @return int|false new file offset as measured from the start of the file on success. | |||
*/ | |||
public function lseek($file, $offset, $whence = SEEK_SET) { | |||
public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) { | |||
/** @var int|false $result */ | |||
$result = @smbclient_lseek($this->state, $file, $offset, $whence); | |||
$this->testResult($result, $file); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
/** | |||
* @param resource $file | |||
* @param int $size | |||
* @param string $path | |||
* @return bool | |||
*/ | |||
public function ftruncate($file, $size) { | |||
public function ftruncate($file, int $size, string $path): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_ftruncate($this->state, $file, $size); | |||
$this->testResult($result, $file); | |||
$this->testResult($result, $path); | |||
return $result; | |||
} | |||
public function close($file, $path) { | |||
/** | |||
* @param resource $file | |||
* @param string $path | |||
* @return bool | |||
*/ | |||
public function close($file, string $path): bool { | |||
/** @var bool $result */ | |||
$result = @smbclient_close($this->state, $file); | |||
$this->testResult($result, $path); | |||
@@ -288,7 +332,8 @@ class NativeState { | |||
* @param string $key | |||
* @return string | |||
*/ | |||
public function getxattr($uri, $key) { | |||
public function getxattr(string $uri, string $key) { | |||
/** @var string $result */ | |||
$result = @smbclient_getxattr($this->state, $uri, $key); | |||
$this->testResult($result, $uri); | |||
@@ -300,9 +345,10 @@ class NativeState { | |||
* @param string $key | |||
* @param string $value | |||
* @param int $flags | |||
* @return mixed | |||
* @return bool | |||
*/ | |||
public function setxattr($uri, $key, $value, $flags = 0) { | |||
public function setxattr(string $uri, string $key, string $value, int $flags = 0) { | |||
/** @var bool $result */ | |||
$result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags); | |||
$this->testResult($result, $uri); |
@@ -10,20 +10,24 @@ namespace Icewind\SMB\Native; | |||
use Icewind\SMB\Exception\Exception; | |||
use Icewind\SMB\Exception\InvalidRequestException; | |||
use Icewind\Streams\File; | |||
use InvalidArgumentException; | |||
class NativeStream implements File { | |||
abstract class NativeStream implements File { | |||
/** | |||
* @var resource | |||
* @psalm-suppress PropertyNotSetInConstructor | |||
*/ | |||
public $context; | |||
/** | |||
* @var NativeState | |||
* @psalm-suppress PropertyNotSetInConstructor | |||
*/ | |||
protected $state; | |||
/** | |||
* @var resource | |||
* @psalm-suppress PropertyNotSetInConstructor | |||
*/ | |||
protected $handle; | |||
@@ -35,19 +39,20 @@ class NativeStream implements File { | |||
/** | |||
* @var string | |||
*/ | |||
protected $url; | |||
protected $url = ''; | |||
/** | |||
* Wrap a stream from libsmbclient-php into a regular php stream | |||
* | |||
* @param \Icewind\SMB\NativeState $state | |||
* @param NativeState $state | |||
* @param resource $smbStream | |||
* @param string $mode | |||
* @param string $url | |||
* @param class-string<NativeStream> $class | |||
* @return resource | |||
*/ | |||
public static function wrap($state, $smbStream, $mode, $url) { | |||
stream_wrapper_register('nativesmb', NativeStream::class); | |||
protected static function wrapClass(NativeState $state, $smbStream, string $mode, string $url, string $class) { | |||
stream_wrapper_register('nativesmb', $class); | |||
$context = stream_context_create([ | |||
'nativesmb' => [ | |||
'state' => $state, | |||
@@ -73,19 +78,35 @@ class NativeStream implements File { | |||
} | |||
public function stream_flush() { | |||
return false; | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$context = stream_context_get_options($this->context); | |||
$this->state = $context['nativesmb']['state']; | |||
$this->handle = $context['nativesmb']['handle']; | |||
$this->url = $context['nativesmb']['url']; | |||
if (!isset($context['nativesmb']) || !is_array($context['nativesmb'])) { | |||
throw new InvalidArgumentException("context not set"); | |||
} | |||
$state = $context['nativesmb']['state']; | |||
if (!$state instanceof NativeState) { | |||
throw new InvalidArgumentException("invalid context set"); | |||
} | |||
$this->state = $state; | |||
$handle = $context['nativesmb']['handle']; | |||
if (!is_resource($handle)) { | |||
throw new InvalidArgumentException("invalid context set"); | |||
} | |||
$this->handle = $handle; | |||
$url = $context['nativesmb']['url']; | |||
if (!is_string($url)) { | |||
throw new InvalidArgumentException("invalid context set"); | |||
} | |||
$this->url = $url; | |||
return true; | |||
} | |||
public function stream_read($count) { | |||
$result = $this->state->read($this->handle, $count); | |||
$result = $this->state->read($this->handle, $count, $this->url); | |||
if (strlen($result) < $count) { | |||
$this->eof = true; | |||
} | |||
@@ -95,12 +116,15 @@ class NativeStream implements File { | |||
public function stream_seek($offset, $whence = SEEK_SET) { | |||
$this->eof = false; | |||
try { | |||
return $this->state->lseek($this->handle, $offset, $whence) !== false; | |||
return $this->state->lseek($this->handle, $offset, $whence, $this->url) !== false; | |||
} catch (InvalidRequestException $e) { | |||
return false; | |||
} | |||
} | |||
/** | |||
* @return array{"mtime": int, "size": int, "mode": int}|false | |||
*/ | |||
public function stream_stat() { | |||
try { | |||
return $this->state->stat($this->url); | |||
@@ -110,7 +134,7 @@ class NativeStream implements File { | |||
} | |||
public function stream_tell() { | |||
return $this->state->lseek($this->handle, 0, SEEK_CUR); | |||
return $this->state->lseek($this->handle, 0, SEEK_CUR, $this->url); | |||
} | |||
public function stream_write($data) { | |||
@@ -118,7 +142,7 @@ class NativeStream implements File { | |||
} | |||
public function stream_truncate($size) { | |||
return $this->state->ftruncate($this->handle, $size); | |||
return $this->state->ftruncate($this->handle, $size, $this->url); | |||
} | |||
public function stream_set_option($option, $arg1, $arg2) { |
@@ -7,71 +7,63 @@ | |||
namespace Icewind\SMB\Native; | |||
use Icewind\SMB\StringBuffer; | |||
/** | |||
* Stream optimized for write only usage | |||
*/ | |||
class NativeWriteStream extends NativeStream { | |||
const CHUNK_SIZE = 1048576; // 1MB chunks | |||
/** | |||
* @var resource | |||
*/ | |||
private $writeBuffer = null; | |||
private $bufferSize = 0; | |||
/** @var StringBuffer */ | |||
private $writeBuffer; | |||
/** @var int */ | |||
private $pos = 0; | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$this->writeBuffer = fopen('php://memory', 'r+'); | |||
public function __construct() { | |||
$this->writeBuffer = new StringBuffer(); | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path): bool { | |||
return parent::stream_open($path, $mode, $options, $opened_path); | |||
} | |||
/** | |||
* Wrap a stream from libsmbclient-php into a regular php stream | |||
* | |||
* @param \Icewind\SMB\NativeState $state | |||
* @param NativeState $state | |||
* @param resource $smbStream | |||
* @param string $mode | |||
* @param string $url | |||
* @return resource | |||
*/ | |||
public static function wrap($state, $smbStream, $mode, $url) { | |||
stream_wrapper_register('nativesmb', NativeWriteStream::class); | |||
$context = stream_context_create([ | |||
'nativesmb' => [ | |||
'state' => $state, | |||
'handle' => $smbStream, | |||
'url' => $url | |||
] | |||
]); | |||
$fh = fopen('nativesmb://', $mode, false, $context); | |||
stream_wrapper_unregister('nativesmb'); | |||
return $fh; | |||
public static function wrap(NativeState $state, $smbStream, string $mode, string $url) { | |||
return parent::wrapClass($state, $smbStream, $mode, $url, NativeWriteStream::class); | |||
} | |||
public function stream_seek($offset, $whence = SEEK_SET) { | |||
$this->flushWrite(); | |||
$result = parent::stream_seek($offset, $whence); | |||
if ($result) { | |||
$this->pos = parent::stream_tell(); | |||
$pos = parent::stream_tell(); | |||
if ($pos === false) { | |||
return false; | |||
} | |||
$this->pos = $pos; | |||
} | |||
return $result; | |||
} | |||
private function flushWrite() { | |||
rewind($this->writeBuffer); | |||
$this->state->write($this->handle, stream_get_contents($this->writeBuffer), $this->url); | |||
$this->writeBuffer = fopen('php://memory', 'r+'); | |||
$this->bufferSize = 0; | |||
private function flushWrite(): void { | |||
parent::stream_write($this->writeBuffer->flush()); | |||
} | |||
public function stream_write($data) { | |||
$written = fwrite($this->writeBuffer, $data); | |||
$this->bufferSize += $written; | |||
$written = $this->writeBuffer->push($data); | |||
$this->pos += $written; | |||
if ($this->bufferSize >= self::CHUNK_SIZE) { | |||
if ($this->writeBuffer->remaining() >= self::CHUNK_SIZE) { | |||
$this->flushWrite(); | |||
} | |||
@@ -25,11 +25,32 @@ class Options implements IOptions { | |||
/** @var int */ | |||
private $timeout = 20; | |||
public function getTimeout() { | |||
/** @var string|null */ | |||
private $minProtocol; | |||
/** @var string|null */ | |||
private $maxProtocol; | |||
public function getTimeout(): int { | |||
return $this->timeout; | |||
} | |||
public function setTimeout($timeout) { | |||
public function setTimeout(int $timeout): void { | |||
$this->timeout = $timeout; | |||
} | |||
public function getMinProtocol(): ?string { | |||
return $this->minProtocol; | |||
} | |||
public function setMinProtocol(?string $minProtocol): void { | |||
$this->minProtocol = $minProtocol; | |||
} | |||
public function getMaxProtocol(): ?string { | |||
return $this->maxProtocol; | |||
} | |||
public function setMaxProtocol(?string $maxProtocol): void { | |||
$this->maxProtocol = $maxProtocol; | |||
} | |||
} |
@@ -31,7 +31,7 @@ class ServerFactory { | |||
Server::class | |||
]; | |||
/** @var System */ | |||
/** @var ISystem */ | |||
private $system; | |||
/** @var IOptions */ | |||
@@ -68,12 +68,12 @@ class ServerFactory { | |||
/** | |||
* @param $host | |||
* @param string $host | |||
* @param IAuth $credentials | |||
* @return IServer | |||
* @throws DependencyException | |||
*/ | |||
public function createServer($host, IAuth $credentials) { | |||
public function createServer(string $host, IAuth $credentials): IServer { | |||
foreach (self::BACKENDS as $backend) { | |||
if (call_user_func("$backend::available", $this->system)) { | |||
return new $backend($host, $credentials, $this->system, $this->timeZoneProvider, $this->options); |
@@ -0,0 +1,63 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
namespace Icewind\SMB; | |||
class StringBuffer { | |||
/** @var string */ | |||
private $buffer = ""; | |||
/** @var int */ | |||
private $pos = 0; | |||
public function clear(): void { | |||
$this->buffer = ""; | |||
$this->pos = 0; | |||
} | |||
public function push(string $data): int { | |||
$this->buffer = $this->flush() . $data; | |||
return strlen($data); | |||
} | |||
public function remaining(): int { | |||
return strlen($this->buffer) - $this->pos; | |||
} | |||
public function read(int $count): string { | |||
$chunk = substr($this->buffer, $this->pos, $this->pos + $count); | |||
$this->pos += strlen($chunk); | |||
return $chunk; | |||
} | |||
public function flush(): string { | |||
if ($this->pos === 0) { | |||
$remaining = $this->buffer; | |||
} else { | |||
$remaining = substr($this->buffer, $this->pos); | |||
} | |||
$this->clear(); | |||
return $remaining; | |||
} | |||
} |
@@ -10,7 +10,7 @@ namespace Icewind\SMB; | |||
use Icewind\SMB\Exception\Exception; | |||
class System implements ISystem { | |||
/** @var (string|bool)[] */ | |||
/** @var (string|null)[] */ | |||
private $paths = []; | |||
/** | |||
@@ -20,7 +20,7 @@ class System implements ISystem { | |||
* @return string | |||
* @throws Exception | |||
*/ | |||
public function getFD($num) { | |||
public function getFD(int $num): string { | |||
$folders = [ | |||
'/proc/self/fd', | |||
'/dev/fd' | |||
@@ -33,36 +33,36 @@ class System implements ISystem { | |||
throw new Exception('Cant find file descriptor path'); | |||
} | |||
public function getSmbclientPath() { | |||
public function getSmbclientPath(): ?string { | |||
return $this->getBinaryPath('smbclient'); | |||
} | |||
public function getNetPath() { | |||
public function getNetPath(): ?string { | |||
return $this->getBinaryPath('net'); | |||
} | |||
public function getSmbcAclsPath() { | |||
public function getSmbcAclsPath(): ?string { | |||
return $this->getBinaryPath('smbcacls'); | |||
} | |||
public function getStdBufPath() { | |||
public function getStdBufPath(): ?string { | |||
return $this->getBinaryPath('stdbuf'); | |||
} | |||
public function getDatePath() { | |||
public function getDatePath(): ?string { | |||
return $this->getBinaryPath('date'); | |||
} | |||
public function libSmbclientAvailable() { | |||
public function libSmbclientAvailable(): bool { | |||
return function_exists('smbclient_state_new'); | |||
} | |||
protected function getBinaryPath($binary) { | |||
protected function getBinaryPath(string $binary): ?string { | |||
if (!isset($this->paths[$binary])) { | |||
$result = null; | |||
$output = []; | |||
exec("which $binary 2>&1", $output, $result); | |||
$this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : false; | |||
$this->paths[$binary] = $result === 0 ? trim(implode('', $output)) : null; | |||
} | |||
return $this->paths[$binary]; | |||
} |
@@ -25,7 +25,7 @@ class TimeZoneProvider implements ITimeZoneProvider { | |||
$this->system = $system; | |||
} | |||
public function get($host) { | |||
public function get(string $host): string { | |||
if (!isset($this->timeZones[$host])) { | |||
$timeZone = null; | |||
$net = $this->system->getNetPath(); |
@@ -7,9 +7,11 @@ | |||
namespace Icewind\SMB\Wrapped; | |||
use Icewind\SMB\Exception\AccessDeniedException; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
use Icewind\SMB\Exception\ConnectException; | |||
use Icewind\SMB\Exception\ConnectionException; | |||
use Icewind\SMB\Exception\ConnectionRefusedException; | |||
use Icewind\SMB\Exception\InvalidHostException; | |||
use Icewind\SMB\Exception\NoLoginServerException; | |||
@@ -20,7 +22,12 @@ class Connection extends RawConnection { | |||
/** @var Parser */ | |||
private $parser; | |||
public function __construct($command, Parser $parser, $env = []) { | |||
/** | |||
* @param string $command | |||
* @param Parser $parser | |||
* @param array<string, string> $env | |||
*/ | |||
public function __construct(string $command, Parser $parser, array $env = []) { | |||
parent::__construct($command, $env); | |||
$this->parser = $parser; | |||
} | |||
@@ -30,39 +37,48 @@ class Connection extends RawConnection { | |||
* | |||
* @param string $input | |||
*/ | |||
public function write($input) { | |||
parent::write($input . PHP_EOL); | |||
public function write(string $input) { | |||
return parent::write($input . PHP_EOL); | |||
} | |||
/** | |||
* @throws ConnectException | |||
*/ | |||
public function clearTillPrompt() { | |||
public function clearTillPrompt(): void { | |||
$this->write(''); | |||
do { | |||
$promptLine = $this->readLine(); | |||
if ($promptLine === false) { | |||
break; | |||
} | |||
$this->parser->checkConnectionError($promptLine); | |||
} while (!$this->isPrompt($promptLine)); | |||
$this->write(''); | |||
if ($this->write('') === false) { | |||
throw new ConnectionRefusedException(); | |||
} | |||
$this->readLine(); | |||
} | |||
/** | |||
* get all unprocessed output from smbclient until the next prompt | |||
* | |||
* @param callable $callback (optional) callback to call for every line read | |||
* @param (callable(string):bool)|null $callback (optional) callback to call for every line read | |||
* @return string[] | |||
* @throws AuthenticationException | |||
* @throws ConnectException | |||
* @throws ConnectionException | |||
* @throws InvalidHostException | |||
* @throws NoLoginServerException | |||
* @throws AccessDeniedException | |||
*/ | |||
public function read(callable $callback = null) { | |||
public function read(callable $callback = null): array { | |||
if (!$this->isValid()) { | |||
throw new ConnectionException('Connection not valid'); | |||
} | |||
$promptLine = $this->readLine(); //first line is prompt | |||
if ($promptLine === false) { | |||
$this->unknownError($promptLine); | |||
} | |||
$this->parser->checkConnectionError($promptLine); | |||
$output = []; | |||
@@ -74,7 +90,7 @@ class Connection extends RawConnection { | |||
if ($line === false) { | |||
$this->unknownError($promptLine); | |||
} | |||
while (!$this->isPrompt($line)) { //next prompt functions as delimiter | |||
while ($line !== false && !$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 | |||
@@ -82,26 +98,21 @@ class Connection extends RawConnection { | |||
break; | |||
} | |||
} else { | |||
$output[] .= $line; | |||
$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; | |||
private function isPrompt(string $line): bool { | |||
return mb_substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER; | |||
} | |||
/** | |||
* @param string $promptLine (optional) prompt line that might contain some info about the error | |||
* @param string|bool $promptLine (optional) prompt line that might contain some info about the error | |||
* @throws ConnectException | |||
* @return no-return | |||
*/ | |||
private function unknownError($promptLine = '') { | |||
if ($promptLine) { //maybe we have some error we missed on the previous line | |||
@@ -116,7 +127,7 @@ class Connection extends RawConnection { | |||
} | |||
} | |||
public function close($terminate = true) { | |||
public function close(bool $terminate = true): void { | |||
if (get_resource_type($this->getInputStream()) === 'stream') { | |||
// ignore any errors while trying to send the close command, the process might already be dead | |||
@$this->write('close' . PHP_EOL); |
@@ -11,34 +11,17 @@ use Icewind\SMB\ACL; | |||
use Icewind\SMB\IFileInfo; | |||
class FileInfo implements IFileInfo { | |||
/** | |||
* @var string | |||
*/ | |||
/** @var string */ | |||
protected $path; | |||
/** | |||
* @var string | |||
*/ | |||
/** @var string */ | |||
protected $name; | |||
/** | |||
* @var int | |||
*/ | |||
/** @var int */ | |||
protected $size; | |||
/** | |||
* @var int | |||
*/ | |||
/** @var int */ | |||
protected $time; | |||
/** | |||
* @var int | |||
*/ | |||
/** @var int */ | |||
protected $mode; | |||
/** | |||
* @var callable | |||
*/ | |||
/** @var callable(): ACL[] */ | |||
protected $aclCallback; | |||
/** | |||
@@ -47,9 +30,9 @@ class FileInfo implements IFileInfo { | |||
* @param int $size | |||
* @param int $time | |||
* @param int $mode | |||
* @param callable $aclCallback | |||
* @param callable(): ACL[] $aclCallback | |||
*/ | |||
public function __construct($path, $name, $size, $time, $mode, callable $aclCallback) { | |||
public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) { | |||
$this->path = $path; | |||
$this->name = $name; | |||
$this->size = $size; | |||
@@ -61,63 +44,39 @@ class FileInfo implements IFileInfo { | |||
/** | |||
* @return string | |||
*/ | |||
public function getPath() { | |||
public function getPath(): string { | |||
return $this->path; | |||
} | |||
/** | |||
* @return string | |||
*/ | |||
public function getName() { | |||
public function getName(): string { | |||
return $this->name; | |||
} | |||
/** | |||
* @return int | |||
*/ | |||
public function getSize() { | |||
public function getSize(): int { | |||
return $this->size; | |||
} | |||
/** | |||
* @return int | |||
*/ | |||
public function getMTime() { | |||
public function getMTime(): int { | |||
return $this->time; | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isDirectory() { | |||
public function isDirectory(): bool { | |||
return (bool)($this->mode & IFileInfo::MODE_DIRECTORY); | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isReadOnly() { | |||
public function isReadOnly(): bool { | |||
return (bool)($this->mode & IFileInfo::MODE_READONLY); | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isHidden() { | |||
public function isHidden(): bool { | |||
return (bool)($this->mode & IFileInfo::MODE_HIDDEN); | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isSystem() { | |||
public function isSystem(): bool { | |||
return (bool)($this->mode & IFileInfo::MODE_SYSTEM); | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
public function isArchived() { | |||
public function isArchived(): bool { | |||
return (bool)($this->mode & IFileInfo::MODE_ARCHIVE); | |||
} | |||
@@ -14,16 +14,13 @@ use Icewind\SMB\Exception\RevisionMismatchException; | |||
use Icewind\SMB\INotifyHandler; | |||
class NotifyHandler implements INotifyHandler { | |||
/** | |||
* @var Connection | |||
*/ | |||
/** @var Connection */ | |||
private $connection; | |||
/** | |||
* @var string | |||
*/ | |||
/** @var string */ | |||
private $path; | |||
/** @var bool */ | |||
private $listening = true; | |||
// see error.h | |||
@@ -35,7 +32,7 @@ class NotifyHandler implements INotifyHandler { | |||
* @param Connection $connection | |||
* @param string $path | |||
*/ | |||
public function __construct(Connection $connection, $path) { | |||
public function __construct(Connection $connection, string $path) { | |||
$this->connection = $connection; | |||
$this->path = $path; | |||
} | |||
@@ -45,17 +42,17 @@ class NotifyHandler implements INotifyHandler { | |||
* | |||
* @return Change[] | |||
*/ | |||
public function getChanges() { | |||
public function getChanges(): array { | |||
if (!$this->listening) { | |||
return []; | |||
} | |||
stream_set_blocking($this->connection->getOutputStream(), 0); | |||
stream_set_blocking($this->connection->getOutputStream(), false); | |||
$lines = []; | |||
while (($line = $this->connection->readLine())) { | |||
$this->checkForError($line); | |||
$lines[] = $line; | |||
} | |||
stream_set_blocking($this->connection->getOutputStream(), 1); | |||
stream_set_blocking($this->connection->getOutputStream(), true); | |||
return array_values(array_filter(array_map([$this, 'parseChangeLine'], $lines))); | |||
} | |||
@@ -64,21 +61,24 @@ class NotifyHandler implements INotifyHandler { | |||
* | |||
* Note that this is a blocking process and will cause the process to block forever if not explicitly terminated | |||
* | |||
* @param callable $callback | |||
* @param callable(Change):?bool $callback | |||
*/ | |||
public function listen($callback) { | |||
public function listen(callable $callback): void { | |||
if ($this->listening) { | |||
$this->connection->read(function ($line) use ($callback) { | |||
$this->connection->read(function (string $line) use ($callback): bool { | |||
$this->checkForError($line); | |||
$change = $this->parseChangeLine($line); | |||
if ($change) { | |||
return $callback($change); | |||
$result = $callback($change); | |||
return $result === false ? false : true; | |||
} else { | |||
return true; | |||
} | |||
}); | |||
} | |||
} | |||
private function parseChangeLine($line) { | |||
private function parseChangeLine(string $line): ?Change { | |||
$code = (int)substr($line, 0, 4); | |||
if ($code === 0) { | |||
return null; | |||
@@ -91,14 +91,14 @@ class NotifyHandler implements INotifyHandler { | |||
} | |||
} | |||
private function checkForError($line) { | |||
private function checkForError(string $line): void { | |||
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() { | |||
public function stop(): void { | |||
$this->listening = false; | |||
$this->connection->close(); | |||
} |
@@ -7,6 +7,7 @@ | |||
namespace Icewind\SMB\Wrapped; | |||
use Icewind\SMB\ACL; | |||
use Icewind\SMB\Exception\AccessDeniedException; | |||
use Icewind\SMB\Exception\AlreadyExistsException; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
@@ -28,11 +29,6 @@ class Parser { | |||
*/ | |||
protected $timeZone; | |||
/** | |||
* @var string | |||
*/ | |||
private $host; | |||
// see error.h | |||
const EXCEPTION_MAP = [ | |||
ErrorCodes::LogonFailure => AuthenticationException::class, | |||
@@ -60,21 +56,29 @@ class Parser { | |||
/** | |||
* @param string $timeZone | |||
*/ | |||
public function __construct($timeZone) { | |||
public function __construct(string $timeZone) { | |||
$this->timeZone = $timeZone; | |||
} | |||
private function getErrorCode($line) { | |||
private function getErrorCode(string $line): ?string { | |||
$parts = explode(' ', $line); | |||
foreach ($parts as $part) { | |||
if (substr($part, 0, 9) === 'NT_STATUS') { | |||
return $part; | |||
} | |||
} | |||
return false; | |||
return null; | |||
} | |||
public function checkForError($output, $path) { | |||
/** | |||
* @param string[] $output | |||
* @param string $path | |||
* @return no-return | |||
* @throws Exception | |||
* @throws InvalidResourceException | |||
* @throws NotFoundException | |||
*/ | |||
public function checkForError(array $output, string $path): void { | |||
if (strpos($output[0], 'does not exist')) { | |||
throw new NotFoundException($path); | |||
} | |||
@@ -91,13 +95,13 @@ class Parser { | |||
/** | |||
* check if the first line holds a connection failure | |||
* | |||
* @param $line | |||
* @param string $line | |||
* @throws AuthenticationException | |||
* @throws InvalidHostException | |||
* @throws NoLoginServerException | |||
* @throws AccessDeniedException | |||
*/ | |||
public function checkConnectionError($line) { | |||
public function checkConnectionError(string $line): void { | |||
$line = rtrim($line, ')'); | |||
if (substr($line, -23) === ErrorCodes::LogonFailure) { | |||
throw new AuthenticationException('Invalid login'); | |||
@@ -119,7 +123,7 @@ class Parser { | |||
} | |||
} | |||
public function parseMode($mode) { | |||
public function parseMode(string $mode): int { | |||
$result = 0; | |||
foreach (self::MODE_STRINGS as $char => $val) { | |||
if (strpos($mode, $char) !== false) { | |||
@@ -129,7 +133,12 @@ class Parser { | |||
return $result; | |||
} | |||
public function parseStat($output) { | |||
/** | |||
* @param string[] $output | |||
* @return array{"mtime": int, "mode": int, "size": int} | |||
* @throws Exception | |||
*/ | |||
public function parseStat(array $output): array { | |||
$data = []; | |||
foreach ($output as $line) { | |||
// A line = explode statement may not fill all array elements | |||
@@ -143,14 +152,24 @@ class Parser { | |||
$data[$name] = $value; | |||
} | |||
} | |||
$attributeStart = strpos($data['attributes'], '('); | |||
if ($attributeStart === false) { | |||
throw new Exception("Malformed state response from server"); | |||
} | |||
return [ | |||
'mtime' => strtotime($data['write_time']), | |||
'mode' => hexdec(substr($data['attributes'], strpos($data['attributes'], '(') + 1, -1)), | |||
'mode' => hexdec(substr($data['attributes'], $attributeStart + 1, -1)), | |||
'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0 | |||
]; | |||
} | |||
public function parseDir($output, $basePath, callable $aclCallback) { | |||
/** | |||
* @param string[] $output | |||
* @param string $basePath | |||
* @param callable(string):ACL[] $aclCallback | |||
* @return FileInfo[] | |||
*/ | |||
public function parseDir(array $output, string $basePath, callable $aclCallback): array { | |||
//last line is used space | |||
array_pop($output); | |||
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/'; | |||
@@ -163,7 +182,7 @@ class Parser { | |||
$mode = $this->parseMode($mode); | |||
$time = strtotime($time . ' ' . $this->timeZone); | |||
$path = $basePath . '/' . $name; | |||
$content[] = new FileInfo($path, $name, $size, $time, $mode, function () use ($aclCallback, $path) { | |||
$content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array { | |||
return $aclCallback($path); | |||
}); | |||
} | |||
@@ -172,7 +191,11 @@ class Parser { | |||
return $content; | |||
} | |||
public function parseListShares($output) { | |||
/** | |||
* @param string[] $output | |||
* @return array<string, string> | |||
*/ | |||
public function parseListShares(array $output): array { | |||
$shareNames = []; | |||
foreach ($output as $line) { | |||
if (strpos($line, '|')) { | |||
@@ -188,4 +211,67 @@ class Parser { | |||
} | |||
return $shareNames; | |||
} | |||
/** | |||
* @param string[] $rawAcls | |||
* @return ACL[] | |||
*/ | |||
public function parseACLs(array $rawAcls): array { | |||
$acls = []; | |||
foreach ($rawAcls as $acl) { | |||
if (strpos($acl, ':') === false) { | |||
continue; | |||
} | |||
[$type, $acl] = explode(':', $acl, 2); | |||
if ($type !== 'ACL') { | |||
continue; | |||
} | |||
[$user, $permissions] = explode(':', $acl, 2); | |||
[$type, $flags, $mask] = explode('/', $permissions); | |||
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY; | |||
$flagsInt = 0; | |||
foreach (explode('|', $flags) as $flagString) { | |||
if ($flagString === 'OI') { | |||
$flagsInt += ACL::FLAG_OBJECT_INHERIT; | |||
} elseif ($flagString === 'CI') { | |||
$flagsInt += ACL::FLAG_CONTAINER_INHERIT; | |||
} | |||
} | |||
if (substr($mask, 0, 2) === '0x') { | |||
$maskInt = hexdec($mask); | |||
} else { | |||
$maskInt = 0; | |||
foreach (explode('|', $mask) as $maskString) { | |||
if ($maskString === 'R') { | |||
$maskInt += ACL::MASK_READ; | |||
} elseif ($maskString === 'W') { | |||
$maskInt += ACL::MASK_WRITE; | |||
} elseif ($maskString === 'X') { | |||
$maskInt += ACL::MASK_EXECUTE; | |||
} elseif ($maskString === 'D') { | |||
$maskInt += ACL::MASK_DELETE; | |||
} elseif ($maskString === 'READ') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE; | |||
} elseif ($maskString === 'CHANGE') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; | |||
} elseif ($maskString === 'FULL') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; | |||
} | |||
} | |||
} | |||
if (isset($acls[$user])) { | |||
$existing = $acls[$user]; | |||
$maskInt += $existing->getMask(); | |||
} | |||
$acls[$user] = new ACL($type, $flagsInt, $maskInt); | |||
} | |||
ksort($acls); | |||
return $acls; | |||
} | |||
} |
@@ -30,10 +30,10 @@ class RawConnection { | |||
* $pipes[4] holds the stream for writing files | |||
* $pipes[5] holds the stream for reading files | |||
*/ | |||
private $pipes; | |||
private $pipes = []; | |||
/** | |||
* @var resource $process | |||
* @var resource|null $process | |||
*/ | |||
private $process; | |||
@@ -42,17 +42,20 @@ class RawConnection { | |||
*/ | |||
private $authStream = null; | |||
private $connected = false; | |||
public function __construct($command, array $env = []) { | |||
/** | |||
* @param string $command | |||
* @param array<string, string> $env | |||
*/ | |||
public function __construct(string $command, array $env = []) { | |||
$this->command = $command; | |||
$this->env = $env; | |||
} | |||
/** | |||
* @throws ConnectException | |||
* @psalm-assert resource $this->process | |||
*/ | |||
public function connect() { | |||
public function connect(): void { | |||
if (is_null($this->getAuthStream())) { | |||
throw new ConnectException('Authentication not set before connecting'); | |||
} | |||
@@ -77,18 +80,18 @@ class RawConnection { | |||
if (!$this->isValid()) { | |||
throw new ConnectionException(); | |||
} | |||
$this->connected = true; | |||
} | |||
/** | |||
* check if the connection is still active | |||
* | |||
* @return bool | |||
* @psalm-assert-if-true resource $this->process | |||
*/ | |||
public function isValid() { | |||
public function isValid(): bool { | |||
if (is_resource($this->process)) { | |||
$status = proc_get_status($this->process); | |||
return $status['running']; | |||
return (bool)$status['running']; | |||
} else { | |||
return false; | |||
} | |||
@@ -98,10 +101,12 @@ class RawConnection { | |||
* send input to the process | |||
* | |||
* @param string $input | |||
* @return int|bool | |||
*/ | |||
public function write($input) { | |||
fwrite($this->getInputStream(), $input); | |||
public function write(string $input) { | |||
$result = @fwrite($this->getInputStream(), $input); | |||
fflush($this->getInputStream()); | |||
return $result; | |||
} | |||
/** | |||
@@ -116,18 +121,19 @@ class RawConnection { | |||
/** | |||
* read a line of output | |||
* | |||
* @return string | |||
* @return string|false | |||
*/ | |||
public function readError() { | |||
return trim(stream_get_line($this->getErrorStream(), 4086)); | |||
$line = stream_get_line($this->getErrorStream(), 4086); | |||
return $line !== false ? trim($line) : false; | |||
} | |||
/** | |||
* get all output until the process closes | |||
* | |||
* @return array | |||
* @return string[] | |||
*/ | |||
public function readAll() { | |||
public function readAll(): array { | |||
$output = []; | |||
while ($line = $this->readLine()) { | |||
$output[] = $line; | |||
@@ -135,40 +141,67 @@ class RawConnection { | |||
return $output; | |||
} | |||
/** | |||
* @return resource | |||
*/ | |||
public function getInputStream() { | |||
return $this->pipes[0]; | |||
} | |||
/** | |||
* @return resource | |||
*/ | |||
public function getOutputStream() { | |||
return $this->pipes[1]; | |||
} | |||
/** | |||
* @return resource | |||
*/ | |||
public function getErrorStream() { | |||
return $this->pipes[2]; | |||
} | |||
/** | |||
* @return resource|null | |||
*/ | |||
public function getAuthStream() { | |||
return $this->authStream; | |||
} | |||
/** | |||
* @return resource | |||
*/ | |||
public function getFileInputStream() { | |||
return $this->pipes[4]; | |||
} | |||
/** | |||
* @return resource | |||
*/ | |||
public function getFileOutputStream() { | |||
return $this->pipes[5]; | |||
} | |||
public function writeAuthentication($user, $password) { | |||
$auth = ($password === false) | |||
/** | |||
* @param string|null $user | |||
* @param string|null $password | |||
* @psalm-assert resource $this->authStream | |||
*/ | |||
public function writeAuthentication(?string $user, ?string $password): void { | |||
$auth = ($password === null) | |||
? "username=$user" | |||
: "username=$user\npassword=$password\n"; | |||
$this->authStream = fopen('php://temp', 'w+'); | |||
fwrite($this->getAuthStream(), $auth); | |||
fwrite($this->authStream, $auth); | |||
} | |||
public function close($terminate = true) { | |||
/** | |||
* @param bool $terminate | |||
* @psalm-assert null $this->process | |||
*/ | |||
public function close(bool $terminate = true): void { | |||
if (!is_resource($this->process)) { | |||
return; | |||
} | |||
@@ -176,9 +209,10 @@ class RawConnection { | |||
proc_terminate($this->process); | |||
} | |||
proc_close($this->process); | |||
$this->process = null; | |||
} | |||
public function reconnect() { | |||
public function reconnect(): void { | |||
$this->close(); | |||
$this->connect(); | |||
} |
@@ -11,6 +11,8 @@ use Icewind\SMB\AbstractServer; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
use Icewind\SMB\Exception\ConnectException; | |||
use Icewind\SMB\Exception\ConnectionException; | |||
use Icewind\SMB\Exception\ConnectionRefusedException; | |||
use Icewind\SMB\Exception\Exception; | |||
use Icewind\SMB\Exception\InvalidHostException; | |||
use Icewind\SMB\IShare; | |||
use Icewind\SMB\ISystem; | |||
@@ -22,11 +24,11 @@ class Server extends AbstractServer { | |||
* @param ISystem $system | |||
* @return bool | |||
*/ | |||
public static function available(ISystem $system) { | |||
return $system->getSmbclientPath(); | |||
public static function available(ISystem $system): bool { | |||
return $system->getSmbclientPath() !== null; | |||
} | |||
private function getAuthFileArgument() { | |||
private function getAuthFileArgument(): string { | |||
if ($this->getAuth()->getUsername()) { | |||
return '--authentication-file=' . $this->system->getFD(3); | |||
} else { | |||
@@ -41,22 +43,30 @@ class Server extends AbstractServer { | |||
* @throws InvalidHostException | |||
* @throws ConnectException | |||
*/ | |||
public function listShares() { | |||
public function listShares(): array { | |||
$maxProtocol = $this->options->getMaxProtocol(); | |||
$minProtocol = $this->options->getMinProtocol(); | |||
$smbClient = $this->system->getSmbclientPath(); | |||
if ($smbClient === null) { | |||
throw new Exception("Backend not available"); | |||
} | |||
$command = sprintf( | |||
'%s %s %s -L %s', | |||
$this->system->getSmbclientPath(), | |||
'%s %s %s %s %s -L %s', | |||
$smbClient, | |||
$this->getAuthFileArgument(), | |||
$this->getAuth()->getExtraCommandLineArguments(), | |||
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "", | |||
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "", | |||
escapeshellarg('//' . $this->getHost()) | |||
); | |||
$connection = new RawConnection($command); | |||
$connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword()); | |||
$connection->connect(); | |||
if (!$connection->isValid()) { | |||
throw new ConnectionException($connection->readLine()); | |||
throw new ConnectionException((string)$connection->readLine()); | |||
} | |||
$parser = new Parser($this->timezoneProvider); | |||
$parser = new Parser($this->timezoneProvider->get($this->host)); | |||
$output = $connection->readAll(); | |||
if (isset($output[0])) { | |||
@@ -71,6 +81,9 @@ class Server extends AbstractServer { | |||
if (isset($output[0])) { | |||
$parser->checkConnectionError($output[0]); | |||
} | |||
if (count($output) === 0) { | |||
throw new ConnectionRefusedException(); | |||
} | |||
$shareNames = $parser->parseListShares($output); | |||
@@ -85,7 +98,7 @@ class Server extends AbstractServer { | |||
* @param string $name | |||
* @return IShare | |||
*/ | |||
public function getShare($name) { | |||
public function getShare(string $name): IShare { | |||
return new Share($this, $name, $this->system); | |||
} | |||
} |
@@ -9,9 +9,14 @@ namespace Icewind\SMB\Wrapped; | |||
use Icewind\SMB\AbstractShare; | |||
use Icewind\SMB\ACL; | |||
use Icewind\SMB\Exception\AlreadyExistsException; | |||
use Icewind\SMB\Exception\AuthenticationException; | |||
use Icewind\SMB\Exception\ConnectException; | |||
use Icewind\SMB\Exception\ConnectionException; | |||
use Icewind\SMB\Exception\DependencyException; | |||
use Icewind\SMB\Exception\Exception; | |||
use Icewind\SMB\Exception\FileInUseException; | |||
use Icewind\SMB\Exception\InvalidHostException; | |||
use Icewind\SMB\Exception\InvalidTypeException; | |||
use Icewind\SMB\Exception\NotFoundException; | |||
use Icewind\SMB\Exception\InvalidRequestException; | |||
@@ -35,9 +40,9 @@ class Share extends AbstractShare { | |||
private $name; | |||
/** | |||
* @var Connection $connection | |||
* @var Connection|null $connection | |||
*/ | |||
public $connection; | |||
public $connection = null; | |||
/** | |||
* @var Parser | |||
@@ -63,7 +68,7 @@ class Share extends AbstractShare { | |||
* @param string $name | |||
* @param ISystem $system | |||
*/ | |||
public function __construct(IServer $server, $name, ISystem $system) { | |||
public function __construct(IServer $server, string $name, ISystem $system) { | |||
parent::__construct(); | |||
$this->server = $server; | |||
$this->name = $name; | |||
@@ -71,7 +76,7 @@ class Share extends AbstractShare { | |||
$this->parser = new Parser($server->getTimeZone()); | |||
} | |||
private function getAuthFileArgument() { | |||
private function getAuthFileArgument(): string { | |||
if ($this->server->getAuth()->getUsername()) { | |||
return '--authentication-file=' . $this->system->getFD(3); | |||
} else { | |||
@@ -79,22 +84,31 @@ class Share extends AbstractShare { | |||
} | |||
} | |||
protected function getConnection() { | |||
protected function getConnection(): Connection { | |||
$maxProtocol = $this->server->getOptions()->getMaxProtocol(); | |||
$minProtocol = $this->server->getOptions()->getMinProtocol(); | |||
$smbClient = $this->system->getSmbclientPath(); | |||
$stdBuf = $this->system->getStdBufPath(); | |||
if ($smbClient === null) { | |||
throw new Exception("Backend not available"); | |||
} | |||
$command = sprintf( | |||
'%s %s%s -t %s %s %s %s', | |||
'%s %s%s -t %s %s %s %s %s %s', | |||
self::EXEC_CMD, | |||
$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '', | |||
$this->system->getSmbclientPath(), | |||
$stdBuf ? $stdBuf . ' -o0 ' : '', | |||
$smbClient, | |||
$this->server->getOptions()->getTimeout(), | |||
$this->getAuthFileArgument(), | |||
$this->server->getAuth()->getExtraCommandLineArguments(), | |||
$maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "", | |||
$minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "", | |||
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()); | |||
throw new ConnectionException((string)$connection->readLine()); | |||
} | |||
// some versions of smbclient add a help message in first of the first prompt | |||
$connection->clearTillPrompt(); | |||
@@ -102,21 +116,33 @@ class Share extends AbstractShare { | |||
} | |||
/** | |||
* @throws \Icewind\SMB\Exception\ConnectionException | |||
* @throws \Icewind\SMB\Exception\AuthenticationException | |||
* @throws \Icewind\SMB\Exception\InvalidHostException | |||
* @throws ConnectionException | |||
* @throws AuthenticationException | |||
* @throws InvalidHostException | |||
* @psalm-assert Connection $this->connection | |||
*/ | |||
protected function connect() { | |||
protected function connect(): Connection { | |||
if ($this->connection and $this->connection->isValid()) { | |||
return; | |||
return $this->connection; | |||
} | |||
$this->connection = $this->getConnection(); | |||
return $this->connection; | |||
} | |||
protected function reconnect() { | |||
$this->connection->reconnect(); | |||
if (!$this->connection->isValid()) { | |||
throw new ConnectionException(); | |||
/** | |||
* @throws ConnectionException | |||
* @throws AuthenticationException | |||
* @throws InvalidHostException | |||
* @psalm-assert Connection $this->connection | |||
*/ | |||
protected function reconnect(): void { | |||
if ($this->connection === null) { | |||
$this->connect(); | |||
} else { | |||
$this->connection->reconnect(); | |||
if (!$this->connection->isValid()) { | |||
throw new ConnectionException(); | |||
} | |||
} | |||
} | |||
@@ -125,11 +151,11 @@ class Share extends AbstractShare { | |||
* | |||
* @return string | |||
*/ | |||
public function getName() { | |||
public function getName(): string { | |||
return $this->name; | |||
} | |||
protected function simpleCommand($command, $path) { | |||
protected function simpleCommand(string $command, string $path): bool { | |||
$escapedPath = $this->escapePath($path); | |||
$cmd = $command . ' ' . $escapedPath; | |||
$output = $this->execute($cmd); | |||
@@ -139,13 +165,13 @@ class Share extends AbstractShare { | |||
/** | |||
* List the content of a remote folder | |||
* | |||
* @param $path | |||
* @return \Icewind\SMB\IFileInfo[] | |||
* @param string $path | |||
* @return IFileInfo[] | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function dir($path) { | |||
public function dir(string $path): array { | |||
$escapedPath = $this->escapePath($path); | |||
$output = $this->execute('cd ' . $escapedPath); | |||
//check output for errors | |||
@@ -154,16 +180,16 @@ class Share extends AbstractShare { | |||
$this->execute('cd /'); | |||
return $this->parser->parseDir($output, $path, function ($path) { | |||
return $this->parser->parseDir($output, $path, function (string $path) { | |||
return $this->getAcls($path); | |||
}); | |||
} | |||
/** | |||
* @param string $path | |||
* @return \Icewind\SMB\IFileInfo | |||
* @return IFileInfo | |||
*/ | |||
public function stat($path) { | |||
public function stat(string $path): IFileInfo { | |||
// some windows server setups don't seem to like the allinfo command | |||
// use the dir command instead to get the file info where possible | |||
if ($path !== "" && $path !== "/") { | |||
@@ -200,10 +226,10 @@ class Share extends AbstractShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function mkdir($path) { | |||
public function mkdir(string $path): bool { | |||
return $this->simpleCommand('mkdir', $path); | |||
} | |||
@@ -213,10 +239,10 @@ class Share extends AbstractShare { | |||
* @param string $path | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function rmdir($path) { | |||
public function rmdir(string $path): bool { | |||
return $this->simpleCommand('rmdir', $path); | |||
} | |||
@@ -230,7 +256,7 @@ class Share extends AbstractShare { | |||
* @throws NotFoundException | |||
* @throws \Exception | |||
*/ | |||
public function del($path, $secondTry = false) { | |||
public function del(string $path, bool $secondTry = false): bool { | |||
//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 { | |||
@@ -261,10 +287,10 @@ class Share extends AbstractShare { | |||
* @param string $to | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws NotFoundException | |||
* @throws AlreadyExistsException | |||
*/ | |||
public function rename($from, $to) { | |||
public function rename(string $from, string $to): bool { | |||
$path1 = $this->escapePath($from); | |||
$path2 = $this->escapePath($to); | |||
$output = $this->execute('rename ' . $path1 . ' ' . $path2); | |||
@@ -278,10 +304,10 @@ class Share extends AbstractShare { | |||
* @param string $target remove file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function put($source, $target) { | |||
public function put(string $source, string $target): bool { | |||
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping | |||
$path2 = $this->escapePath($target); | |||
$output = $this->execute('put ' . $path1 . ' ' . $path2); | |||
@@ -295,10 +321,10 @@ class Share extends AbstractShare { | |||
* @param string $target local file | |||
* @return bool | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function get($source, $target) { | |||
public function get(string $source, string $target): bool { | |||
$path1 = $this->escapePath($source); | |||
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping | |||
$output = $this->execute('get ' . $path1 . ' ' . $path2); | |||
@@ -311,10 +337,10 @@ class Share extends AbstractShare { | |||
* @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 | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function read($source) { | |||
public function read(string $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 | |||
@@ -333,10 +359,10 @@ class Share extends AbstractShare { | |||
* @param string $target | |||
* @return resource a write only stream to upload a remote file | |||
* | |||
* @throws \Icewind\SMB\Exception\NotFoundException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws NotFoundException | |||
* @throws InvalidTypeException | |||
*/ | |||
public function write($target) { | |||
public function write(string $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 | |||
@@ -348,9 +374,14 @@ class Share extends AbstractShare { | |||
// 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) { | |||
$stream = CallbackWrapper::wrap($fh, null, null, function () use ($connection) { | |||
$connection->close(false); // dont terminate, give the upload some time | |||
}); | |||
if (is_resource($stream)) { | |||
return $stream; | |||
} else { | |||
throw new InvalidRequestException($target); | |||
} | |||
} | |||
/** | |||
@@ -359,9 +390,9 @@ class Share extends AbstractShare { | |||
* | |||
* @param string $target | |||
* | |||
* @throws \Icewind\SMB\Exception\DependencyException | |||
* @throws DependencyException | |||
*/ | |||
public function append($target) { | |||
public function append(string $target) { | |||
throw new DependencyException('php-libsmbclient is required for append'); | |||
} | |||
@@ -370,7 +401,7 @@ class Share extends AbstractShare { | |||
* @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) { | |||
public function setMode(string $path, int $mode) { | |||
$modeString = ''; | |||
foreach (self::MODE_MAP as $modeByte => $string) { | |||
if ($mode & $modeByte) { | |||
@@ -400,7 +431,7 @@ class Share extends AbstractShare { | |||
* @throws ConnectionException | |||
* @throws DependencyException | |||
*/ | |||
public function notify($path) { | |||
public function notify(string $path): INotifyHandler { | |||
if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering | |||
throw new DependencyException('stdbuf is required for usage of the notify command'); | |||
} | |||
@@ -412,12 +443,11 @@ class Share extends AbstractShare { | |||
/** | |||
* @param string $command | |||
* @return array | |||
* @return string[] | |||
*/ | |||
protected function execute($command) { | |||
$this->connect(); | |||
$this->connection->write($command . PHP_EOL); | |||
return $this->connection->read(); | |||
protected function execute(string $command): array { | |||
$this->connect()->write($command . PHP_EOL); | |||
return $this->connect()->read(); | |||
} | |||
/** | |||
@@ -427,19 +457,18 @@ class Share extends AbstractShare { | |||
* @param string $path | |||
* | |||
* @return bool | |||
* @throws \Icewind\SMB\Exception\AlreadyExistsException | |||
* @throws AlreadyExistsException | |||
* @throws \Icewind\SMB\Exception\AccessDeniedException | |||
* @throws \Icewind\SMB\Exception\NotEmptyException | |||
* @throws \Icewind\SMB\Exception\InvalidTypeException | |||
* @throws InvalidTypeException | |||
* @throws \Icewind\SMB\Exception\Exception | |||
* @throws NotFoundException | |||
*/ | |||
protected function parseOutput($lines, $path = '') { | |||
protected function parseOutput(array $lines, string $path = ''): bool { | |||
if (count($lines) === 0) { | |||
return true; | |||
} else { | |||
$this->parser->checkForError($lines, $path); | |||
return false; | |||
} | |||
} | |||
@@ -447,7 +476,7 @@ class Share extends AbstractShare { | |||
* @param string $string | |||
* @return string | |||
*/ | |||
protected function escape($string) { | |||
protected function escape(string $string): string { | |||
return escapeshellarg($string); | |||
} | |||
@@ -455,7 +484,7 @@ class Share extends AbstractShare { | |||
* @param string $path | |||
* @return string | |||
*/ | |||
protected function escapePath($path) { | |||
protected function escapePath(string $path): string { | |||
$this->verifyPath($path); | |||
if ($path === '/') { | |||
$path = ''; | |||
@@ -470,12 +499,18 @@ class Share extends AbstractShare { | |||
* @param string $path | |||
* @return string | |||
*/ | |||
protected function escapeLocalPath($path) { | |||
protected function escapeLocalPath(string $path): string { | |||
$path = str_replace('"', '\"', $path); | |||
return '"' . $path . '"'; | |||
} | |||
protected function getAcls($path) { | |||
/** | |||
* @param string $path | |||
* @return ACL[] | |||
* @throws ConnectionException | |||
* @throws ConnectException | |||
*/ | |||
protected function getAcls(string $path): array { | |||
$commandPath = $this->system->getSmbcAclsPath(); | |||
if (!$commandPath) { | |||
return []; | |||
@@ -494,62 +529,11 @@ class Share extends AbstractShare { | |||
$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword()); | |||
$connection->connect(); | |||
if (!$connection->isValid()) { | |||
throw new ConnectionException($connection->readLine()); | |||
throw new ConnectionException((string)$connection->readLine()); | |||
} | |||
$rawAcls = $connection->readAll(); | |||
$acls = []; | |||
foreach ($rawAcls as $acl) { | |||
[$type, $acl] = explode(':', $acl, 2); | |||
if ($type !== 'ACL') { | |||
continue; | |||
} | |||
[$user, $permissions] = explode(':', $acl, 2); | |||
[$type, $flags, $mask] = explode('/', $permissions); | |||
$type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY; | |||
$flagsInt = 0; | |||
foreach (explode('|', $flags) as $flagString) { | |||
if ($flagString === 'OI') { | |||
$flagsInt += ACL::FLAG_OBJECT_INHERIT; | |||
} elseif ($flagString === 'CI') { | |||
$flagsInt += ACL::FLAG_CONTAINER_INHERIT; | |||
} | |||
} | |||
if (substr($mask, 0, 2) === '0x') { | |||
$maskInt = hexdec($mask); | |||
} else { | |||
$maskInt = 0; | |||
foreach (explode('|', $mask) as $maskString) { | |||
if ($maskString === 'R') { | |||
$maskInt += ACL::MASK_READ; | |||
} elseif ($maskString === 'W') { | |||
$maskInt += ACL::MASK_WRITE; | |||
} elseif ($maskString === 'X') { | |||
$maskInt += ACL::MASK_EXECUTE; | |||
} elseif ($maskString === 'D') { | |||
$maskInt += ACL::MASK_DELETE; | |||
} elseif ($maskString === 'READ') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE; | |||
} elseif ($maskString === 'CHANGE') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; | |||
} elseif ($maskString === 'FULL') { | |||
$maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; | |||
} | |||
} | |||
} | |||
if (isset($acls[$user])) { | |||
$existing = $acls[$user]; | |||
$maskInt += $existing->getMask(); | |||
} | |||
$acls[$user] = new ACL($type, $flagsInt, $maskInt); | |||
} | |||
return $acls; | |||
return $this->parser->parseACLs($rawAcls); | |||
} | |||
public function getServer(): IServer { |
@@ -1,3 +1,6 @@ | |||
.idea | |||
vendor | |||
composer.lock | |||
build | |||
example.php | |||
*.cache |
@@ -1,24 +0,0 @@ | |||
language: php | |||
php: | |||
- 5.4 | |||
- 5.5 | |||
- 5.6 | |||
- 7.0 | |||
- 7.1 | |||
- 7.2 | |||
env: | |||
global: | |||
- CURRENT_DIR=`pwd` | |||
install: | |||
- composer install --dev --no-interaction | |||
script: | |||
- mkdir -p build/logs | |||
- cd tests | |||
- phpunit --coverage-clover ../build/logs/clover.xml --configuration phpunit.xml | |||
after_script: | |||
- cd $CURRENT_DIR | |||
- php vendor/bin/coveralls -v |
@@ -1,8 +1,7 @@ | |||
# Streams # | |||
[![Build Status](https://travis-ci.org/icewind1991/Streams.svg?branch=master)](https://travis-ci.org/icewind1991/Streams) | |||
[![Coverage Status](https://img.shields.io/coveralls/icewind1991/Streams.svg)](https://coveralls.io/r/icewind1991/Streams?branch=master) | |||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/icewind1991/Streams/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/icewind1991/Streams/?branch=master) | |||
[![CI](https://github.com/icewind1991/Streams/actions/workflows/ci.yaml/badge.svg)](https://github.com/icewind1991/Streams/actions/workflows/ci.yaml) | |||
[![codecov](https://codecov.io/gh/icewind1991/Streams/branch/master/graph/badge.svg?token=bfPcAdGAaq)](https://codecov.io/gh/icewind1991/Streams) | |||
Generic stream wrappers for php. | |||
@@ -14,7 +13,7 @@ it wraps an existing stream and can thus be used for any stream in php | |||
The callbacks are passed in the stream context along with the source stream | |||
and can be any valid [php callable](http://php.net/manual/en/language.types.callable.php) | |||
###Example### | |||
### Example ### | |||
```php | |||
<?php | |||
@@ -1,24 +1,29 @@ | |||
{ | |||
"name" : "icewind/streams", | |||
"description" : "A set of generic stream wrappers", | |||
"license" : "MIT", | |||
"authors" : [ | |||
"name": "icewind/streams", | |||
"description": "A set of generic stream wrappers", | |||
"license": "MIT", | |||
"authors": [ | |||
{ | |||
"name" : "Robin Appelman", | |||
"name": "Robin Appelman", | |||
"email": "icewind@owncloud.com" | |||
} | |||
], | |||
"require" : { | |||
"php": ">=5.3" | |||
"require": { | |||
"php": ">=7.1" | |||
}, | |||
"require-dev" : { | |||
"satooshi/php-coveralls": "v1.0.0", | |||
"phpunit/phpunit": "^4.8" | |||
"require-dev": { | |||
"phpunit/phpunit": "^9", | |||
"friendsofphp/php-cs-fixer": "^2", | |||
"phpstan/phpstan": "^0.12" | |||
}, | |||
"autoload" : { | |||
"autoload": { | |||
"psr-4": { | |||
"Icewind\\Streams\\Tests\\": "tests/", | |||
"Icewind\\Streams\\": "src/" | |||
} | |||
}, | |||
"autoload-dev": { | |||
"psr-4": { | |||
"Icewind\\Streams\\Tests\\": "tests/" | |||
} | |||
} | |||
} |
@@ -25,27 +25,27 @@ namespace Icewind\Streams; | |||
*/ | |||
class CallbackWrapper extends Wrapper { | |||
/** | |||
* @var callable | |||
* @var callable|null | |||
*/ | |||
protected $readCallback; | |||
/** | |||
* @var callable | |||
* @var callable|null | |||
*/ | |||
protected $writeCallback; | |||
/** | |||
* @var callable | |||
* @var callable|null | |||
*/ | |||
protected $closeCallback; | |||
/** | |||
* @var callable | |||
* @var callable|null | |||
*/ | |||
protected $readDirCallBack; | |||
/** | |||
* @var callable | |||
* @var callable|null | |||
*/ | |||
protected $preCloseCallback; | |||
@@ -53,30 +53,28 @@ class CallbackWrapper extends Wrapper { | |||
* Wraps a stream with the provided callbacks | |||
* | |||
* @param resource $source | |||
* @param callable $read (optional) | |||
* @param callable $write (optional) | |||
* @param callable $close (optional) | |||
* @param callable $readDir (optional) | |||
* @return resource | |||
* @param callable|null $read (optional) | |||
* @param callable|null $write (optional) | |||
* @param callable|null $close (optional) | |||
* @param callable|null $readDir (optional) | |||
* @param callable|null $preClose (optional) | |||
* @return resource|bool | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
public static function wrap($source, $read = null, $write = null, $close = null, $readDir = null, $preClose = null) { | |||
$context = stream_context_create(array( | |||
'callback' => array( | |||
'source' => $source, | |||
'read' => $read, | |||
'write' => $write, | |||
'close' => $close, | |||
'readDir' => $readDir, | |||
'preClose' => $preClose, | |||
) | |||
)); | |||
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\CallbackWrapper'); | |||
$context = [ | |||
'source' => $source, | |||
'read' => $read, | |||
'write' => $write, | |||
'close' => $close, | |||
'readDir' => $readDir, | |||
'preClose' => $preClose, | |||
]; | |||
return self::wrapSource($source, $context); | |||
} | |||
protected function open() { | |||
$context = $this->loadContext('callback'); | |||
$context = $this->loadContext(); | |||
$this->readCallback = $context['read']; | |||
$this->writeCallback = $context['write']; | |||
@@ -112,7 +110,7 @@ class CallbackWrapper extends Wrapper { | |||
public function stream_close() { | |||
if (is_callable($this->preCloseCallback)) { | |||
call_user_func($this->preCloseCallback, $this->loadContext('callback')['source']); | |||
call_user_func($this->preCloseCallback, $this->source); | |||
// prevent further calls by potential PHP 7 GC ghosts | |||
$this->preCloseCallback = null; | |||
} |
@@ -55,7 +55,7 @@ class CountWrapper extends Wrapper { | |||
* | |||
* @param resource $source | |||
* @param callable $callback | |||
* @return resource | |||
* @return resource|bool | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
@@ -63,17 +63,14 @@ class CountWrapper extends Wrapper { | |||
if (!is_callable($callback)) { | |||
throw new \InvalidArgumentException('Invalid or missing callback'); | |||
} | |||
$context = stream_context_create(array( | |||
'count' => array( | |||
'source' => $source, | |||
'callback' => $callback | |||
) | |||
)); | |||
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\CountWrapper'); | |||
return self::wrapSource($source, [ | |||
'source' => $source, | |||
'callback' => $callback | |||
]); | |||
} | |||
protected function open() { | |||
$context = $this->loadContext('count'); | |||
$context = $this->loadContext(); | |||
$this->callback = $context['callback']; | |||
return true; | |||
} |
@@ -19,7 +19,7 @@ interface Directory { | |||
public function dir_opendir($path, $options); | |||
/** | |||
* @return string | |||
* @return string|bool | |||
*/ | |||
public function dir_readdir(); | |||
@@ -25,7 +25,7 @@ class DirectoryFilter extends DirectoryWrapper { | |||
* @return bool | |||
*/ | |||
public function dir_opendir($path, $options) { | |||
$context = $this->loadContext('filter'); | |||
$context = $this->loadContext(); | |||
$this->filter = $context['filter']; | |||
return true; | |||
} | |||
@@ -36,7 +36,7 @@ class DirectoryFilter extends DirectoryWrapper { | |||
public function dir_readdir() { | |||
$file = readdir($this->source); | |||
$filter = $this->filter; | |||
// keep reading untill we have an accepted entry or we're at the end of the folder | |||
// keep reading until we have an accepted entry or we're at the end of the folder | |||
while ($file !== false && $filter($file) === false) { | |||
$file = readdir($this->source); | |||
} | |||
@@ -46,15 +46,12 @@ class DirectoryFilter extends DirectoryWrapper { | |||
/** | |||
* @param resource $source | |||
* @param callable $filter | |||
* @return resource | |||
* @return resource|bool | |||
*/ | |||
public static function wrap($source, callable $filter) { | |||
$options = array( | |||
'filter' => array( | |||
'source' => $source, | |||
'filter' => $filter | |||
) | |||
); | |||
return self::wrapWithOptions($options, '\Icewind\Streams\DirectoryFilter'); | |||
return self::wrapSource($source, [ | |||
'source' => $source, | |||
'filter' => $filter | |||
]); | |||
} | |||
} |
@@ -7,37 +7,9 @@ | |||
namespace Icewind\Streams; | |||
class DirectoryWrapper implements Directory { | |||
/** | |||
* @var resource | |||
*/ | |||
public $context; | |||
/** | |||
* @var resource | |||
*/ | |||
protected $source; | |||
/** | |||
* Load the source from the stream context and return the context options | |||
* | |||
* @param string $name | |||
* @return array | |||
* @throws \Exception | |||
*/ | |||
protected function loadContext($name) { | |||
$context = stream_context_get_options($this->context); | |||
if (isset($context[$name])) { | |||
$context = $context[$name]; | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); | |||
} | |||
if (isset($context['source']) and is_resource($context['source'])) { | |||
$this->source = $context['source']; | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, source not set'); | |||
} | |||
return $context; | |||
class DirectoryWrapper extends Wrapper implements Directory { | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
return false; | |||
} | |||
/** | |||
@@ -46,7 +18,7 @@ class DirectoryWrapper implements Directory { | |||
* @return bool | |||
*/ | |||
public function dir_opendir($path, $options) { | |||
$this->loadContext('dir'); | |||
$this->loadContext(); | |||
return true; | |||
} | |||
@@ -72,17 +44,4 @@ class DirectoryWrapper implements Directory { | |||
rewinddir($this->source); | |||
return true; | |||
} | |||
/** | |||
* @param array $options the options for the context to wrap the stream with | |||
* @param string $class | |||
* @return resource | |||
*/ | |||
protected static function wrapWithOptions($options, $class) { | |||
$context = stream_context_create($options); | |||
stream_wrapper_register('dirwrapper', $class); | |||
$wrapped = opendir('dirwrapper://', $context); | |||
stream_wrapper_unregister('dirwrapper'); | |||
return $wrapped; | |||
} | |||
} |
@@ -15,7 +15,7 @@ interface File { | |||
* @param string $path | |||
* @param string $mode | |||
* @param int $options | |||
* @param string &$opened_path | |||
* @param string $opened_path | |||
* @return bool | |||
*/ | |||
public function stream_open($path, $mode, $options, &$opened_path); | |||
@@ -28,19 +28,19 @@ interface File { | |||
public function stream_seek($offset, $whence = SEEK_SET); | |||
/** | |||
* @return int | |||
* @return int|false | |||
*/ | |||
public function stream_tell(); | |||
/** | |||
* @param int $count | |||
* @return string | |||
* @return string|false | |||
*/ | |||
public function stream_read($count); | |||
/** | |||
* @param string $data | |||
* @return int | |||
* @return int|false | |||
*/ | |||
public function stream_write($data); | |||
@@ -59,7 +59,7 @@ interface File { | |||
public function stream_truncate($size); | |||
/** | |||
* @return array | |||
* @return array|false | |||
*/ | |||
public function stream_stat(); | |||
@@ -0,0 +1,78 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
namespace Icewind\Streams; | |||
abstract class HashWrapper extends Wrapper { | |||
/** | |||
* @var callable|null | |||
*/ | |||
private $callback; | |||
/** | |||
* @var resource|\HashContext | |||
*/ | |||
private $hashContext; | |||
/** | |||
* Wraps a stream to make it seekable | |||
* | |||
* @param resource $source | |||
* @param string $hash | |||
* @param callable $callback | |||
* @return resource|bool | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
public static function wrap($source, $hash, $callback) { | |||
$context = [ | |||
'hash' => $hash, | |||
'callback' => $callback, | |||
]; | |||
return self::wrapSource($source, $context); | |||
} | |||
public function dir_opendir($path, $options) { | |||
return false; | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$context = $this->loadContext(); | |||
$this->callback = $context['callback']; | |||
$this->hashContext = hash_init($context['hash']); | |||
return true; | |||
} | |||
protected function updateHash($data) { | |||
hash_update($this->hashContext, $data); | |||
} | |||
public function stream_close() { | |||
$hash = hash_final($this->hashContext); | |||
if ($this->hashContext !== false && is_callable($this->callback)) { | |||
call_user_func($this->callback, $hash); | |||
} | |||
return parent::stream_close(); | |||
} | |||
} |
@@ -20,7 +20,7 @@ namespace Icewind\Streams; | |||
* | |||
* Either 'array' or 'iterator' need to be set, if both are set, 'iterator' takes preference | |||
*/ | |||
class IteratorDirectory implements Directory { | |||
class IteratorDirectory extends WrapperHandler implements Directory { | |||
/** | |||
* @var resource | |||
*/ | |||
@@ -36,18 +36,13 @@ class IteratorDirectory implements Directory { | |||
* | |||
* @param string $name | |||
* @return array | |||
* @throws \Exception | |||
* @throws \BadMethodCallException | |||
*/ | |||
protected function loadContext($name) { | |||
$context = stream_context_get_options($this->context); | |||
if (isset($context[$name])) { | |||
$context = $context[$name]; | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); | |||
} | |||
protected function loadContext($name = null) { | |||
$context = parent::loadContext($name); | |||
if (isset($context['iterator'])) { | |||
$this->iterator = $context['iterator']; | |||
} else if (isset($context['array'])) { | |||
} elseif (isset($context['array'])) { | |||
$this->iterator = new \ArrayIterator($context['array']); | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, iterator or array not set'); | |||
@@ -61,12 +56,12 @@ class IteratorDirectory implements Directory { | |||
* @return bool | |||
*/ | |||
public function dir_opendir($path, $options) { | |||
$this->loadContext('dir'); | |||
$this->loadContext(); | |||
return true; | |||
} | |||
/** | |||
* @return string | |||
* @return string|bool | |||
*/ | |||
public function dir_readdir() { | |||
if ($this->iterator->valid()) { | |||
@@ -97,27 +92,22 @@ class IteratorDirectory implements Directory { | |||
* Creates a directory handle from the provided array or iterator | |||
* | |||
* @param \Iterator | array $source | |||
* @return resource | |||
* @return resource|bool | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
public static function wrap($source) { | |||
if ($source instanceof \Iterator) { | |||
$context = stream_context_create(array( | |||
'dir' => array( | |||
'iterator' => $source) | |||
)); | |||
} else if (is_array($source)) { | |||
$context = stream_context_create(array( | |||
'dir' => array( | |||
'array' => $source) | |||
)); | |||
$options = [ | |||
'iterator' => $source | |||
]; | |||
} elseif (is_array($source)) { | |||
$options = [ | |||
'array' => $source | |||
]; | |||
} else { | |||
throw new \BadMethodCallException('$source should be an Iterator or array'); | |||
} | |||
stream_wrapper_register('iterator', '\Icewind\Streams\IteratorDirectory'); | |||
$wrapped = opendir('iterator://', $context); | |||
stream_wrapper_unregister('iterator'); | |||
return $wrapped; | |||
return self::wrapSource(self::NO_SOURCE_DIR, $options); | |||
} | |||
} |
@@ -11,29 +11,17 @@ namespace Icewind\Streams; | |||
* Stream wrapper that does nothing, used for tests | |||
*/ | |||
class NullWrapper extends Wrapper { | |||
/** | |||
* Wraps a stream with the provided callbacks | |||
* | |||
* @param resource $source | |||
* @return resource | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
public static function wrap($source) { | |||
$context = stream_context_create(array( | |||
'null' => array( | |||
'source' => $source) | |||
)); | |||
return Wrapper::wrapSource($source, $context, 'null', '\Icewind\Streams\NullWrapper'); | |||
return self::wrapSource($source); | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$this->loadContext('null'); | |||
$this->loadContext(); | |||
return true; | |||
} | |||
public function dir_opendir($path, $options) { | |||
$this->loadContext('null'); | |||
$this->loadContext(); | |||
return true; | |||
} | |||
} |
@@ -38,7 +38,7 @@ class Path { | |||
* @param string $class | |||
* @param array $contextOptions | |||
*/ | |||
public function __construct($class, $contextOptions = array()) { | |||
public function __construct($class, $contextOptions = []) { | |||
$this->class = $class; | |||
$this->contextOptions = $contextOptions; | |||
} | |||
@@ -75,7 +75,7 @@ class Path { | |||
*/ | |||
protected function appendDefaultContent($values) { | |||
if (!is_array(current($values))) { | |||
$values = array($this->getProtocol() => $values); | |||
$values = [$this->getProtocol() => $values]; | |||
} | |||
$context = stream_context_get_default(); | |||
$defaults = stream_context_get_options($context); |
@@ -16,10 +16,8 @@ class PathWrapper extends NullWrapper { | |||
* @return Path|string | |||
*/ | |||
public static function getPath($source) { | |||
return new Path(__CLASS__, [ | |||
'null' => [ | |||
'source' => $source | |||
] | |||
return new Path(NullWrapper::class, [ | |||
NullWrapper::getProtocol() => ['source' => $source] | |||
]); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
namespace Icewind\Streams; | |||
/** | |||
* Wrapper that calculates the hash on the stream on read | |||
* | |||
* The stream and hash should be passed in when wrapping the stream. | |||
* On close the callback will be called with the calculated checksum. | |||
* | |||
* For supported hashes see: http://php.net/manual/en/function.hash-algos.php | |||
*/ | |||
class ReadHashWrapper extends HashWrapper { | |||
public function stream_read($count) { | |||
$data = parent::stream_read($count); | |||
$this->updateHash($data); | |||
return $data; | |||
} | |||
} |
@@ -11,25 +11,8 @@ namespace Icewind\Streams; | |||
* Wrapper that retries reads/writes to remote streams that dont deliver/recieve all requested data at once | |||
*/ | |||
class RetryWrapper extends Wrapper { | |||
/** | |||
* Wraps a stream with the provided callbacks | |||
* | |||
* @param resource $source | |||
* @return resource | |||
*/ | |||
public static function wrap($source) { | |||
$context = stream_context_create(array( | |||
'retry' => array( | |||
'source' => $source | |||
) | |||
)); | |||
return Wrapper::wrapSource($source, $context, 'retry', '\Icewind\Streams\RetryWrapper'); | |||
} | |||
protected function open() { | |||
$this->loadContext('retry'); | |||
return true; | |||
return self::wrapSource($source); | |||
} | |||
public function dir_opendir($path, $options) { | |||
@@ -37,7 +20,8 @@ class RetryWrapper extends Wrapper { | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
return $this->open(); | |||
$this->loadContext(); | |||
return true; | |||
} | |||
public function stream_read($count) { |
@@ -25,21 +25,8 @@ class SeekableWrapper extends Wrapper { | |||
*/ | |||
protected $cache; | |||
/** | |||
* Wraps a stream to make it seekable | |||
* | |||
* @param resource $source | |||
* @return resource | |||
* | |||
* @throws \BadMethodCallException | |||
*/ | |||
public static function wrap($source) { | |||
$context = stream_context_create(array( | |||
'callback' => array( | |||
'source' => $source | |||
) | |||
)); | |||
return Wrapper::wrapSource($source, $context, 'callback', '\Icewind\Streams\SeekableWrapper'); | |||
return self::wrapSource($source); | |||
} | |||
public function dir_opendir($path, $options) { | |||
@@ -47,8 +34,12 @@ class SeekableWrapper extends Wrapper { | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$this->loadContext('callback'); | |||
$this->cache = fopen('php://temp', 'w+'); | |||
$this->loadContext(); | |||
$cache = fopen('php://temp', 'w+'); | |||
if ($cache === false) { | |||
return false; | |||
} | |||
$this->cache = $cache; | |||
return true; | |||
} | |||
@@ -72,7 +63,7 @@ class SeekableWrapper extends Wrapper { | |||
public function stream_seek($offset, $whence = SEEK_SET) { | |||
if ($whence === SEEK_SET) { | |||
$target = $offset; | |||
} else if ($whence === SEEK_CUR) { | |||
} elseif ($whence === SEEK_CUR) { | |||
$current = ftell($this->cache); | |||
$target = $current + $offset; | |||
} else { |
@@ -22,7 +22,7 @@ interface Url { | |||
* @param string $path | |||
* @param string $mode | |||
* @param int $options | |||
* @param string &$opened_path | |||
* @param string $opened_path | |||
* @return bool | |||
*/ | |||
public function stream_open($path, $mode, $options, &$opened_path); | |||
@@ -50,7 +50,7 @@ interface Url { | |||
public function rmdir($path, $options); | |||
/** | |||
* @param string | |||
* @param string $path | |||
* @return bool | |||
*/ | |||
public function unlink($path); | |||
@@ -58,7 +58,7 @@ interface Url { | |||
/** | |||
* @param string $path | |||
* @param int $flags | |||
* @return array | |||
* @return array|false | |||
*/ | |||
public function url_stat($path, $flags); | |||
} |
@@ -47,24 +47,30 @@ class UrlCallback extends Wrapper implements Url { | |||
* @return \Icewind\Streams\Path | |||
* | |||
* @throws \BadMethodCallException | |||
* @throws \Exception | |||
*/ | |||
public static function wrap($source, $fopen = null, $opendir = null, $mkdir = null, $rename = null, $rmdir = null, | |||
$unlink = null, $stat = null) { | |||
$options = array( | |||
'source' => $source, | |||
'fopen' => $fopen, | |||
public static function wrap( | |||
$source, | |||
$fopen = null, | |||
$opendir = null, | |||
$mkdir = null, | |||
$rename = null, | |||
$rmdir = null, | |||
$unlink = null, | |||
$stat = null | |||
) { | |||
return new Path(static::class, [ | |||
'source' => $source, | |||
'fopen' => $fopen, | |||
'opendir' => $opendir, | |||
'mkdir' => $mkdir, | |||
'rename' => $rename, | |||
'rmdir' => $rmdir, | |||
'unlink' => $unlink, | |||
'stat' => $stat | |||
); | |||
return new Path('\Icewind\Streams\UrlCallBack', $options); | |||
'mkdir' => $mkdir, | |||
'rename' => $rename, | |||
'rmdir' => $rmdir, | |||
'unlink' => $unlink, | |||
'stat' => $stat | |||
]); | |||
} | |||
protected function loadContext($url) { | |||
protected function loadUrlContext($url) { | |||
list($protocol) = explode('://', $url); | |||
$options = stream_context_get_options($this->context); | |||
return $options[$protocol]; | |||
@@ -77,40 +83,48 @@ class UrlCallback extends Wrapper implements Url { | |||
} | |||
public function stream_open($path, $mode, $options, &$opened_path) { | |||
$context = $this->loadContext($path); | |||
$context = $this->loadUrlContext($path); | |||
$this->callCallBack($context, 'fopen'); | |||
$this->setSourceStream(fopen($context['source'], $mode)); | |||
$source = fopen($context['source'], $mode); | |||
if ($source === false) { | |||
return false; | |||
} | |||
$this->setSourceStream($source); | |||
return true; | |||
} | |||
public function dir_opendir($path, $options) { | |||
$context = $this->loadContext($path); | |||
$context = $this->loadUrlContext($path); | |||
$this->callCallBack($context, 'opendir'); | |||
$this->setSourceStream(opendir($context['source'])); | |||
$source = opendir($context['source']); | |||
if ($source === false) { | |||
return false; | |||
} | |||
$this->setSourceStream($source); | |||
return true; | |||
} | |||
public function mkdir($path, $mode, $options) { | |||
$context = $this->loadContext($path); | |||
$context = $this->loadUrlContext($path); | |||
$this->callCallBack($context, 'mkdir'); | |||
return mkdir($context['source'], $mode, $options & STREAM_MKDIR_RECURSIVE); | |||
return mkdir($context['source'], $mode, ($options & STREAM_MKDIR_RECURSIVE) > 0); | |||
} | |||
public function rmdir($path, $options) { | |||
$context = $this->loadContext($path); | |||
$context = $this->loadUrlContext($path); | |||
$this->callCallBack($context, 'rmdir'); | |||
return rmdir($context['source']); | |||
} | |||
public function rename($source, $target) { | |||
$context = $this->loadContext($source); | |||
$context = $this->loadUrlContext($source); | |||
$this->callCallBack($context, 'rename'); | |||
list(, $target) = explode('://', $target); | |||
return rename($context['source'], $target); | |||
} | |||
public function unlink($path) { | |||
$context = $this->loadContext($path); | |||
$context = $this->loadUrlContext($path); | |||
$this->callCallBack($context, 'unlink'); | |||
return unlink($context['source']); | |||
} |
@@ -12,7 +12,7 @@ namespace Icewind\Streams; | |||
* | |||
* This wrapper itself doesn't implement any functionality but is just a base class for other wrappers to extend | |||
*/ | |||
abstract class Wrapper implements File, Directory { | |||
abstract class Wrapper extends WrapperHandler implements File, Directory { | |||
/** | |||
* @var resource | |||
*/ | |||
@@ -25,44 +25,15 @@ abstract class Wrapper implements File, Directory { | |||
*/ | |||
protected $source; | |||
protected static function wrapSource($source, $context, $protocol, $class) { | |||
if (!is_resource($source)) { | |||
throw new \BadMethodCallException(); | |||
} | |||
try { | |||
stream_wrapper_register($protocol, $class); | |||
if (self::isDirectoryHandle($source)) { | |||
$wrapped = opendir($protocol . '://', $context); | |||
} else { | |||
$wrapped = fopen($protocol . '://', 'r+', false, $context); | |||
} | |||
} catch (\BadMethodCallException $e) { | |||
stream_wrapper_unregister($protocol); | |||
throw $e; | |||
} | |||
stream_wrapper_unregister($protocol); | |||
return $wrapped; | |||
} | |||
protected static function isDirectoryHandle($resource) { | |||
$meta = stream_get_meta_data($resource); | |||
return $meta['stream_type'] == 'dir'; | |||
} | |||
/** | |||
* Load the source from the stream context and return the context options | |||
* | |||
* @param string $name | |||
* @return array | |||
* @throws \Exception | |||
* @param resource $source | |||
*/ | |||
protected function loadContext($name) { | |||
$context = stream_context_get_options($this->context); | |||
if (isset($context[$name])) { | |||
$context = $context[$name]; | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); | |||
} | |||
protected function setSourceStream($source) { | |||
$this->source = $source; | |||
} | |||
protected function loadContext($name = null) { | |||
$context = parent::loadContext($name); | |||
if (isset($context['source']) and is_resource($context['source'])) { | |||
$this->setSourceStream($context['source']); | |||
} else { | |||
@@ -71,13 +42,6 @@ abstract class Wrapper implements File, Directory { | |||
return $context; | |||
} | |||
/** | |||
* @param resource $source | |||
*/ | |||
protected function setSourceStream($source) { | |||
$this->source = $source; | |||
} | |||
public function stream_seek($offset, $whence = SEEK_SET) { | |||
$result = fseek($this->source, $offset, $whence); | |||
return $result == 0 ? true : false; | |||
@@ -98,14 +62,13 @@ abstract class Wrapper implements File, Directory { | |||
public function stream_set_option($option, $arg1, $arg2) { | |||
switch ($option) { | |||
case STREAM_OPTION_BLOCKING: | |||
stream_set_blocking($this->source, $arg1); | |||
break; | |||
return stream_set_blocking($this->source, (bool)$arg1); | |||
case STREAM_OPTION_READ_TIMEOUT: | |||
stream_set_timeout($this->source, $arg1, $arg2); | |||
break; | |||
return stream_set_timeout($this->source, $arg1, $arg2); | |||
case STREAM_OPTION_WRITE_BUFFER: | |||
stream_set_write_buffer($this->source, $arg1); | |||
return stream_set_write_buffer($this->source, $arg1) === 0; | |||
} | |||
return false; | |||
} | |||
public function stream_truncate($size) { |
@@ -0,0 +1,114 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
namespace Icewind\Streams; | |||
class WrapperHandler { | |||
/** @var resource $context */ | |||
protected $context; | |||
const NO_SOURCE_DIR = 1; | |||
/** | |||
* get the protocol name that is generated for the class | |||
* @param string|null $class | |||
* @return string | |||
*/ | |||
public static function getProtocol($class = null) { | |||
if ($class === null) { | |||
$class = static::class; | |||
} | |||
$parts = explode('\\', $class); | |||
return strtolower(array_pop($parts)); | |||
} | |||
private static function buildContext($protocol, $context, $source) { | |||
if (is_array($context)) { | |||
$context['source'] = $source; | |||
return stream_context_create([$protocol => $context]); | |||
} else { | |||
return $context; | |||
} | |||
} | |||
/** | |||
* @param resource|int $source | |||
* @param resource|array $context | |||
* @param string|null $protocol deprecated, protocol is now automatically generated | |||
* @param string|null $class deprecated, class is now automatically generated | |||
* @return bool|resource | |||
*/ | |||
protected static function wrapSource($source, $context = [], $protocol = null, $class = null) { | |||
if ($class === null) { | |||
$class = static::class; | |||
} | |||
if ($protocol === null) { | |||
$protocol = self::getProtocol($class); | |||
} | |||
$context = self::buildContext($protocol, $context, $source); | |||
try { | |||
stream_wrapper_register($protocol, $class); | |||
if (self::isDirectoryHandle($source)) { | |||
return opendir($protocol . '://', $context); | |||
} else { | |||
return fopen($protocol . '://', 'r+', false, $context); | |||
} | |||
} finally { | |||
stream_wrapper_unregister($protocol); | |||
} | |||
} | |||
protected static function isDirectoryHandle($resource) { | |||
if ($resource === self::NO_SOURCE_DIR) { | |||
return true; | |||
} | |||
if (!is_resource($resource)) { | |||
throw new \BadMethodCallException('Invalid stream source'); | |||
} | |||
$meta = stream_get_meta_data($resource); | |||
return $meta['stream_type'] === 'dir' || $meta['stream_type'] === 'user-space-dir'; | |||
} | |||
/** | |||
* Load the source from the stream context and return the context options | |||
* | |||
* @param string|null $name if not set, the generated protocol name is used | |||
* @return array | |||
* @throws \BadMethodCallException | |||
*/ | |||
protected function loadContext($name = null) { | |||
if ($name === null) { | |||
$parts = explode('\\', static::class); | |||
$name = strtolower(array_pop($parts)); | |||
} | |||
$context = stream_context_get_options($this->context); | |||
if (isset($context[$name])) { | |||
$context = $context[$name]; | |||
} else { | |||
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); | |||
} | |||
return $context; | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
namespace Icewind\Streams; | |||
/** | |||
* Wrapper that calculates the hash on the stream on write | |||
* | |||
* The stream and hash should be passed in when wrapping the stream. | |||
* On close the callback will be called with the calculated checksum. | |||
* | |||
* For supported hashes see: http://php.net/manual/en/function.hash-algos.php | |||
*/ | |||
class WriteHashWrapper extends HashWrapper { | |||
public function stream_write($data) { | |||
$this->updateHash($data); | |||
return parent::stream_write($data); | |||
} | |||
} |