diff --git a/apps/files_external/3rdparty/smb/Icewind/SMB/Connection.php b/apps/files_external/3rdparty/smb/Icewind/SMB/Connection.php new file mode 100644 index 000000000000..d7f7397346cb --- /dev/null +++ b/apps/files_external/3rdparty/smb/Icewind/SMB/Connection.php @@ -0,0 +1,86 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Icewind\SMB; + +class Connection extends RawConnection { + const DELIMITER = 'smb:'; + + /** + * send input to smbclient + * + * @param string $input + */ + public function write($input) { + parent::write($input . PHP_EOL); + } + + /** + * get all unprocessed output from smbclient until the next prompt + * + * @throws ConnectionError + * @return string + */ + public function read() { + if (!$this->isValid()) { + throw new ConnectionError(); + } + $line = $this->readLine(); //first line is prompt + $this->checkConnectionError($line); + + $output = array(); + $line = $this->readLine(); + $length = strlen(self::DELIMITER); + while (substr($line, 0, $length) !== self::DELIMITER) { //next prompt functions as delimiter + $output[] .= $line; + $line = parent::read(); + } + return $output; + } + + /** + * read a single line of unprocessed output + * + * @return string + */ + public function readLine() { + return parent::read(); + } + + /** + * check if the first line holds a connection failure + * + * @param $line + * @throws AuthenticationException + * @throws InvalidHostException + */ + private function checkConnectionError($line) { + $line = rtrim($line, ')'); + if (substr($line, -23) === ErrorCodes::LogonFailure) { + throw new AuthenticationException(); + } + if (substr($line, -26) === ErrorCodes::BadHostName) { + throw new InvalidHostException(); + } + if (substr($line, -22) === ErrorCodes::Unsuccessful) { + throw new InvalidHostException(); + } + if (substr($line, -28) === ErrorCodes::ConnectionRefused) { + throw new InvalidHostException(); + } + } + + public function close() { + $this->write('close' . PHP_EOL); + } + + public function __destruct() { + $this->close(); + parent::__destruct(); + } +} diff --git a/apps/files_external/3rdparty/smb/Icewind/SMB/ErrorCodes.php b/apps/files_external/3rdparty/smb/Icewind/SMB/ErrorCodes.php new file mode 100644 index 000000000000..ea01ec7a2eae --- /dev/null +++ b/apps/files_external/3rdparty/smb/Icewind/SMB/ErrorCodes.php @@ -0,0 +1,52 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Icewind\SMB; + +class NotFoundException extends \Exception { +} + +class AlreadyExistsException extends \Exception { +} + +class NotEmptyException extends \Exception { +} + +class ConnectionError extends \Exception { +} + +class AuthenticationException extends \Exception { +} + +class InvalidHostException extends \Exception { +} + +class AccessDeniedException extends \Exception { +} + +class InvalidTypeException extends \Exception { +} + +class ErrorCodes { + /** + * connection errors + */ + const LogonFailure = 'NT_STATUS_LOGON_FAILURE'; + const BadHostName = 'NT_STATUS_BAD_NETWORK_NAME'; + const Unsuccessful = 'NT_STATUS_UNSUCCESSFUL'; + const ConnectionRefused = 'NT_STATUS_CONNECTION_REFUSED'; + + const PathNotFound = 'NT_STATUS_OBJECT_PATH_NOT_FOUND'; + const NoSuchFile = 'NT_STATUS_NO_SUCH_FILE'; + const ObjectNotFound = 'NT_STATUS_OBJECT_NAME_NOT_FOUND'; + const NameCollision = 'NT_STATUS_OBJECT_NAME_COLLISION'; + const AccessDenied = 'NT_STATUS_ACCESS_DENIED'; + const DirectoryNotEmpty = 'NT_STATUS_DIRECTORY_NOT_EMPTY'; + const FileIsADirectory = 'NT_STATUS_FILE_IS_A_DIRECTORY'; + const NotADirectory = 'NT_STATUS_NOT_A_DIRECTORY'; +} diff --git a/apps/files_external/3rdparty/smb/Icewind/SMB/RawConnection.php b/apps/files_external/3rdparty/smb/Icewind/SMB/RawConnection.php new file mode 100644 index 000000000000..1337c4c30a3f --- /dev/null +++ b/apps/files_external/3rdparty/smb/Icewind/SMB/RawConnection.php @@ -0,0 +1,92 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Icewind\SMB; + +class RawConnection { + /** + * @var resource[] $pipes + * + * $pipes[0] holds STDIN for smbclient + * $pipes[1] holds STDOUT for smbclient + */ + private $pipes; + + /** + * @var resource $process + */ + private $process; + + + public function __construct($command) { + $descriptorSpec = array( + 0 => array("pipe", "r"), + 1 => array("pipe", "w"), + 2 => array('file', '/dev/null', 'w') + ); + setlocale(LC_ALL, Server::LOCALE); + $this->process = proc_open($command, $descriptorSpec, $this->pipes, null, array( + 'CLI_FORCE_INTERACTIVE' => 'y', // Needed or the prompt isn't displayed!! + 'LC_ALL' => Server::LOCALE + )); + if (!$this->isValid()) { + throw new ConnectionError(); + } + } + + /** + * check if the connection is still active + * + * @return bool + */ + public function isValid() { + if (is_resource($this->process)) { + $status = proc_get_status($this->process); + return $status['running']; + } else { + return false; + } + } + + /** + * send input to the process + * + * @param string $input + */ + public function write($input) { + fwrite($this->pipes[0], $input); + fflush($this->pipes[0]); + } + + /** + * read a line of output + * + * @return string + */ + public function read() { + return trim(fgets($this->pipes[1])); + } + + /** + * get all output until the process closes + * + * @return array + */ + public function readAll() { + $output = array(); + while ($line = $this->read()) { + $output[] = $line; + } + return $output; + } + + public function __destruct() { + proc_terminate($this->process); + proc_close($this->process); + } +} diff --git a/apps/files_external/3rdparty/smb/Icewind/SMB/Server.php b/apps/files_external/3rdparty/smb/Icewind/SMB/Server.php new file mode 100644 index 000000000000..056f6a971011 --- /dev/null +++ b/apps/files_external/3rdparty/smb/Icewind/SMB/Server.php @@ -0,0 +1,134 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Icewind\SMB; + +class Server { + const CLIENT = 'smbclient'; + const LOCALE = 'en_US.UTF-8'; + + /** + * @var string $host + */ + private $host; + + /** + * @var string $user + */ + private $user; + + /** + * @var string $password + */ + private $password; + + /** + * @param string $host + * @param string $user + * @param string $password + */ + public function __construct($host, $user, $password) { + $this->host = $host; + $this->user = $user; + $this->password = $password; + } + + /** + * @return string + */ + public function getAuthString() { + return $this->user . '%' . $this->password; + } + + /** + * @return string + */ + public function getUser() { + return $this->user; + } + + /** + * @return string + */ + public function getPassword() { + return $this->password; + } + + /** + * return string + */ + public function getHost() { + return $this->host; + } + + /** + * @return Share[] + * @throws \Icewind\SMB\AuthenticationException + * @throws \Icewind\SMB\InvalidHostException + */ + public function listShares() { + $user = escapeshellarg($this->getUser()); + $command = self::CLIENT . ' -U ' . $user . ' ' . '-gL ' . escapeshellarg($this->getHost()); + $connection = new RawConnection($command); + $connection->write($this->getPassword() . PHP_EOL); + $output = $connection->readAll(); + + $line = $output[0]; + + // disregard password prompt + if (substr($line, 0, 6) == 'Enter ' and count($output) > 1) { + $line = $output[1]; + } + + $line = rtrim($line, ')'); + if (substr($line, -23) === ErrorCodes::LogonFailure) { + throw new AuthenticationException(); + } + if (substr($line, -26) === ErrorCodes::BadHostName) { + throw new InvalidHostException(); + } + if (substr($line, -22) === ErrorCodes::Unsuccessful) { + throw new InvalidHostException(); + } + if (substr($line, -28) === ErrorCodes::ConnectionRefused) { + throw new InvalidHostException(); + } + + $shareNames = array(); + foreach ($output as $line) { + if (strpos($line, '|')) { + list($type, $name, $description) = explode('|', $line); + if (strtolower($type) === 'disk') { + $shareNames[$name] = $description; + } + } + } + + $shares = array(); + foreach ($shareNames as $name => $description) { + $shares[] = $this->getShare($name); + } + return $shares; + } + + /** + * @param string $name + * @return Share + */ + public function getShare($name) { + return new Share($this, $name); + } + + /** + * @return string + */ + public function getTimeZone() { + $command = 'net time zone -S ' . escapeshellarg($this->getHost()); + return exec($command); + } +} diff --git a/apps/files_external/3rdparty/smb/Icewind/SMB/Share.php b/apps/files_external/3rdparty/smb/Icewind/SMB/Share.php new file mode 100644 index 000000000000..e6dd24377d86 --- /dev/null +++ b/apps/files_external/3rdparty/smb/Icewind/SMB/Share.php @@ -0,0 +1,273 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Icewind\SMB; + +class Share { + /** + * @var Server $server + */ + private $server; + + /** + * @var string $name + */ + private $name; + + /** + * @var Connection $connection + */ + private $connection; + + private $serverTimezone; + + /** + * @param Server $server + * @param string $name + * @throws ConnectionError + */ + public function __construct($server, $name) { + $this->server = $server; + $this->name = $name; + } + + public function connect() { + if ($this->connection and $this->connection->isValid()) { + return; + } + $command = Server::CLIENT . ' -U ' . escapeshellarg($this->server->getUser()) . + ' //' . $this->server->getHost() . '/' . $this->name; + $this->connection = new Connection($command); + $this->connection->write($this->server->getPassword()); + $this->connection->readLine(); // discard password prompt + if (!$this->connection->isValid()) { + throw new ConnectionError(); + } + } + + public function getName() { + return $this->name; + } + + protected function simpleCommand($command, $path) { + $path = $this->escapePath($path); + $cmd = $command . ' ' . $path; + $output = $this->execute($cmd); + return $this->parseOutput($output); + } + + private function getServerTimeZone() { + if (!$this->serverTimezone) { + $this->serverTimezone = $this->server->getTimeZone(); + } + return $this->serverTimezone; + } + + /** + * List the content of a remote folder + * + * @param $path + * @return array + */ + public function dir($path) { + $path = $this->escapePath($path); + $output = $this->execute('cd ' . $path); + //check output for errors + $this->parseOutput($output); + $output = $this->execute('dir'); + $this->execute('cd /'); + + //last line is used space + array_pop($output); + $regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/'; + //2 spaces, filename, optional type, size, date + $content = array(); + foreach ($output as $line) { + if (preg_match($regex, $line, $matches)) { + list(, $name, $type, $size, $time) = $matches; + if ($name !== '.' and $name !== '..') { + $content[$name] = array( + 'size' => intval(trim($size)), + 'type' => (strpos($type, 'D') !== false) ? 'dir' : 'file', + 'time' => strtotime($time . ' ' . $this->getServerTimeZone()) + ); + } + } + } + return $content; + } + + /** + * Create a folder on the share + * + * @param string $path + * @return bool + */ + public function mkdir($path) { + return $this->simpleCommand('mkdir', $path); + } + + /** + * Remove a folder on the share + * + * @param string $path + * @return bool + */ + public function rmdir($path) { + return $this->simpleCommand('rmdir', $path); + } + + /** + * Delete a file on the share + * + * @param string $path + * @return bool + */ + public function del($path) { + return $this->simpleCommand('del', $path); + } + + /** + * Rename a remote file + * + * @param string $from + * @param string $to + * @return bool + */ + public function rename($from, $to) { + $path1 = $this->escapePath($from); + $path2 = $this->escapePath($to); + $cmd = 'rename ' . $path1 . ' ' . $path2; + $output = $this->execute($cmd); + return $this->parseOutput($output); + } + + /** + * Upload a local file + * + * @param string $source local file + * @param string $target remove file + * @return bool + */ + public function put($source, $target) { + $path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping + $path2 = $this->escapePath($target); + $output = $this->execute('put ' . $path1 . ' ' . $path2); + return $this->parseOutput($output); + } + + /** + * Download a remote file + * + * @param string $source remove file + * @param string $target local file + * @return bool + */ + public function get($source, $target) { + $path1 = $this->escapePath($source); + $path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping + $output = $this->execute('get ' . $path1 . ' ' . $path2); + return $this->parseOutput($output); + } + + /** + * @return Server + */ + public function getServer() { + return $this->server; + } + + /** + * @param string $command + * @return array + */ + protected function execute($command) { + $this->connect(); + $this->connection->write($command . PHP_EOL); + $output = $this->connection->read(); + return $output; + } + + /** + * check output for errors + * + * @param $lines + * + * @throws NotFoundException + * @throws AlreadyExistsException + * @throws AccessDeniedException + * @throws NotEmptyException + * @throws InvalidTypeException + * @throws \Exception + * @return bool + */ + protected function parseOutput($lines) { + if (count($lines) === 0) { + return true; + } else { + if (strpos($lines[0], 'does not exist')) { + throw new NotFoundException(); + } + $parts = explode(' ', $lines[0]); + $error = false; + foreach ($parts as $part) { + if (substr($part, 0, 9) === 'NT_STATUS') { + $error = $part; + } + } + switch ($error) { + case ErrorCodes::PathNotFound: + case ErrorCodes::ObjectNotFound: + case ErrorCodes::NoSuchFile: + throw new NotFoundException(); + case ErrorCodes::NameCollision: + throw new AlreadyExistsException(); + case ErrorCodes::AccessDenied: + throw new AccessDeniedException(); + case ErrorCodes::DirectoryNotEmpty: + throw new NotEmptyException(); + case ErrorCodes::FileIsADirectory: + case ErrorCodes::NotADirectory: + throw new InvalidTypeException(); + default: + throw new \Exception(); + } + } + } + + /** + * @param string $string + * @return string + */ + protected function escape($string) { + return escapeshellarg($string); + } + + /** + * @param string $path + * @return string + */ + protected function escapePath($path) { + $path = str_replace('/', '\\', $path); + $path = str_replace('"', '^"', $path); + return '"' . $path . '"'; + } + + /** + * @param string $path + * @return string + */ + protected function escapeLocalPath($path) { + $path = str_replace('"', '\"', $path); + return '"' . $path . '"'; + } + + public function __destruct() { + unset($this->connection); + } +} diff --git a/apps/files_external/3rdparty/smb4php/smb.php b/apps/files_external/3rdparty/smb4php/smb.php deleted file mode 100644 index e325506fa147..000000000000 --- a/apps/files_external/3rdparty/smb4php/smb.php +++ /dev/null @@ -1,516 +0,0 @@ - -# Copyright (c) 2012 Frank Karlitschek -# Copyright (c) 2014 Robin McCorkell -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# 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 General Public License for more details. -# -# On the official website http://www.phpclasses.org/smb4php the -# license is listed as LGPL so we assume that this is -# dual-licensed GPL/LGPL -################################################################### - -define ('SMB4PHP_VERSION', '0.8'); - -################################################################### -# CONFIGURATION SECTION - Change for your needs -################################################################### - -define ('SMB4PHP_SMBCLIENT', 'smbclient'); -define ('SMB4PHP_SMBOPTIONS', 'TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192'); -define ('SMB4PHP_AUTHMODE', 'arg'); # set to 'env' to use USER enviroment variable - -################################################################### -# SMB - commands that does not need an instance -################################################################### - -$GLOBALS['__smb_cache'] = array ('stat' => array (), 'dir' => array ()); - -class smb { - - private static $regexp = array ( - '^added interface ip=(.*) bcast=(.*) nmask=(.*)$' => 'skip', - 'Anonymous login successful' => 'skip', - '^Domain=\[(.*)\] OS=\[(.*)\] Server=\[(.*)\]$' => 'skip', - '^\tSharename[ ]+Type[ ]+Comment$' => 'shares', - '^\t---------[ ]+----[ ]+-------$' => 'skip', - '^\tServer [ ]+Comment$' => 'servers', - '^\t---------[ ]+-------$' => 'skip', - '^\tWorkgroup[ ]+Master$' => 'workg', - '^\t(.*)[ ]+(Disk|IPC)[ ]+IPC.*$' => 'skip', - '^\tIPC\\\$(.*)[ ]+IPC' => 'skip', - '^\t(.*)[ ]+(Disk)[ ]+(.*)$' => 'share', - '^\t(.*)[ ]+(Printer)[ ]+(.*)$' => 'skip', - '([0-9]+) blocks of size ([0-9]+)\. ([0-9]+) blocks available' => 'skip', - 'Got a positive name query response from ' => 'skip', - '^(session setup failed): (.*)$' => 'error', - '^(.*): ERRSRV - ERRbadpw' => 'error', - '^Error returning browse list: (.*)$' => 'error', - '^tree connect failed: (.*)$' => 'error', - '^(Connection to .* failed)(.*)$' => 'error-connect', - '^NT_STATUS_(.*) ' => 'error', - '^NT_STATUS_(.*)\$' => 'error', - 'ERRDOS - ERRbadpath \((.*).\)' => 'error', - 'cd (.*): (.*)$' => 'error', - '^cd (.*): NT_STATUS_(.*)' => 'error', - '^\t(.*)$' => 'srvorwg', - '^([0-9]+)[ ]+([0-9]+)[ ]+(.*)$' => 'skip', - '^Job ([0-9]+) cancelled' => 'skip', - '^[ ]+(.*)[ ]+([0-9]+)[ ]+(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[ ](Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ ]+([0-9]+)[ ]+([0-9]{2}:[0-9]{2}:[0-9]{2})[ ]([0-9]{4})$' => 'files', - '^message start: ERRSRV - (ERRmsgoff)' => 'error' - ); - - function getRegexp() { - return self::$regexp; - } - - function parse_url ($url) { - $pu = parse_url (trim($url)); - foreach (array ('domain', 'user', 'pass', 'host', 'port', 'path') as $i) { - if (! isset($pu[$i])) { - $pu[$i] = ''; - } - } - if (count ($userdomain = explode (';', urldecode ($pu['user']))) > 1) { - @list ($pu['domain'], $pu['user']) = $userdomain; - } - $path = preg_replace (array ('/^\//', '/\/$/'), '', urldecode ($pu['path'])); - list ($pu['share'], $pu['path']) = (preg_match ('/^([^\/]+)\/(.*)/', $path, $regs)) - ? array ($regs[1], preg_replace ('/\//', '\\', $regs[2])) - : array ($path, ''); - $pu['type'] = $pu['path'] ? 'path' : ($pu['share'] ? 'share' : ($pu['host'] ? 'host' : '**error**')); - if (! ($pu['port'] = intval(@$pu['port']))) { - $pu['port'] = 139; - } - - // decode user and password - $pu['user'] = urldecode($pu['user']); - $pu['pass'] = urldecode($pu['pass']); - return $pu; - } - - - function look ($purl) { - return smb::client ('-L ' . escapeshellarg ($purl['host']), $purl); - } - - - function execute ($command, $purl, $regexp = NULL) { - return smb::client ('-d 0 ' - . escapeshellarg ('//' . $purl['host'] . '/' . $purl['share']) - . ' -c ' . escapeshellarg ($command), $purl, $regexp - ); - } - - function client ($params, $purl, $regexp = NULL) { - - if ($regexp === NULL) $regexp = smb::$regexp; - - if (SMB4PHP_AUTHMODE == 'env') { - putenv("USER={$purl['user']}%{$purl['pass']}"); - $auth = ''; - } else { - $auth = ($purl['user'] <> '' ? (' -U ' . escapeshellarg ($purl['user'] . '%' . $purl['pass'])) : ''); - } - if ($purl['domain'] <> '') { - $auth .= ' -W ' . escapeshellarg ($purl['domain']); - } - $port = ($purl['port'] <> 139 ? ' -p ' . escapeshellarg ($purl['port']) : ''); - $options = '-O ' . escapeshellarg(SMB4PHP_SMBOPTIONS); - - // this put env is necessary to read the output of smbclient correctly - $old_locale = getenv('LC_ALL'); - putenv('LC_ALL=en_US.UTF-8'); - $output = popen ('TZ=UTC '.SMB4PHP_SMBCLIENT." -N {$auth} {$options} {$port} {$options} {$params} 2>/dev/null", 'r'); - $gotInfo = false; - $info = array (); - $info['info']= array (); - $mode = ''; - while ($line = fgets ($output, 4096)) { - list ($tag, $regs, $i) = array ('skip', array (), array ()); - reset ($regexp); - foreach ($regexp as $r => $t) if (preg_match ('/'.$r.'/', $line, $regs)) { - $tag = $t; - break; - } - switch ($tag) { - case 'skip': continue; - case 'shares': $mode = 'shares'; break; - case 'servers': $mode = 'servers'; break; - case 'workg': $mode = 'workgroups'; break; - case 'share': - list($name, $type) = array ( - trim(substr($line, 1, 15)), - trim(strtolower(substr($line, 17, 10))) - ); - $i = ($type <> 'disk' && preg_match('/^(.*) Disk/', $line, $regs)) - ? array(trim($regs[1]), 'disk') - : array($name, 'disk'); - break; - case 'srvorwg': - list ($name, $master) = array ( - strtolower(trim(substr($line,1,21))), - strtolower(trim(substr($line, 22))) - ); - $i = ($mode == 'servers') ? array ($name, "server") : array ($name, "workgroup", $master); - break; - case 'files': - list ($attr, $name) = preg_match ("/^(.*)[ ]+([D|A|H|N|S|R]+)$/", trim ($regs[1]), $regs2) - ? array (trim ($regs2[2]), trim ($regs2[1])) - : array ('', trim ($regs[1])); - list ($his, $im) = array ( - explode(':', $regs[6]), 1 + strpos("JanFebMarAprMayJunJulAugSepOctNovDec", $regs[4]) / 3); - $i = ($name <> '.' && $name <> '..') - ? array ( - $name, - (strpos($attr,'D') === FALSE) ? 'file' : 'folder', - 'attr' => $attr, - 'size' => intval($regs[2]), - 'time' => mktime ($his[0], $his[1], $his[2], $im, $regs[5], $regs[7]) - ) - : array(); - break; - case 'error': - if(substr($regs[0],0,22)=='NT_STATUS_NO_SUCH_FILE'){ - return false; - }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_NAME_COLLISION'){ - return false; - }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_PATH_NOT_FOUND'){ - return false; - }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_NAME_NOT_FOUND'){ - return false; - }elseif(substr($regs[0],0,29)=='NT_STATUS_FILE_IS_A_DIRECTORY'){ - return false; - } - trigger_error($regs[0].' params('.$params.')', E_USER_ERROR); - case 'error-connect': - // connection error can happen after obtaining share list if - // NetBIOS is disabled/blocked on the target server, - // in which case we keep the info and continue - if (!$gotInfo) { - return false; - } - } - if ($i) switch ($i[1]) { - case 'file': - case 'folder': $info['info'][$i[0]] = $i; - case 'disk': - case 'server': - case 'workgroup': $info[$i[1]][] = $i[0]; - $gotInfo = true; - } - } - pclose($output); - - - // restore previous locale - if ($old_locale===false) { - putenv('LC_ALL'); - } else { - putenv('LC_ALL='.$old_locale); - } - - return $info; - } - - - # stats - - function url_stat ($url, $flags = STREAM_URL_STAT_LINK) { - if ($s = smb::getstatcache($url)) { - return $s; - } - list ($stat, $pu) = array (false, smb::parse_url ($url)); - switch ($pu['type']) { - case 'host': - if ($o = smb::look ($pu)) - $stat = stat ("/tmp"); - else - trigger_error ("url_stat(): list failed for host '{$pu['host']}'", E_USER_WARNING); - break; - case 'share': - if (smb::execute("ls", $pu)) - $stat = stat ("/tmp"); - else - trigger_error ("url_stat(): disk resource '{$pu['share']}' not found in '{$pu['host']}'", E_USER_WARNING); - break; - case 'path': - if ($o = smb::execute ('dir "'.$pu['path'].'"', $pu)) { - $p = explode('\\', $pu['path']); - $name = $p[count($p)-1]; - if (isset ($o['info'][$name])) { - $stat = smb::addstatcache ($url, $o['info'][$name]); - } else { - trigger_error ("url_stat(): path '{$pu['path']}' not found", E_USER_WARNING); - } - } else { - return false; -// trigger_error ("url_stat(): dir failed for path '{$pu['path']}'", E_USER_WARNING); - } - break; - default: trigger_error ('error in URL', E_USER_ERROR); - } - return $stat; - } - - function addstatcache ($url, $info) { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - $is_file = (strpos ($info['attr'],'D') === FALSE); - $s = ($is_file) ? stat ('/etc/passwd') : stat ('/tmp'); - $s[7] = $s['size'] = $info['size']; - $s[8] = $s[9] = $s[10] = $s['atime'] = $s['mtime'] = $s['ctime'] = $info['time']; - return $__smb_cache['stat'][$url] = $s; - } - - function getstatcache ($url) { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - return isset ($__smb_cache['stat'][$url]) ? $__smb_cache['stat'][$url] : FALSE; - } - - function clearstatcache ($url='') { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - if ($url == '') $__smb_cache['stat'] = array (); else unset ($__smb_cache['stat'][$url]); - } - - - # commands - - function unlink ($url) { - $pu = smb::parse_url($url); - if ($pu['type'] <> 'path') trigger_error('unlink(): error in URL', E_USER_ERROR); - smb::clearstatcache ($url); - smb_stream_wrapper::cleardircache (dirname($url)); - return smb::execute ('del "'.$pu['path'].'"', $pu); - } - - function rename ($url_from, $url_to) { - $replace = false; - list ($from, $to) = array (smb::parse_url($url_from), smb::parse_url($url_to)); - if ($from['host'] <> $to['host'] || - $from['share'] <> $to['share'] || - $from['user'] <> $to['user'] || - $from['pass'] <> $to['pass'] || - $from['domain'] <> $to['domain']) { - trigger_error('rename(): FROM & TO must be in same server-share-user-pass-domain', E_USER_ERROR); - } - if ($from['type'] <> 'path' || $to['type'] <> 'path') { - trigger_error('rename(): error in URL', E_USER_ERROR); - } - smb::clearstatcache ($url_from); - $cmd = ''; - // check if target file exists - if (smb::url_stat($url_to)) { - // delete target file first - $cmd = 'del "' . $to['path'] . '"; '; - $replace = true; - } - $cmd .= 'rename "' . $from['path'] . '" "' . $to['path'] . '"'; - $result = smb::execute($cmd, $to); - if ($replace) { - // clear again, else the cache will return the info - // from the old file - smb::clearstatcache ($url_to); - } - return $result !== false; - } - - function mkdir ($url, $mode, $options) { - $pu = smb::parse_url($url); - if ($pu['type'] <> 'path') trigger_error('mkdir(): error in URL', E_USER_ERROR); - return smb::execute ('mkdir "'.$pu['path'].'"', $pu)!==false; - } - - function rmdir ($url) { - $pu = smb::parse_url($url); - if ($pu['type'] <> 'path') trigger_error('rmdir(): error in URL', E_USER_ERROR); - smb::clearstatcache ($url); - smb_stream_wrapper::cleardircache (dirname($url)); - return smb::execute ('rmdir "'.$pu['path'].'"', $pu)!==false; - } - -} - -################################################################### -# SMB_STREAM_WRAPPER - class to be registered for smb:// URLs -################################################################### - -class smb_stream_wrapper extends smb { - - # variables - - private $stream, $url, $parsed_url = array (), $mode, $tmpfile; - private $need_flush = FALSE; - private $dir = array (), $dir_index = -1; - - - # directories - - function dir_opendir ($url, $options) { - if ($d = $this->getdircache ($url)) { - $this->dir = $d; - $this->dir_index = 0; - return TRUE; - } - $pu = smb::parse_url ($url); - switch ($pu['type']) { - case 'host': - if ($o = smb::look ($pu)) { - $this->dir = $o['disk']; - $this->dir_index = 0; - } else { - trigger_error ("dir_opendir(): list failed for host '{$pu['host']}'", E_USER_WARNING); - return false; - } - break; - case 'share': - case 'path': - if (is_array($o = smb::execute ('dir "'.$pu['path'].'\*"', $pu))) { - $this->dir = array_keys($o['info']); - $this->dir_index = 0; - $this->adddircache ($url, $this->dir); - if(substr($url,-1,1)=='/'){ - $url=substr($url,0,-1); - } - foreach ($o['info'] as $name => $info) { - smb::addstatcache($url . '/' . $name, $info); - } - } else { - trigger_error ("dir_opendir(): dir failed for path '".$pu['path']."'", E_USER_WARNING); - return false; - } - break; - default: - trigger_error ('dir_opendir(): error in URL', E_USER_ERROR); - return false; - } - return TRUE; - } - - function dir_readdir () { - return ($this->dir_index < count($this->dir)) ? $this->dir[$this->dir_index++] : FALSE; - } - - function dir_rewinddir () { $this->dir_index = 0; } - - function dir_closedir () { $this->dir = array(); $this->dir_index = -1; return TRUE; } - - - # cache - - function adddircache ($url, $content) { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - return $__smb_cache['dir'][$url] = $content; - } - - function getdircache ($url) { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - return isset ($__smb_cache['dir'][$url]) ? $__smb_cache['dir'][$url] : FALSE; - } - - function cleardircache ($url='') { - $url = str_replace('//', '/', $url); - $url = rtrim($url, '/'); - global $__smb_cache; - if ($url == ''){ - $__smb_cache['dir'] = array (); - }else{ - unset ($__smb_cache['dir'][$url]); - } - } - - - # streams - - function stream_open ($url, $mode, $options, $opened_path) { - $this->url = $url; - $this->mode = $mode; - $this->parsed_url = $pu = smb::parse_url($url); - if ($pu['type'] <> 'path') trigger_error('stream_open(): error in URL', E_USER_ERROR); - switch ($mode) { - case 'r': - case 'r+': - case 'rb': - case 'a': - case 'a+': $this->tmpfile = tempnam('/tmp', 'smb.down.'); - $result = smb::execute ('get "'.$pu['path'].'" "'.$this->tmpfile.'"', $pu); - if($result === false){ - return $result; - } - break; - case 'w': - case 'w+': - case 'wb': - case 'x': - case 'x+': $this->cleardircache(); - $this->tmpfile = tempnam('/tmp', 'smb.up.'); - $this->need_flush=true; - } - $this->stream = fopen ($this->tmpfile, $mode); - return TRUE; - } - - function stream_close () { return fclose($this->stream); } - - function stream_read ($count) { return fread($this->stream, $count); } - - function stream_write ($data) { $this->need_flush = TRUE; return fwrite($this->stream, $data); } - - function stream_eof () { return feof($this->stream); } - - function stream_tell () { return ftell($this->stream); } - - // PATCH: the wrapper must return true when fseek succeeded by returning 0. - function stream_seek ($offset, $whence=null) { return fseek($this->stream, $offset, $whence) === 0; } - - function stream_flush () { - if ($this->mode <> 'r' && $this->need_flush) { - smb::clearstatcache ($this->url); - smb::execute ('put "'.$this->tmpfile.'" "'.$this->parsed_url['path'].'"', $this->parsed_url); - $this->need_flush = FALSE; - } - } - - function stream_stat () { return smb::url_stat ($this->url); } - - function __destruct () { - if ($this->tmpfile <> '') { - if ($this->need_flush) $this->stream_flush (); - unlink ($this->tmpfile); - - } - } - -} - -################################################################### -# Register 'smb' protocol ! -################################################################### - -stream_wrapper_register('smb', 'smb_stream_wrapper') - or die ('Failed to register protocol'); diff --git a/apps/files_external/appinfo/app.php b/apps/files_external/appinfo/app.php index aeb7a2cb23a8..fbb0130f2070 100644 --- a/apps/files_external/appinfo/app.php +++ b/apps/files_external/appinfo/app.php @@ -21,6 +21,8 @@ OC::$CLASSPATH['OC\Files\Storage\iRODS'] = 'files_external/lib/irods.php'; OC::$CLASSPATH['OC_Mount_Config'] = 'files_external/lib/config.php'; +OC::$loader->registerPrefix('Icewind\\SMB', 'files_external/3rdparty/smb'); + OCP\App::registerAdmin('files_external', 'settings'); if (OCP\Config::getAppValue('files_external', 'allow_user_mounting', 'yes') == 'yes') { OCP\App::registerPersonal('files_external', 'personal'); diff --git a/apps/files_external/lib/smb.php b/apps/files_external/lib/smb.php index f3f3b3ed7f39..5b65f051d723 100644 --- a/apps/files_external/lib/smb.php +++ b/apps/files_external/lib/smb.php @@ -8,130 +8,286 @@ namespace OC\Files\Storage; -require_once __DIR__ . '/../3rdparty/smb4php/smb.php'; +use OC\Files\Stream\Dir; +use \Icewind\SMB\AlreadyExistsException; +use \Icewind\SMB\NotFoundException; +use \Icewind\SMB\Server; -class SMB extends \OC\Files\Storage\StreamWrapper{ - private $password; - private $user; - private $host; - private $root; +class SMB extends Common { + /** + * @var \Icewind\SMB\Share $share + */ private $share; + /** + * @var \Icewind\SMB\Server $server + */ + private $server; + + private $dirCache = array(); + + private static $tempFiles = array(); + public function __construct($params) { if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) { - $this->host=$params['host']; - $this->user=$params['user']; - $this->password=$params['password']; - $this->share=$params['share']; - $this->root=isset($params['root'])?$params['root']:'/'; - if ( ! $this->root || $this->root[0]!='/') { - $this->root='/'.$this->root; - } - if (substr($this->root, -1, 1)!='/') { - $this->root.='/'; - } - if ( ! $this->share || $this->share[0]!='/') { - $this->share='/'.$this->share; + $params['share'] = trim($params['share'], '/'); + $this->server = new Server($params['host'], $params['user'], $params['password']); + $this->share = $this->server->getShare($params['share']); + $this->root = isset($params['root']) ? $params['root'] : '/'; + if (!$this->root || $this->root[0] != '/') { + $this->root = '/' . $this->root; } - if (substr($this->share, -1, 1)=='/') { - $this->share = substr($this->share, 0, -1); + if (substr($this->root, -1, 1) != '/') { + $this->root .= '/'; } } else { throw new \Exception('Invalid configuration'); } } - public function getId(){ - return 'smb::' . $this->user . '@' . $this->host . '/' . $this->share . '/' . $this->root; + public function getId() { + return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '/' . $this->share->getName() . '/' . $this->root; } - public function constructUrl($path) { - if (substr($path, -1)=='/') { - $path = substr($path, 0, -1); - } - if (substr($path, 0, 1)=='/') { - $path = substr($path, 1); + /** + * @param string $path + * @return array[] + */ + private function dir($path) { + $path = trim($path, '/'); + try { + if (!isset($this->dirCache[$path])) { + $this->dirCache[$path] = $this->share->dir($path); + } + return $this->dirCache[$path]; + } catch (NotFoundException $e) { + return array(); } - // remove trailing dots which some versions of samba don't seem to like - $path = rtrim($path, '.'); - $path = urlencode($path); - $user = urlencode($this->user); - $pass = urlencode($this->password); - return 'smb://'.$user.':'.$pass.'@'.$this->host.$this->share.$this->root.$path; } - public function stat($path) { - if ( ! $path and $this->root=='/') {//mtime doesn't work for shares - $stat=stat($this->constructUrl($path)); - if (empty($stat)) { - return false; - } - $mtime=$this->shareMTime(); - $stat['mtime']=$mtime; - return $stat; - } else { - $stat = stat($this->constructUrl($path)); - - // smb4php can return an empty array if the connection could not be established - if (empty($stat)) { - return false; - } - - return $stat; + private function removeFromCache($path) { + $path = trim($path, '/'); + if (isset($this->dirCache[$path])) { + unset($this->dirCache[$path]); + } + if (isset($this->dirCache[dirname($path)])) { + unset($this->dirCache[dirname($path)]); } } - /** - * Unlinks file or directory - * @param string @path - */ - public function unlink($path) { - if ($this->is_dir($path)) { - $this->rmdir($path); - } - else { - $url = $this->constructUrl($path); - unlink($url); - clearstatcache(false, $url); + public function file_put_contents($path, $data) { + $path = trim($path, '/'); + $tmpFile = \OCP\Files::tmpFile(); + file_put_contents($tmpFile, $data); + $this->share->put($tmpFile, $this->root . $path); + $this->removeFromCache($this->root . $path); + unlink($tmpFile); + } + + public function stat($path) { + $path = trim($path, '/'); + try { + if (!$path and $this->root === '/') { //mtime doesn't work for shares + return array('mtime' => $this->shareMTime(), 'size' => 0, 'type' => 'dir'); + } else { + $parent = dirname($this->root . $path); + $files = $this->dir($parent); + if (isset($files[basename($this->root . $path)])) { + $file = $files[basename($this->root . $path)]; + if (isset($file['time'])) { + $file['mtime'] = $file['time']; + } + return $file; + } else { + return false; + } + } + } catch (NotFoundException $e) { + return false; } - // smb4php still returns false even on success so - // check here whether file was really deleted - return !file_exists($path); } /** * check if a file or folder has been updated since $time + * * @param string $path * @param int $time * @return bool */ - public function hasUpdated($path,$time) { - if(!$path and $this->root=='/') { - // mtime doesn't work for shares, but giving the nature of the backend, - // doing a full update is still just fast enough - return true; - } else { - $actualTime=$this->filemtime($path); - return $actualTime>$time; - } + public function hasUpdated($path, $time) { + // there is no reliable way to check this and updating the file/folder takes the same amount of requests as checking the mtime + return true; } /** * get the best guess for the modification time of the share */ private function shareMTime() { - $dh=$this->opendir(''); - $lastCtime=0; - if(is_resource($dh)) { - while (($file = readdir($dh)) !== false) { - if ($file!='.' and $file!='..') { - $ctime=$this->filemtime($file); - if ($ctime>$lastCtime) { - $lastCtime=$ctime; - } + $files = $this->dir($this->root); + $lastMtime = 0; + foreach ($files as $file) { + if ($file['time'] > $lastMtime) { + $lastMtime = $file['time']; + } + } + return $lastMtime; + } + + public function writeBack($tmpFile) { + if (isset(self::$tempFiles[$tmpFile])) { + $this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]); + unlink($tmpFile); + } + } + + public function fopen($path, $mode) { + $path = trim($path, '/'); + $tmpFile = \OCP\Files::tmpFile(); + try { + $this->share->get($this->root . $path, $tmpFile); + } catch (NotFoundException $e) { + if ($mode === 'r' or $mode === 'rb' or $mode === 'rw' or $mode === 'rwb') { + return false; + } + } + if ($mode === 'r' or $mode === 'rb') { + return fopen($tmpFile, $mode); + } else { + self::$tempFiles[$tmpFile] = $path; + \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); + return fopen('close://' . $tmpFile, $mode); + } + } + + public function rename($source, $target) { + try { + $result = $this->share->rename($this->root . $source, $this->root . $target); + } catch (AlreadyExistsException $e) { + $this->unlink($target); + $result = $this->share->rename($this->root . $source, $this->root . $target); + } + $this->removeFromCache($this->root . $source); + $this->removeFromCache($this->root . $target); + return $result; + } + + public function copy($source, $target) { + $tmpFile = \OCP\Files::tmpFile(); + $this->share->get($this->root . $source, $tmpFile); + $this->share->put($tmpFile, $this->root . $target); + unlink($tmpFile); + $this->removeFromCache($this->root . $target); + } + + public function file_exists($path) { + $path = trim($path, '/'); + try { + $parent = dirname($this->root . $path); + $files = $this->dir($parent); + return isset($files[basename($this->root . $path)]); + } catch (NotFoundException $e) { + return false; + } + } + + public function touch($path, $time = null) { + if (!is_null($time)) { + return false; + } + if (!file_exists($path)) { + $parent = trim(dirname($this->root . $path), '/'); + $this->dirCache[$parent][basename($path)] = array('size' => 0, 'mtime' => time(), 'type' => 'file'); + return true; + } else { + return false; + } + } + + public function uploadFile($path, $target) { + $this->removeFromCache($this->root . $target); + $this->share->put($path, $this->root . $target); + } + + public function getMimeType($path) { + if ($this->is_dir($path)) { + return 'httpd/unix-directory'; + } + if (!$this->file_exists($path)) { + return false; + } + return \OC_Helper::getFileNameMimeType($path); + } + + public function isReadable($path) { + return $this->file_exists($path); + } + + public function isUpdatable($path) { + return $this->file_exists($path); + } + + public function mkdir($path) { + $path = trim($path, '/'); + try { + if (!$this->file_exists($path)) { + $this->removeFromCache($this->root . $path); + $this->share->mkdir($this->root . $path); + return true; + } else { + return false; + } + } catch (NotFoundException $e) { + return false; + } + } + + public function rmdir($path) { + $path = trim($path, '/'); + try { + $content = $this->dir($this->root . $path); + foreach ($content as $name => $stat) { + if ($stat['type'] === 'file') { + $this->unlink($path . '/' . $name); + } else { + $this->rmdir($path . '/' . $name); } } + $this->share->rmdir($this->root . $path); + $this->removeFromCache($this->root . $path); + return true; + } catch (NotFoundException $e) { + return false; } - return $lastCtime; + } + + public function unlink($path) { + $path = trim($path, '/'); + try { + if ($this->is_dir($path)) { + return $this->rmdir($path); + } else { + $this->removeFromCache($this->root . $path); + return $this->share->del($this->root . $path); + } + } catch (NotFoundException $e) { + return false; + } + } + + public function filetype($path) { + $path = trim($path, '/'); + $stat = $this->stat($path); + if ($stat) { + return $stat['type']; + } else { + return false; + } + } + + public function opendir($path) { + $path = trim($path, '/'); + $files = $this->dir($this->root . $path); + Dir::register('smb:' . $path, array_keys($files)); + return opendir('fakedir://smb:' . $path); } } diff --git a/apps/files_external/tests/smb.php b/apps/files_external/tests/smb.php index 199e35af6763..47cd2b9690de 100644 --- a/apps/files_external/tests/smb.php +++ b/apps/files_external/tests/smb.php @@ -25,7 +25,7 @@ public function setUp() { public function tearDown() { if ($this->instance) { - \OCP\Files::rmdirr($this->instance->constructUrl('')); + $this->instance->rmdir(''); } }