1<?php
2/**
3 * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
4 * @author Jesús Macias <jmacias@solidgear.es>
5 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
6 * @author Juan Pablo Villafañez <jvillafanez@solidgear.es>
7 * @author Michael Gapczynski <GapczynskiM@gmail.com>
8 * @author Morris Jobke <hey@morrisjobke.de>
9 * @author Philipp Kapfer <philipp.kapfer@gmx.at>
10 * @author Robin Appelman <icewind@owncloud.com>
11 * @author Robin McCorkell <robin@mccorkell.me.uk>
12 * @author Thomas Müller <thomas.mueller@tmit.eu>
13 * @author Vincent Petry <pvince81@owncloud.com>
14 *
15 * @copyright Copyright (c) 2018, ownCloud GmbH
16 * @license AGPL-3.0
17 *
18 * This code is free software: you can redistribute it and/or modify
19 * it under the terms of the GNU Affero General Public License, version 3,
20 * as published by the Free Software Foundation.
21 *
22 * This program is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU Affero General Public License for more details.
26 *
27 * You should have received a copy of the GNU Affero General Public License, version 3,
28 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29 *
30 */
31
32namespace OCA\Files_External\Lib\Storage;
33
34use Icewind\SMB\Exception\AlreadyExistsException;
35use Icewind\SMB\Exception\ConnectException;
36use Icewind\SMB\Exception\Exception;
37use Icewind\SMB\Exception\ForbiddenException;
38use Icewind\SMB\Exception\NotFoundException;
39use Icewind\SMB\BasicAuth;
40use Icewind\SMB\IFileInfo;
41use Icewind\SMB\IServer;
42use Icewind\SMB\Native\NativeServer;
43use Icewind\SMB\Wrapped\FileInfo;
44use Icewind\SMB\ServerFactory;
45use Icewind\SMB\System;
46use Icewind\SMB\IShare;
47use Icewind\Streams\CallbackWrapper;
48use Icewind\Streams\IteratorDirectory;
49use OC\Cache\CappedMemoryCache;
50use OC\Files\Filesystem;
51use OCA\Files_External\Lib\Cache\SmbCacheWrapper;
52use OCP\Files\Storage\StorageAdapter;
53use OCP\Files\StorageNotAvailableException;
54use OCP\Util;
55
56class SMB extends StorageAdapter {
57	/** @var bool */
58	protected $logActive;
59
60	/**
61	 * @var IServer
62	 */
63	protected $server;
64
65	/**
66	 * @var IShare
67	 */
68	protected $share;
69
70	/**
71	 * @var string
72	 */
73	protected $root;
74
75	/**
76	 * @var CappedMemoryCache
77	 */
78	protected $statCache;
79
80	public function __construct($params) {
81		// log switch might be set already (from a subclass), so don't change it.
82		if (!isset($this->logActive)) {
83			$this->logActive = \OC::$server->getConfig()->getSystemValue('smb.logging.enable', false) === true;
84		}
85
86		$loggedParams = $params;
87		// remove password from log if it is set
88		if (!empty($loggedParams['password'])) {
89			$loggedParams['password'] = '***removed***';
90		}
91		$this->log('enter: '.__FUNCTION__.'('.\json_encode($loggedParams).')');
92
93		if (isset($params['host'], $params['user'], $params['password'], $params['share'])) {
94			$domain = $params['domain'] ?? '';
95
96			$auth = new BasicAuth($params['user'], $domain, $params['password']);
97			$serverFactory = new ServerFactory();
98			$this->server = $serverFactory->createServer($params['host'], $auth);
99			$this->share = $this->server->getShare(\trim($params['share'], '/'));
100
101			$shareClass = \get_class($this->share);
102			$this->log("using $shareClass for the connection");
103
104			$this->root = isset($params['root']) ? $params['root'] : '/';
105			if (!$this->root || $this->root[0] != '/') {
106				$this->root = '/' . $this->root;
107			}
108			if (\substr($this->root, -1, 1) !== '/') {
109				$this->root .= '/';
110			}
111		} else {
112			$ex = new \Exception('Invalid configuration: '.\json_encode($loggedParams));
113			$this->leave(__FUNCTION__, $ex);
114			throw $ex;
115		}
116		$this->statCache = new CappedMemoryCache();
117		$this->log('leave: '.__FUNCTION__.', getId:'.$this->getId());
118	}
119
120	public function getId(): string {
121		// FIXME: double slash to keep compatible with the old storage ids,
122		// failure to do so will lead to creation of a new storage id and
123		// loss of shares from the storage
124		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
125	}
126
127	/**
128	 * @param string $path
129	 * @return string
130	 */
131	protected function buildPath($path) {
132		$this->log('enter: '.__FUNCTION__."($path)");
133		$result = Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
134		return $this->leave(__FUNCTION__, $result);
135	}
136
137	/**
138	 * @param string $path
139	 * @return \Icewind\SMB\IFileInfo
140	 * @throws StorageNotAvailableException
141	 * @throws ForbiddenException
142	 * @throws NotFoundException
143	 */
144	protected function getFileInfo($path) {
145		$this->log('enter: '.__FUNCTION__."($path)");
146		$path = $this->buildPath($path);
147		if (!isset($this->statCache[$path])) {
148			try {
149				$this->log("stat fetching '$path'");
150				try {
151					$this->statCache[$path] = $this->share->stat($path);
152				} catch (NotFoundException $e) {
153					if ($this->share instanceof IShare) {
154						// smbclient may have problems with the allinfo cmd
155						$this->log("stat for '$path' failed, trying to read parent dir");
156						$infos = $this->share->dir(\dirname($path));
157						foreach ($infos as $fileInfo) {
158							if ($fileInfo->getName() === \basename($path)) {
159								$this->statCache[$path] = $fileInfo;
160								break;
161							}
162						}
163						if (empty($this->statCache[$path])) {
164							$this->leave(__FUNCTION__, $e);
165							throw $e;
166						}
167					} else {
168						// trust the results of libsmb
169						$this->leave(__FUNCTION__, $e);
170						throw $e;
171					}
172				}
173				if ($this->isRootDir($path) && $this->statCache[$path]->isHidden()) {
174					$this->log("unhiding stat for '$path'");
175					// make root never hidden, may happen when accessing a shared drive (mode is 22, archived and readonly - neither is true ... whatever)
176					if ($this->statCache[$path]->isReadOnly()) {
177						$mode = IFileInfo::MODE_DIRECTORY & IFileInfo::MODE_READONLY;
178					} else {
179						$mode = IFileInfo::MODE_DIRECTORY;
180					}
181					$this->statCache[$path] = new FileInfo(
182						$path,
183						'',
184						0,
185						$this->statCache[$path]->getMTime(),
186						$mode,
187						function () {
188							return [];
189						}
190					);
191				}
192			} catch (ConnectException $e) {
193				$ex = new StorageNotAvailableException(
194					$e->getMessage(),
195					$e->getCode(),
196					$e
197				);
198				$this->leave(__FUNCTION__, $ex);
199				throw $ex;
200			} catch (ForbiddenException $e) {
201				if ($this->remoteIsShare() && $this->isRootDir($path)) { //mtime may not work for share root
202					$this->log("faking stat for forbidden '$path'");
203					$this->statCache[$path] = new FileInfo(
204						$path,
205						'',
206						0,
207						$this->shareMTime(),
208						IFileInfo::MODE_DIRECTORY,
209						function () {
210							return [];
211						}
212					);
213				} else {
214					$this->leave(__FUNCTION__, $e);
215					throw $e;
216				}
217			}
218		} else {
219			$this->log("stat cache hit for '$path'");
220		}
221		$result = $this->statCache[$path];
222		return $this->leave(__FUNCTION__, $result);
223	}
224
225	/**
226	 * @param string $path
227	 * @return \Icewind\SMB\IFileInfo[]
228	 * @throws StorageNotAvailableException
229	 */
230	protected function getFolderContents($path) {
231		$this->log('enter: '.__FUNCTION__."($path)");
232		try {
233			$path = $this->buildPath($path);
234			$result = [];
235			$children = $this->share->dir($path);
236			$trimmedPath = \rtrim($path, '/');
237			foreach ($children as $fileInfo) {
238				$fullPath = "{$trimmedPath}/{$fileInfo->getName()}";
239				if (isset($this->statCache[$fullPath])) {
240					// reference in the cache might have its fileinfo's mode
241					// already resolved, so use that
242					$fileInfo = $this->statCache[$fullPath];
243				}
244				// check if the file is readable before adding it to the list
245				// can't use "isReadable" function here, use smb internals instead
246				try {
247					if ($fileInfo->isHidden()) {
248						$this->log("{$fileInfo->getName()} isn't readable, skipping", Util::DEBUG);
249					} else {
250						$result[] = $fileInfo;
251						//remember entry so we can answer file_exists and filetype without a full stat
252						$this->statCache[$fullPath] = $fileInfo;
253					}
254				} catch (NotFoundException $e) {
255					$this->swallow(__FUNCTION__, $e);
256				} catch (ForbiddenException $e) {
257					$this->swallow(__FUNCTION__, $e);
258				}
259			}
260		} catch (ConnectException $e) {
261			$ex = new StorageNotAvailableException(
262				$e->getMessage(),
263				$e->getCode(),
264				$e
265			);
266			$this->leave(__FUNCTION__, $ex);
267			throw $ex;
268		}
269		return $this->leave(__FUNCTION__, $result);
270	}
271
272	/**
273	 * @param \Icewind\SMB\IFileInfo $info
274	 * @return array
275	 */
276	protected function formatInfo($info) {
277		$result = [
278			'size' => $info->getSize(),
279			'mtime' => $info->getMTime(),
280		];
281		if ($info->isDirectory()) {
282			$result['type'] = 'dir';
283		} else {
284			$result['type'] = 'file';
285		}
286		return $result;
287	}
288
289	/**
290	 * Rename the files. If the source or the target is the root, the rename won't happen.
291	 *
292	 * @param string $source the old name of the path
293	 * @param string $target the new name of the path
294	 * @return bool true if the rename is successful, false otherwise
295	 */
296	public function rename($source, $target) {
297		$this->log("enter: rename('$source', '$target')", Util::DEBUG);
298
299		if ($this->isRootDir($source) || $this->isRootDir($target)) {
300			$this->log("refusing to rename \"$source\" to \"$target\"");
301			return $this->leave(__FUNCTION__, false);
302		}
303
304		$buildSource = $this->buildPath($source);
305		$buildTarget = $this->buildPath($target);
306		try {
307			$result = $this->share->rename($buildSource, $buildTarget);
308			if ($result) {
309				$this->removeFromCache($buildSource);
310				$this->removeFromCache($buildTarget);
311			}
312		} catch (AlreadyExistsException $e) {
313			$this->swallow(__FUNCTION__, $e);
314			if ($this->unlink($target)) {
315				$result = $this->share->rename($buildSource, $buildTarget);
316				if ($result) {
317					$this->removeFromCache($buildSource);
318					$this->removeFromCache($buildTarget);
319				}
320			} else {
321				$result = false;
322			}
323		} catch (ConnectException $e) {
324			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
325			$this->leave(__FUNCTION__, $ex);
326			throw $ex;
327		} catch (Exception $e) {
328			$this->swallow(__FUNCTION__, $e);
329			// Icewind\SMB\Exception\Exception, not a plain exception
330			if ($e->getCode() === 22) {
331				// some servers seem to return an error code 22 instead of the expected AlreadyExistException
332				if ($this->unlink($target)) {
333					$result = $this->share->rename($buildSource, $buildTarget);
334					if ($result) {
335						$this->removeFromCache($buildSource);
336						$this->removeFromCache($buildTarget);
337					}
338				} else {
339					$result = false;
340				}
341			} else {
342				$result = false;
343			}
344		} catch (\Exception $e) {
345			$this->swallow(__FUNCTION__, $e);
346			$result = false;
347		}
348		return $this->leave(__FUNCTION__, $result);
349	}
350
351	private function removeFromCache($path) {
352		// TODO The CappedCache does not really clear by prefix. It just clears all.
353		'@phan-var \OC\Cache\CappedMemoryCache $this->statCache';
354		$this->statCache->clear("$path/");
355		unset($this->statCache[$path]);
356	}
357	/**
358	 * @param string $path
359	 * @return array
360	 */
361	public function stat($path) {
362		$this->log('enter: '.__FUNCTION__."($path)");
363		try {
364			$result = $this->formatInfo($this->getFileInfo($path));
365		} catch (ConnectException $e) {
366			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
367			$this->leave(__FUNCTION__, $ex);
368			throw $ex;
369		} catch (Exception $e) {
370			$this->swallow(__FUNCTION__, $e);
371			$result = false;
372		}
373		return $this->leave(__FUNCTION__, $result);
374	}
375
376	/**
377	 * get the best guess for the modification time of the share
378	 * NOTE: modification times do not bubble up the directory tree, basically
379	 * we are just guessing a time
380	 *
381	 * @return int the calculated mtime for the folder
382	 */
383	private function shareMTime() {
384		$this->log('enter: '.__FUNCTION__, Util::DEBUG);
385		$files = $this->share->dir($this->root);
386		$result = 0;
387		foreach ($files as $fileInfo) {
388			if ($fileInfo->getMTime() > $result) {
389				$result = $fileInfo->getMTime();
390			}
391		}
392		return $this->leave(__FUNCTION__, $result);
393	}
394	/**
395	 * Check if the path is our root dir (not the smb one)
396	 *
397	 * @param string $path the path
398	 * @return bool true if it's root, false if not
399	 */
400	private function isRootDir($path) {
401		$this->log('enter: '.__FUNCTION__."($path)", Util::DEBUG);
402		$result = $path === '' || $path === '/' || $path === '.';
403		return $this->leave(__FUNCTION__, $result);
404	}
405	/**
406	 * Check if our root points to a smb share
407	 *
408	 * @return bool true if our root points to a share false otherwise
409	 */
410	private function remoteIsShare() {
411		$this->log('enter: '.__FUNCTION__, Util::DEBUG);
412		$result = $this->share->getName() && (!$this->root || $this->root === '/');
413		return $this->leave(__FUNCTION__, $result);
414	}
415	/**
416	 * @param string $path
417	 * @return bool
418	 * @throws StorageNotAvailableException
419	 */
420	public function unlink($path) {
421		$this->log('enter: '.__FUNCTION__."($path)");
422
423		if ($this->isRootDir($path)) {
424			$this->log("refusing to unlink \"$path\"");
425			return $this->leave(__FUNCTION__, false);
426		}
427
428		$result = false;
429		try {
430			if ($this->is_dir($path)) {
431				$result = $this->rmdir($path);
432			} else {
433				$buildPath = $this->buildPath($path);
434				$this->share->del($buildPath);
435				unset($this->statCache[$buildPath]);
436				$result = true;
437			}
438		} catch (ConnectException $e) {
439			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
440			$this->leave(__FUNCTION__, $ex);
441			throw $ex;
442		} catch (Exception $e) {
443			$this->swallow(__FUNCTION__, $e);
444		}
445		return $this->leave(__FUNCTION__, $result);
446	}
447
448	/**
449	 * check if a file or folder has been updated since $time
450	 *
451	 * @param string $path
452	 * @param int $time
453	 * @return bool
454	 */
455	public function hasUpdated($path, $time) {
456		$this->log('enter: '.__FUNCTION__."($path, $time)");
457		$actualTime = $this->filemtime($path);
458		$result = $actualTime > $time;
459		return $this->leave(__FUNCTION__, $result);
460	}
461
462	/**
463	 * @param string $path
464	 * @param string $mode
465	 * @return resource
466	 * @throws StorageNotAvailableException
467	 */
468	public function fopen($path, $mode) {
469		$this->log('enter: '.__FUNCTION__."($path, $mode)");
470		$fullPath = $this->buildPath($path);
471		$result = false;
472		try {
473			switch ($mode) {
474				case 'r':
475				case 'rb':
476					if ($this->file_exists($path)) {
477						$result = $this->share->read($fullPath);
478					}
479					break;
480				case 'w':
481				case 'wb':
482					$source = $this->share->write($fullPath);
483					$result = CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
484						unset($this->statCache[$fullPath]);
485					});
486					break;
487				case 'a':
488				case 'ab':
489				case 'r+':
490				case 'w+':
491				case 'wb+':
492				case 'a+':
493				case 'x':
494				case 'x+':
495				case 'c':
496				case 'c+':
497					//emulate these
498					if (\strrpos($path, '.') !== false) {
499						$ext = \substr($path, \strrpos($path, '.'));
500					} else {
501						$ext = '';
502					}
503					if ($this->file_exists($path)) {
504						if (!$this->isUpdatable($path)) {
505							break;
506						}
507						$tmpFile = $this->getCachedFile($path);
508					} else {
509						if (!$this->isCreatable(\dirname($path))) {
510							break;
511						}
512						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
513					}
514					$source = \fopen($tmpFile, $mode);
515					$share = $this->share;
516					$result = CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
517						unset($this->statCache[$fullPath]);
518						$share->put($tmpFile, $fullPath);
519						\unlink($tmpFile);
520					});
521			}
522		} catch (ConnectException $e) {
523			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
524			$this->leave(__FUNCTION__, $ex);
525			throw $ex;
526		} catch (Exception $e) {
527			$this->swallow(__FUNCTION__, $e);
528		}
529		return $this->leave(__FUNCTION__, $result);
530	}
531
532	public function rmdir($path) {
533		$this->log('enter: '.__FUNCTION__."($path)");
534
535		if ($this->isRootDir($path)) {
536			$this->log("refusing to delete \"$path\"");
537			return $this->leave(__FUNCTION__, false);
538		}
539
540		$result = false;
541		try {
542			$buildPath = $this->buildPath($path);
543			$content = $this->share->dir($buildPath);
544			foreach ($content as $file) {
545				if ($file->isDirectory()) {
546					$this->rmdir($path . '/' . $file->getName());
547				} else {
548					$this->share->del($file->getPath());
549				}
550			}
551			$this->share->rmdir($buildPath);
552			$this->removeFromCache($buildPath);
553			$result = true;
554		} catch (ConnectException $e) {
555			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
556			$this->leave(__FUNCTION__, $ex);
557			throw $ex;
558		} catch (Exception $e) {
559			$this->swallow(__FUNCTION__, $e);
560		}
561		return $this->leave(__FUNCTION__, $result);
562	}
563
564	public function touch($path, $time = null) {
565		$this->log('enter: '.__FUNCTION__."($path, $time)");
566		$result = false;
567		try {
568			if (!$this->file_exists($path)) {
569				$fh = $this->share->write($this->buildPath($path));
570				\fclose($fh);
571				$result = true;
572			}
573		} catch (ConnectException $e) {
574			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
575			$this->leave(__FUNCTION__, $ex);
576			throw $ex;
577		} catch (Exception $e) {
578			$this->swallow(__FUNCTION__, $e);
579		}
580		return $this->leave(__FUNCTION__, $result);
581	}
582
583	public function opendir($path) {
584		$this->log('enter: '.__FUNCTION__."($path)");
585		$result = false;
586		try {
587			$files = $this->getFolderContents($path);
588			$names = \array_map(function ($info) {
589				/** @var \Icewind\SMB\IFileInfo $info */
590				return $info->getName();
591			}, $files);
592			$result = IteratorDirectory::wrap($names);
593		} catch (ConnectException $e) {
594			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
595			$this->leave(__FUNCTION__, $ex);
596			throw $ex;
597		} catch (Exception $e) {
598			$this->swallow(__FUNCTION__, $e);
599		}
600		return $this->leave(__FUNCTION__, $result);
601	}
602
603	public function filetype($path) {
604		$this->log('enter: '.__FUNCTION__."($path)");
605		$result = false;
606		try {
607			$result = $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
608		} catch (ConnectException $e) {
609			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
610			$this->leave(__FUNCTION__, $ex);
611			throw $ex;
612		} catch (Exception $e) {
613			$this->swallow(__FUNCTION__, $e);
614		}
615		return $this->leave(__FUNCTION__, $result);
616	}
617
618	public function mkdir($path) {
619		$this->log('enter: '.__FUNCTION__."($path)");
620		$result = false;
621		$path = $this->buildPath($path);
622		try {
623			$result = $this->share->mkdir($path);
624		} catch (ConnectException $e) {
625			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
626			$this->leave(__FUNCTION__, $ex);
627			throw $ex;
628		} catch (Exception $e) {
629			$this->swallow(__FUNCTION__, $e);
630		}
631		return $this->leave(__FUNCTION__, $result);
632	}
633
634	public function file_exists($path) {
635		$this->log('enter: '.__FUNCTION__."($path)");
636		$result = false;
637		try {
638			$this->getFileInfo($path);
639			$result = true;
640		} catch (ConnectException $e) {
641			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
642			$this->leave(__FUNCTION__, $ex);
643			throw $ex;
644		} catch (Exception $e) {
645			$this->swallow(__FUNCTION__, $e);
646		}
647		return $this->leave(__FUNCTION__, $result);
648	}
649
650	public function isReadable($path) {
651		$this->log('enter: '.__FUNCTION__."($path)");
652		if ($this->isRootDir($path)) {
653			return $this->leave(__FUNCTION__, true);
654		}
655
656		$result = false;
657		try {
658			$info = $this->getFileInfo($path);
659			$result = !$info->isHidden();
660		} catch (ConnectException $e) {
661			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
662			$this->leave(__FUNCTION__, $ex);
663			throw $ex;
664		} catch (Exception $e) {
665			$this->swallow(__FUNCTION__, $e);
666		}
667		return $this->leave(__FUNCTION__, $result);
668	}
669
670	public function isCreatable($path) {
671		$this->log('enter: '.__FUNCTION__."($path)");
672		if ($this->isRootDir($path)) {
673			return $this->leave(__FUNCTION__, true);
674		}
675		return $this->leave(__FUNCTION__, parent::isCreatable($path));
676	}
677
678	public function isUpdatable($path) {
679		$this->log('enter: '.__FUNCTION__."($path)");
680		if ($this->isRootDir($path)) {
681			// root path mustn't be changed
682			return $this->leave(__FUNCTION__, false);
683		}
684
685		$result = false;
686		try {
687			$info = $this->getFileInfo($path);
688			// following windows behaviour for read-only folders: they can be written into
689			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
690			$result = !$info->isHidden() && (!$info->isReadOnly() || $this->is_dir($path));
691		} catch (ConnectException $e) {
692			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
693			$this->leave(__FUNCTION__, $ex);
694			throw $ex;
695		} catch (Exception $e) {
696			$this->swallow(__FUNCTION__, $e);
697		}
698		return $this->leave(__FUNCTION__, $result);
699	}
700
701	public function isDeletable($path) {
702		$this->log('enter: '.__FUNCTION__."($path)");
703		if ($this->isRootDir($path)) {
704			// root path mustn't be deleted
705			return $this->leave(__FUNCTION__, false);
706		}
707
708		$result = false;
709		try {
710			$info = $this->getFileInfo($path);
711			$result = !$info->isHidden() && !$info->isReadOnly();
712		} catch (ConnectException $e) {
713			$ex = new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
714			$this->leave(__FUNCTION__, $ex);
715			throw $ex;
716		} catch (Exception $e) {
717			$this->swallow(__FUNCTION__, $e);
718		}
719		return $this->leave(__FUNCTION__, $result);
720	}
721
722	/**
723	 * check if smbclient is installed
724	 */
725	public static function checkDependencies() {
726		return (
727			(bool)\OC_Helper::findBinaryPath('smbclient')
728			|| NativeServer::available(new System())
729		) ? true : ['smbclient'];
730	}
731
732	/**
733	 * Test a storage for availability
734	 *
735	 * @return bool
736	 */
737	public function test() {
738		$this->log('enter: '.__FUNCTION__."()");
739		$result = false;
740		try {
741			$result = parent::test();
742		} catch (Exception $e) {
743			$this->swallow(__FUNCTION__, $e);
744		}
745		return $this->leave(__FUNCTION__, $result);
746	}
747
748	/**
749	 * @param string $message
750	 * @param int $level
751	 * @param string $from
752	 */
753	private function log($message, $level = Util::DEBUG, $from = 'smb') {
754		if ($this->logActive) {
755			Util::writeLog($from, $message, $level);
756		}
757	}
758
759	/**
760	 * if smb.logging.enable is set to true in the config will log a leave line
761	 * with the given function, the return value or the exception
762	 *
763	 * @param $function
764	 * @param mixed $result an exception will be logged and then returned
765	 * @return mixed
766	 */
767	private function leave($function, $result) {
768		if (!$this->logActive) {
769			//don't bother building log strings
770			return $result;
771		} elseif ($result === true) {
772			Util::writeLog('smb', "leave: $function, return true", Util::DEBUG);
773		} elseif ($result === false) {
774			Util::writeLog('smb', "leave: $function, return false", Util::DEBUG);
775		} elseif (\is_string($result)) {
776			Util::writeLog('smb', "leave: $function, return '$result'", Util::DEBUG);
777		} elseif (\is_resource($result)) {
778			Util::writeLog('smb', "leave: $function, return resource", Util::DEBUG);
779		} elseif ($result instanceof \Exception) {
780			Util::writeLog('smb', "leave: $function, throw ".\get_class($result)
781				.' - code: '.$result->getCode()
782				.' message: '.$result->getMessage()
783				.' trace: '.$result->getTraceAsString(), Util::DEBUG);
784		} else {
785			Util::writeLog('smb', "leave: $function, return ".\json_encode($result, true), Util::DEBUG);
786		}
787		return $result;
788	}
789
790	private function swallow($function, \Exception $exception) {
791		if ($this->logActive) {
792			Util::writeLog('smb', "$function swallowing ".\get_class($exception)
793				.' - code: '.$exception->getCode()
794				.' message: '.$exception->getMessage()
795				.' trace: '.$exception->getTraceAsString(), Util::DEBUG);
796		}
797	}
798
799	/**
800	 * immediately close / free connection
801	 */
802	public function __destruct() {
803		unset($this->share);
804	}
805}
806