smb.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. ###################################################################
  3. # smb.php
  4. # This class implements a SMB stream wrapper based on 'smbclient'
  5. #
  6. # Date: lun oct 22 10:35:35 CEST 2007
  7. #
  8. # Homepage: http://www.phpclasses.org/smb4php
  9. #
  10. # Copyright (c) 2007 Victor M. Varela <vmvarela@gmail.com>
  11. # Copyright (c) 2012 Frank Karlitschek <frank@owncloud.org>
  12. # Copyright (c) 2014 Robin McCorkell <rmccorkell@karoshi.org.uk>
  13. #
  14. # This program is free software; you can redistribute it and/or
  15. # modify it under the terms of the GNU General Public License
  16. # as published by the Free Software Foundation; either version 2
  17. # of the License, or (at your option) any later version.
  18. #
  19. # This program is distributed in the hope that it will be useful,
  20. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. # GNU General Public License for more details.
  23. #
  24. # On the official website http://www.phpclasses.org/smb4php the
  25. # license is listed as LGPL so we assume that this is
  26. # dual-licensed GPL/LGPL
  27. ###################################################################
  28. define ('SMB4PHP_VERSION', '0.8');
  29. ###################################################################
  30. # CONFIGURATION SECTION - Change for your needs
  31. ###################################################################
  32. define ('SMB4PHP_SMBCLIENT', 'smbclient');
  33. define ('SMB4PHP_SMBOPTIONS', 'TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192');
  34. define ('SMB4PHP_AUTHMODE', 'arg'); # set to 'env' to use USER enviroment variable
  35. ###################################################################
  36. # SMB - commands that does not need an instance
  37. ###################################################################
  38. $GLOBALS['__smb_cache'] = array ('stat' => array (), 'dir' => array ());
  39. class smb {
  40. private static $regexp = array (
  41. '^added interface ip=(.*) bcast=(.*) nmask=(.*)$' => 'skip',
  42. 'Anonymous login successful' => 'skip',
  43. '^Domain=\[(.*)\] OS=\[(.*)\] Server=\[(.*)\]$' => 'skip',
  44. '^\tSharename[ ]+Type[ ]+Comment$' => 'shares',
  45. '^\t---------[ ]+----[ ]+-------$' => 'skip',
  46. '^\tServer [ ]+Comment$' => 'servers',
  47. '^\t---------[ ]+-------$' => 'skip',
  48. '^\tWorkgroup[ ]+Master$' => 'workg',
  49. '^\t(.*)[ ]+(Disk|IPC)[ ]+IPC.*$' => 'skip',
  50. '^\tIPC\\\$(.*)[ ]+IPC' => 'skip',
  51. '^\t(.*)[ ]+(Disk)[ ]+(.*)$' => 'share',
  52. '^\t(.*)[ ]+(Printer)[ ]+(.*)$' => 'skip',
  53. '([0-9]+) blocks of size ([0-9]+)\. ([0-9]+) blocks available' => 'skip',
  54. 'Got a positive name query response from ' => 'skip',
  55. '^(session setup failed): (.*)$' => 'error',
  56. '^(.*): ERRSRV - ERRbadpw' => 'error',
  57. '^Error returning browse list: (.*)$' => 'error',
  58. '^tree connect failed: (.*)$' => 'error',
  59. '^(Connection to .* failed)(.*)$' => 'error-connect',
  60. '^NT_STATUS_(.*) ' => 'error',
  61. '^NT_STATUS_(.*)\$' => 'error',
  62. 'ERRDOS - ERRbadpath \((.*).\)' => 'error',
  63. 'cd (.*): (.*)$' => 'error',
  64. '^cd (.*): NT_STATUS_(.*)' => 'error',
  65. '^\t(.*)$' => 'srvorwg',
  66. '^([0-9]+)[ ]+([0-9]+)[ ]+(.*)$' => 'skip',
  67. '^Job ([0-9]+) cancelled' => 'skip',
  68. '^[ ]+(.*)[ ]+([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',
  69. '^message start: ERRSRV - (ERRmsgoff)' => 'error'
  70. );
  71. function getRegexp() {
  72. return self::$regexp;
  73. }
  74. function parse_url ($url) {
  75. $pu = parse_url (trim($url));
  76. foreach (array ('domain', 'user', 'pass', 'host', 'port', 'path') as $i) {
  77. if (! isset($pu[$i])) {
  78. $pu[$i] = '';
  79. }
  80. }
  81. if (count ($userdomain = explode (';', urldecode ($pu['user']))) > 1) {
  82. @list ($pu['domain'], $pu['user']) = $userdomain;
  83. }
  84. $path = preg_replace (array ('/^\//', '/\/$/'), '', urldecode ($pu['path']));
  85. list ($pu['share'], $pu['path']) = (preg_match ('/^([^\/]+)\/(.*)/', $path, $regs))
  86. ? array ($regs[1], preg_replace ('/\//', '\\', $regs[2]))
  87. : array ($path, '');
  88. $pu['type'] = $pu['path'] ? 'path' : ($pu['share'] ? 'share' : ($pu['host'] ? 'host' : '**error**'));
  89. if (! ($pu['port'] = intval(@$pu['port']))) {
  90. $pu['port'] = 139;
  91. }
  92. // decode user and password
  93. $pu['user'] = urldecode($pu['user']);
  94. $pu['pass'] = urldecode($pu['pass']);
  95. return $pu;
  96. }
  97. function look ($purl) {
  98. return smb::client ('-L ' . escapeshellarg ($purl['host']), $purl);
  99. }
  100. function execute ($command, $purl, $regexp = NULL) {
  101. return smb::client ('-d 0 '
  102. . escapeshellarg ('//' . $purl['host'] . '/' . $purl['share'])
  103. . ' -c ' . escapeshellarg ($command), $purl, $regexp
  104. );
  105. }
  106. function client ($params, $purl, $regexp = NULL) {
  107. if ($regexp === NULL) $regexp = smb::$regexp;
  108. if (SMB4PHP_AUTHMODE == 'env') {
  109. putenv("USER={$purl['user']}%{$purl['pass']}");
  110. $auth = '';
  111. } else {
  112. $auth = ($purl['user'] <> '' ? (' -U ' . escapeshellarg ($purl['user'] . '%' . $purl['pass'])) : '');
  113. }
  114. if ($purl['domain'] <> '') {
  115. $auth .= ' -W ' . escapeshellarg ($purl['domain']);
  116. }
  117. $port = ($purl['port'] <> 139 ? ' -p ' . escapeshellarg ($purl['port']) : '');
  118. $options = '-O ' . escapeshellarg(SMB4PHP_SMBOPTIONS);
  119. // this put env is necessary to read the output of smbclient correctly
  120. $old_locale = getenv('LC_ALL');
  121. putenv('LC_ALL=en_US.UTF-8');
  122. $output = popen ('TZ=UTC '.SMB4PHP_SMBCLIENT." -N {$auth} {$options} {$port} {$options} {$params} 2>/dev/null", 'r');
  123. $gotInfo = false;
  124. $info = array ();
  125. $info['info']= array ();
  126. $mode = '';
  127. while ($line = fgets ($output, 4096)) {
  128. list ($tag, $regs, $i) = array ('skip', array (), array ());
  129. reset ($regexp);
  130. foreach ($regexp as $r => $t) if (preg_match ('/'.$r.'/', $line, $regs)) {
  131. $tag = $t;
  132. break;
  133. }
  134. switch ($tag) {
  135. case 'skip': continue;
  136. case 'shares': $mode = 'shares'; break;
  137. case 'servers': $mode = 'servers'; break;
  138. case 'workg': $mode = 'workgroups'; break;
  139. case 'share':
  140. list($name, $type) = array (
  141. trim(substr($line, 1, 15)),
  142. trim(strtolower(substr($line, 17, 10)))
  143. );
  144. $i = ($type <> 'disk' && preg_match('/^(.*) Disk/', $line, $regs))
  145. ? array(trim($regs[1]), 'disk')
  146. : array($name, 'disk');
  147. break;
  148. case 'srvorwg':
  149. list ($name, $master) = array (
  150. strtolower(trim(substr($line,1,21))),
  151. strtolower(trim(substr($line, 22)))
  152. );
  153. $i = ($mode == 'servers') ? array ($name, "server") : array ($name, "workgroup", $master);
  154. break;
  155. case 'files':
  156. list ($attr, $name) = preg_match ("/^(.*)[ ]+([D|A|H|N|S|R]+)$/", trim ($regs[1]), $regs2)
  157. ? array (trim ($regs2[2]), trim ($regs2[1]))
  158. : array ('', trim ($regs[1]));
  159. list ($his, $im) = array (
  160. explode(':', $regs[6]), 1 + strpos("JanFebMarAprMayJunJulAugSepOctNovDec", $regs[4]) / 3);
  161. $i = ($name <> '.' && $name <> '..')
  162. ? array (
  163. $name,
  164. (strpos($attr,'D') === FALSE) ? 'file' : 'folder',
  165. 'attr' => $attr,
  166. 'size' => intval($regs[2]),
  167. 'time' => mktime ($his[0], $his[1], $his[2], $im, $regs[5], $regs[7])
  168. )
  169. : array();
  170. break;
  171. case 'error':
  172. if(substr($regs[0],0,22)=='NT_STATUS_NO_SUCH_FILE'){
  173. return false;
  174. }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_NAME_COLLISION'){
  175. return false;
  176. }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_PATH_NOT_FOUND'){
  177. return false;
  178. }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_NAME_NOT_FOUND'){
  179. return false;
  180. }elseif(substr($regs[0],0,29)=='NT_STATUS_FILE_IS_A_DIRECTORY'){
  181. return false;
  182. }
  183. trigger_error($regs[0].' params('.$params.')', E_USER_ERROR);
  184. case 'error-connect':
  185. // connection error can happen after obtaining share list if
  186. // NetBIOS is disabled/blocked on the target server,
  187. // in which case we keep the info and continue
  188. if (!$gotInfo) {
  189. return false;
  190. }
  191. }
  192. if ($i) switch ($i[1]) {
  193. case 'file':
  194. case 'folder': $info['info'][$i[0]] = $i;
  195. case 'disk':
  196. case 'server':
  197. case 'workgroup': $info[$i[1]][] = $i[0];
  198. $gotInfo = true;
  199. }
  200. }
  201. pclose($output);
  202. // restore previous locale
  203. if ($old_locale===false) {
  204. putenv('LC_ALL');
  205. } else {
  206. putenv('LC_ALL='.$old_locale);
  207. }
  208. return $info;
  209. }
  210. # stats
  211. function url_stat ($url, $flags = STREAM_URL_STAT_LINK) {
  212. if ($s = smb::getstatcache($url)) {
  213. return $s;
  214. }
  215. list ($stat, $pu) = array (false, smb::parse_url ($url));
  216. switch ($pu['type']) {
  217. case 'host':
  218. if ($o = smb::look ($pu))
  219. $stat = stat ("/tmp");
  220. else
  221. trigger_error ("url_stat(): list failed for host '{$pu['host']}'", E_USER_WARNING);
  222. break;
  223. case 'share':
  224. if (smb::execute("ls", $pu))
  225. $stat = stat ("/tmp");
  226. else
  227. trigger_error ("url_stat(): disk resource '{$pu['share']}' not found in '{$pu['host']}'", E_USER_WARNING);
  228. break;
  229. case 'path':
  230. if ($o = smb::execute ('dir "'.$pu['path'].'"', $pu)) {
  231. $p = explode('\\', $pu['path']);
  232. $name = $p[count($p)-1];
  233. if (isset ($o['info'][$name])) {
  234. $stat = smb::addstatcache ($url, $o['info'][$name]);
  235. } else {
  236. trigger_error ("url_stat(): path '{$pu['path']}' not found", E_USER_WARNING);
  237. }
  238. } else {
  239. return false;
  240. // trigger_error ("url_stat(): dir failed for path '{$pu['path']}'", E_USER_WARNING);
  241. }
  242. break;
  243. default: trigger_error ('error in URL', E_USER_ERROR);
  244. }
  245. return $stat;
  246. }
  247. function addstatcache ($url, $info) {
  248. $url = str_replace('//', '/', $url);
  249. $url = rtrim($url, '/');
  250. global $__smb_cache;
  251. $is_file = (strpos ($info['attr'],'D') === FALSE);
  252. $s = ($is_file) ? stat ('/etc/passwd') : stat ('/tmp');
  253. $s[7] = $s['size'] = $info['size'];
  254. $s[8] = $s[9] = $s[10] = $s['atime'] = $s['mtime'] = $s['ctime'] = $info['time'];
  255. return $__smb_cache['stat'][$url] = $s;
  256. }
  257. function getstatcache ($url) {
  258. $url = str_replace('//', '/', $url);
  259. $url = rtrim($url, '/');
  260. global $__smb_cache;
  261. return isset ($__smb_cache['stat'][$url]) ? $__smb_cache['stat'][$url] : FALSE;
  262. }
  263. function clearstatcache ($url='') {
  264. $url = str_replace('//', '/', $url);
  265. $url = rtrim($url, '/');
  266. global $__smb_cache;
  267. if ($url == '') $__smb_cache['stat'] = array (); else unset ($__smb_cache['stat'][$url]);
  268. }
  269. # commands
  270. function unlink ($url) {
  271. $pu = smb::parse_url($url);
  272. if ($pu['type'] <> 'path') trigger_error('unlink(): error in URL', E_USER_ERROR);
  273. smb::clearstatcache ($url);
  274. smb_stream_wrapper::cleardircache (dirname($url));
  275. return smb::execute ('del "'.$pu['path'].'"', $pu);
  276. }
  277. function rename ($url_from, $url_to) {
  278. $replace = false;
  279. list ($from, $to) = array (smb::parse_url($url_from), smb::parse_url($url_to));
  280. if ($from['host'] <> $to['host'] ||
  281. $from['share'] <> $to['share'] ||
  282. $from['user'] <> $to['user'] ||
  283. $from['pass'] <> $to['pass'] ||
  284. $from['domain'] <> $to['domain']) {
  285. trigger_error('rename(): FROM & TO must be in same server-share-user-pass-domain', E_USER_ERROR);
  286. }
  287. if ($from['type'] <> 'path' || $to['type'] <> 'path') {
  288. trigger_error('rename(): error in URL', E_USER_ERROR);
  289. }
  290. smb::clearstatcache ($url_from);
  291. $cmd = '';
  292. // check if target file exists
  293. if (smb::url_stat($url_to)) {
  294. // delete target file first
  295. $cmd = 'del "' . $to['path'] . '"; ';
  296. $replace = true;
  297. }
  298. $cmd .= 'rename "' . $from['path'] . '" "' . $to['path'] . '"';
  299. $result = smb::execute($cmd, $to);
  300. if ($replace) {
  301. // clear again, else the cache will return the info
  302. // from the old file
  303. smb::clearstatcache ($url_to);
  304. }
  305. return $result !== false;
  306. }
  307. function mkdir ($url, $mode, $options) {
  308. $pu = smb::parse_url($url);
  309. if ($pu['type'] <> 'path') trigger_error('mkdir(): error in URL', E_USER_ERROR);
  310. return smb::execute ('mkdir "'.$pu['path'].'"', $pu)!==false;
  311. }
  312. function rmdir ($url) {
  313. $pu = smb::parse_url($url);
  314. if ($pu['type'] <> 'path') trigger_error('rmdir(): error in URL', E_USER_ERROR);
  315. smb::clearstatcache ($url);
  316. smb_stream_wrapper::cleardircache (dirname($url));
  317. return smb::execute ('rmdir "'.$pu['path'].'"', $pu)!==false;
  318. }
  319. }
  320. ###################################################################
  321. # SMB_STREAM_WRAPPER - class to be registered for smb:// URLs
  322. ###################################################################
  323. class smb_stream_wrapper extends smb {
  324. # variables
  325. private $stream, $url, $parsed_url = array (), $mode, $tmpfile;
  326. private $need_flush = FALSE;
  327. private $dir = array (), $dir_index = -1;
  328. # directories
  329. function dir_opendir ($url, $options) {
  330. if ($d = $this->getdircache ($url)) {
  331. $this->dir = $d;
  332. $this->dir_index = 0;
  333. return TRUE;
  334. }
  335. $pu = smb::parse_url ($url);
  336. switch ($pu['type']) {
  337. case 'host':
  338. if ($o = smb::look ($pu)) {
  339. $this->dir = $o['disk'];
  340. $this->dir_index = 0;
  341. } else {
  342. trigger_error ("dir_opendir(): list failed for host '{$pu['host']}'", E_USER_WARNING);
  343. return false;
  344. }
  345. break;
  346. case 'share':
  347. case 'path':
  348. if (is_array($o = smb::execute ('dir "'.$pu['path'].'\*"', $pu))) {
  349. $this->dir = array_keys($o['info']);
  350. $this->dir_index = 0;
  351. $this->adddircache ($url, $this->dir);
  352. if(substr($url,-1,1)=='/'){
  353. $url=substr($url,0,-1);
  354. }
  355. foreach ($o['info'] as $name => $info) {
  356. smb::addstatcache($url . '/' . $name, $info);
  357. }
  358. } else {
  359. trigger_error ("dir_opendir(): dir failed for path '".$pu['path']."'", E_USER_WARNING);
  360. return false;
  361. }
  362. break;
  363. default:
  364. trigger_error ('dir_opendir(): error in URL', E_USER_ERROR);
  365. return false;
  366. }
  367. return TRUE;
  368. }
  369. function dir_readdir () {
  370. return ($this->dir_index < count($this->dir)) ? $this->dir[$this->dir_index++] : FALSE;
  371. }
  372. function dir_rewinddir () { $this->dir_index = 0; }
  373. function dir_closedir () { $this->dir = array(); $this->dir_index = -1; return TRUE; }
  374. # cache
  375. function adddircache ($url, $content) {
  376. $url = str_replace('//', '/', $url);
  377. $url = rtrim($url, '/');
  378. global $__smb_cache;
  379. return $__smb_cache['dir'][$url] = $content;
  380. }
  381. function getdircache ($url) {
  382. $url = str_replace('//', '/', $url);
  383. $url = rtrim($url, '/');
  384. global $__smb_cache;
  385. return isset ($__smb_cache['dir'][$url]) ? $__smb_cache['dir'][$url] : FALSE;
  386. }
  387. function cleardircache ($url='') {
  388. $url = str_replace('//', '/', $url);
  389. $url = rtrim($url, '/');
  390. global $__smb_cache;
  391. if ($url == ''){
  392. $__smb_cache['dir'] = array ();
  393. }else{
  394. unset ($__smb_cache['dir'][$url]);
  395. }
  396. }
  397. # streams
  398. function stream_open ($url, $mode, $options, $opened_path) {
  399. $this->url = $url;
  400. $this->mode = $mode;
  401. $this->parsed_url = $pu = smb::parse_url($url);
  402. if ($pu['type'] <> 'path') trigger_error('stream_open(): error in URL', E_USER_ERROR);
  403. switch ($mode) {
  404. case 'r':
  405. case 'r+':
  406. case 'rb':
  407. case 'a':
  408. case 'a+': $this->tmpfile = tempnam('/tmp', 'smb.down.');
  409. $result = smb::execute ('get "'.$pu['path'].'" "'.$this->tmpfile.'"', $pu);
  410. if($result === false){
  411. return $result;
  412. }
  413. break;
  414. case 'w':
  415. case 'w+':
  416. case 'wb':
  417. case 'x':
  418. case 'x+': $this->cleardircache();
  419. $this->tmpfile = tempnam('/tmp', 'smb.up.');
  420. $this->need_flush=true;
  421. }
  422. $this->stream = fopen ($this->tmpfile, $mode);
  423. return TRUE;
  424. }
  425. function stream_close () { return fclose($this->stream); }
  426. function stream_read ($count) { return fread($this->stream, $count); }
  427. function stream_write ($data) { $this->need_flush = TRUE; return fwrite($this->stream, $data); }
  428. function stream_eof () { return feof($this->stream); }
  429. function stream_tell () { return ftell($this->stream); }
  430. // PATCH: the wrapper must return true when fseek succeeded by returning 0.
  431. function stream_seek ($offset, $whence=null) { return fseek($this->stream, $offset, $whence) === 0; }
  432. function stream_flush () {
  433. if ($this->mode <> 'r' && $this->need_flush) {
  434. smb::clearstatcache ($this->url);
  435. smb::execute ('put "'.$this->tmpfile.'" "'.$this->parsed_url['path'].'"', $this->parsed_url);
  436. $this->need_flush = FALSE;
  437. }
  438. }
  439. function stream_stat () { return smb::url_stat ($this->url); }
  440. function __destruct () {
  441. if ($this->tmpfile <> '') {
  442. if ($this->need_flush) $this->stream_flush ();
  443. unlink ($this->tmpfile);
  444. }
  445. }
  446. }
  447. ###################################################################
  448. # Register 'smb' protocol !
  449. ###################################################################
  450. stream_wrapper_register('smb', 'smb_stream_wrapper')
  451. or die ('Failed to register protocol');