Passed
Push — master ( 08209e...071a38 )
by Roeland
41:40 queued 26:56
created
apps/files_external/lib/Lib/Storage/SMB.php 1 patch
Indentation   +667 added lines, -667 removed lines patch added patch discarded remove patch
@@ -68,671 +68,671 @@
 block discarded – undo
68 68
 use OCP\ILogger;
69 69
 
70 70
 class SMB extends Common implements INotifyStorage {
71
-	/**
72
-	 * @var \Icewind\SMB\IServer
73
-	 */
74
-	protected $server;
75
-
76
-	/**
77
-	 * @var \Icewind\SMB\IShare
78
-	 */
79
-	protected $share;
80
-
81
-	/**
82
-	 * @var string
83
-	 */
84
-	protected $root;
85
-
86
-	/**
87
-	 * @var \Icewind\SMB\IFileInfo[]
88
-	 */
89
-	protected $statCache;
90
-
91
-	/** @var ILogger */
92
-	protected $logger;
93
-
94
-	/** @var bool */
95
-	protected $showHidden;
96
-
97
-	/** @var bool */
98
-	protected $checkAcl;
99
-
100
-	public function __construct($params) {
101
-		if (!isset($params['host'])) {
102
-			throw new \Exception('Invalid configuration, no host provided');
103
-		}
104
-
105
-		if (isset($params['auth'])) {
106
-			$auth = $params['auth'];
107
-		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
108
-			[$workgroup, $user] = $this->splitUser($params['user']);
109
-			$auth = new BasicAuth($user, $workgroup, $params['password']);
110
-		} else {
111
-			throw new \Exception('Invalid configuration, no credentials provided');
112
-		}
113
-
114
-		if (isset($params['logger'])) {
115
-			$this->logger = $params['logger'];
116
-		} else {
117
-			$this->logger = \OC::$server->getLogger();
118
-		}
119
-
120
-		$options = new Options();
121
-		if (isset($params['timeout'])) {
122
-			$timeout = (int)$params['timeout'];
123
-			if ($timeout > 0) {
124
-				$options->setTimeout($timeout);
125
-			}
126
-		}
127
-		$serverFactory = new ServerFactory($options);
128
-		$this->server = $serverFactory->createServer($params['host'], $auth);
129
-		$this->share = $this->server->getShare(trim($params['share'], '/'));
130
-
131
-		$this->root = $params['root'] ?? '/';
132
-		$this->root = '/' . ltrim($this->root, '/');
133
-		$this->root = rtrim($this->root, '/') . '/';
134
-
135
-		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136
-		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
137
-
138
-		$this->statCache = new CappedMemoryCache();
139
-		parent::__construct($params);
140
-	}
141
-
142
-	private function splitUser($user) {
143
-		if (strpos($user, '/')) {
144
-			return explode('/', $user, 2);
145
-		} elseif (strpos($user, '\\')) {
146
-			return explode('\\', $user);
147
-		} else {
148
-			return [null, $user];
149
-		}
150
-	}
151
-
152
-	/**
153
-	 * @return string
154
-	 */
155
-	public function getId() {
156
-		// FIXME: double slash to keep compatible with the old storage ids,
157
-		// failure to do so will lead to creation of a new storage id and
158
-		// loss of shares from the storage
159
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
160
-	}
161
-
162
-	/**
163
-	 * @param string $path
164
-	 * @return string
165
-	 */
166
-	protected function buildPath($path) {
167
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
168
-	}
169
-
170
-	protected function relativePath($fullPath) {
171
-		if ($fullPath === $this->root) {
172
-			return '';
173
-		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
174
-			return substr($fullPath, strlen($this->root));
175
-		} else {
176
-			return null;
177
-		}
178
-	}
179
-
180
-	/**
181
-	 * @param string $path
182
-	 * @return \Icewind\SMB\IFileInfo
183
-	 * @throws StorageAuthException
184
-	 */
185
-	protected function getFileInfo($path) {
186
-		try {
187
-			$path = $this->buildPath($path);
188
-			if (!isset($this->statCache[$path])) {
189
-				$this->statCache[$path] = $this->share->stat($path);
190
-			}
191
-			return $this->statCache[$path];
192
-		} catch (ConnectException $e) {
193
-			$this->throwUnavailable($e);
194
-		} catch (ForbiddenException $e) {
195
-			// with php-smbclient, this exceptions is thrown when the provided password is invalid.
196
-			// Possible is also ForbiddenException with a different error code, so we check it.
197
-			if ($e->getCode() === 1) {
198
-				$this->throwUnavailable($e);
199
-			}
200
-			throw $e;
201
-		}
202
-	}
203
-
204
-	/**
205
-	 * @param \Exception $e
206
-	 * @throws StorageAuthException
207
-	 */
208
-	protected function throwUnavailable(\Exception $e) {
209
-		$this->logger->logException($e, ['message' => 'Error while getting file info']);
210
-		throw new StorageAuthException($e->getMessage(), $e);
211
-	}
212
-
213
-	/**
214
-	 * get the acl from fileinfo that is relevant for the configured user
215
-	 *
216
-	 * @param IFileInfo $file
217
-	 * @return ACL|null
218
-	 */
219
-	private function getACL(IFileInfo $file): ?ACL {
220
-		$acls = $file->getAcls();
221
-		foreach ($acls as $user => $acl) {
222
-			[, $user] = explode('\\', $user); // strip domain
223
-			if ($user === $this->server->getAuth()->getUsername()) {
224
-				return $acl;
225
-			}
226
-		}
227
-
228
-		return null;
229
-	}
230
-
231
-	/**
232
-	 * @param string $path
233
-	 * @return \Icewind\SMB\IFileInfo[]
234
-	 * @throws StorageNotAvailableException
235
-	 */
236
-	protected function getFolderContents($path): iterable {
237
-		try {
238
-			$path = ltrim($this->buildPath($path), '/');
239
-			try {
240
-				$files = $this->share->dir($path);
241
-			} catch (ForbiddenException $e) {
242
-				throw new NotPermittedException();
243
-			}
244
-			foreach ($files as $file) {
245
-				$this->statCache[$path . '/' . $file->getName()] = $file;
246
-			}
247
-
248
-			foreach ($files as $file) {
249
-				try {
250
-					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
251
-					// so we trigger the below exceptions where applicable
252
-					$hide = $file->isHidden() && !$this->showHidden;
253
-
254
-					if ($this->checkAcl && $acl = $this->getACL($file)) {
255
-						// if there is no explicit deny, we assume it's allowed
256
-						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257
-						// additionally, it's better to have false negatives here then false positives
258
-						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
260
-							return false;
261
-						}
262
-					}
263
-
264
-					if ($hide) {
265
-						$this->logger->debug('hiding hidden file ' . $file->getName());
266
-					}
267
-					if (!$hide) {
268
-						yield $file;
269
-					}
270
-				} catch (ForbiddenException $e) {
271
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
272
-				} catch (NotFoundException $e) {
273
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
274
-				}
275
-			}
276
-		} catch (ConnectException $e) {
277
-			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
278
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
279
-		}
280
-	}
281
-
282
-	/**
283
-	 * @param \Icewind\SMB\IFileInfo $info
284
-	 * @return array
285
-	 */
286
-	protected function formatInfo($info) {
287
-		$result = [
288
-			'size' => $info->getSize(),
289
-			'mtime' => $info->getMTime(),
290
-		];
291
-		if ($info->isDirectory()) {
292
-			$result['type'] = 'dir';
293
-		} else {
294
-			$result['type'] = 'file';
295
-		}
296
-		return $result;
297
-	}
298
-
299
-	/**
300
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
301
-	 *
302
-	 * @param string $source the old name of the path
303
-	 * @param string $target the new name of the path
304
-	 * @return bool true if the rename is successful, false otherwise
305
-	 */
306
-	public function rename($source, $target, $retry = true) {
307
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
308
-			return false;
309
-		}
310
-
311
-		$absoluteSource = $this->buildPath($source);
312
-		$absoluteTarget = $this->buildPath($target);
313
-		try {
314
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
315
-		} catch (AlreadyExistsException $e) {
316
-			if ($retry) {
317
-				$this->remove($target);
318
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
319
-			} else {
320
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
321
-				return false;
322
-			}
323
-		} catch (InvalidArgumentException $e) {
324
-			if ($retry) {
325
-				$this->remove($target);
326
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
327
-			} else {
328
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
329
-				return false;
330
-			}
331
-		} catch (\Exception $e) {
332
-			$this->logger->logException($e, ['level' => ILogger::WARN]);
333
-			return false;
334
-		}
335
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
336
-		return $result;
337
-	}
338
-
339
-	public function stat($path, $retry = true) {
340
-		try {
341
-			$result = $this->formatInfo($this->getFileInfo($path));
342
-		} catch (ForbiddenException $e) {
343
-			return false;
344
-		} catch (NotFoundException $e) {
345
-			return false;
346
-		} catch (TimedOutException $e) {
347
-			if ($retry) {
348
-				return $this->stat($path, false);
349
-			} else {
350
-				throw $e;
351
-			}
352
-		}
353
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
354
-			$result['mtime'] = $this->shareMTime();
355
-		}
356
-		return $result;
357
-	}
358
-
359
-	/**
360
-	 * get the best guess for the modification time of the share
361
-	 *
362
-	 * @return int
363
-	 */
364
-	private function shareMTime() {
365
-		$highestMTime = 0;
366
-		$files = $this->share->dir($this->root);
367
-		foreach ($files as $fileInfo) {
368
-			try {
369
-				if ($fileInfo->getMTime() > $highestMTime) {
370
-					$highestMTime = $fileInfo->getMTime();
371
-				}
372
-			} catch (NotFoundException $e) {
373
-				// Ignore this, can happen on unavailable DFS shares
374
-			} catch (ForbiddenException $e) {
375
-				// Ignore this too - it's a symlink
376
-			}
377
-		}
378
-		return $highestMTime;
379
-	}
380
-
381
-	/**
382
-	 * Check if the path is our root dir (not the smb one)
383
-	 *
384
-	 * @param string $path the path
385
-	 * @return bool
386
-	 */
387
-	private function isRootDir($path) {
388
-		return $path === '' || $path === '/' || $path === '.';
389
-	}
390
-
391
-	/**
392
-	 * Check if our root points to a smb share
393
-	 *
394
-	 * @return bool true if our root points to a share false otherwise
395
-	 */
396
-	private function remoteIsShare() {
397
-		return $this->share->getName() && (!$this->root || $this->root === '/');
398
-	}
399
-
400
-	/**
401
-	 * @param string $path
402
-	 * @return bool
403
-	 */
404
-	public function unlink($path) {
405
-		if ($this->isRootDir($path)) {
406
-			return false;
407
-		}
408
-
409
-		try {
410
-			if ($this->is_dir($path)) {
411
-				return $this->rmdir($path);
412
-			} else {
413
-				$path = $this->buildPath($path);
414
-				unset($this->statCache[$path]);
415
-				$this->share->del($path);
416
-				return true;
417
-			}
418
-		} catch (NotFoundException $e) {
419
-			return false;
420
-		} catch (ForbiddenException $e) {
421
-			return false;
422
-		} catch (ConnectException $e) {
423
-			$this->logger->logException($e, ['message' => 'Error while deleting file']);
424
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
425
-		}
426
-	}
427
-
428
-	/**
429
-	 * check if a file or folder has been updated since $time
430
-	 *
431
-	 * @param string $path
432
-	 * @param int $time
433
-	 * @return bool
434
-	 */
435
-	public function hasUpdated($path, $time) {
436
-		if (!$path and $this->root === '/') {
437
-			// mtime doesn't work for shares, but giving the nature of the backend,
438
-			// doing a full update is still just fast enough
439
-			return true;
440
-		} else {
441
-			$actualTime = $this->filemtime($path);
442
-			return $actualTime > $time;
443
-		}
444
-	}
445
-
446
-	/**
447
-	 * @param string $path
448
-	 * @param string $mode
449
-	 * @return resource|bool
450
-	 */
451
-	public function fopen($path, $mode) {
452
-		$fullPath = $this->buildPath($path);
453
-		try {
454
-			switch ($mode) {
455
-				case 'r':
456
-				case 'rb':
457
-					if (!$this->file_exists($path)) {
458
-						return false;
459
-					}
460
-					return $this->share->read($fullPath);
461
-				case 'w':
462
-				case 'wb':
463
-					$source = $this->share->write($fullPath);
464
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
465
-						unset($this->statCache[$fullPath]);
466
-					});
467
-				case 'a':
468
-				case 'ab':
469
-				case 'r+':
470
-				case 'w+':
471
-				case 'wb+':
472
-				case 'a+':
473
-				case 'x':
474
-				case 'x+':
475
-				case 'c':
476
-				case 'c+':
477
-					//emulate these
478
-					if (strrpos($path, '.') !== false) {
479
-						$ext = substr($path, strrpos($path, '.'));
480
-					} else {
481
-						$ext = '';
482
-					}
483
-					if ($this->file_exists($path)) {
484
-						if (!$this->isUpdatable($path)) {
485
-							return false;
486
-						}
487
-						$tmpFile = $this->getCachedFile($path);
488
-					} else {
489
-						if (!$this->isCreatable(dirname($path))) {
490
-							return false;
491
-						}
492
-						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
493
-					}
494
-					$source = fopen($tmpFile, $mode);
495
-					$share = $this->share;
496
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
497
-						unset($this->statCache[$fullPath]);
498
-						$share->put($tmpFile, $fullPath);
499
-						unlink($tmpFile);
500
-					});
501
-			}
502
-			return false;
503
-		} catch (NotFoundException $e) {
504
-			return false;
505
-		} catch (ForbiddenException $e) {
506
-			return false;
507
-		} catch (OutOfSpaceException $e) {
508
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
509
-		} catch (ConnectException $e) {
510
-			$this->logger->logException($e, ['message' => 'Error while opening file']);
511
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
512
-		}
513
-	}
514
-
515
-	public function rmdir($path) {
516
-		if ($this->isRootDir($path)) {
517
-			return false;
518
-		}
519
-
520
-		try {
521
-			$this->statCache = [];
522
-			$content = $this->share->dir($this->buildPath($path));
523
-			foreach ($content as $file) {
524
-				if ($file->isDirectory()) {
525
-					$this->rmdir($path . '/' . $file->getName());
526
-				} else {
527
-					$this->share->del($file->getPath());
528
-				}
529
-			}
530
-			$this->share->rmdir($this->buildPath($path));
531
-			return true;
532
-		} catch (NotFoundException $e) {
533
-			return false;
534
-		} catch (ForbiddenException $e) {
535
-			return false;
536
-		} catch (ConnectException $e) {
537
-			$this->logger->logException($e, ['message' => 'Error while removing folder']);
538
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
539
-		}
540
-	}
541
-
542
-	public function touch($path, $mtime = null) {
543
-		try {
544
-			if (!$this->file_exists($path)) {
545
-				$fh = $this->share->write($this->buildPath($path));
546
-				fclose($fh);
547
-				return true;
548
-			}
549
-			return false;
550
-		} catch (OutOfSpaceException $e) {
551
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
552
-		} catch (ConnectException $e) {
553
-			$this->logger->logException($e, ['message' => 'Error while creating file']);
554
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
555
-		}
556
-	}
557
-
558
-	public function getMetaData($path) {
559
-		try {
560
-			$fileInfo = $this->getFileInfo($path);
561
-		} catch (NotFoundException $e) {
562
-			return null;
563
-		} catch (ForbiddenException $e) {
564
-			return null;
565
-		}
566
-		if (!$fileInfo) {
567
-			return null;
568
-		}
569
-
570
-		return $this->getMetaDataFromFileInfo($fileInfo);
571
-	}
572
-
573
-	private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
574
-		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
575
-
576
-		if (
577
-			!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
578
-		) {
579
-			$permissions += Constants::PERMISSION_DELETE;
580
-			$permissions += Constants::PERMISSION_UPDATE;
581
-			if ($fileInfo->isDirectory()) {
582
-				$permissions += Constants::PERMISSION_CREATE;
583
-			}
584
-		}
585
-
586
-		$data = [];
587
-		if ($fileInfo->isDirectory()) {
588
-			$data['mimetype'] = 'httpd/unix-directory';
589
-		} else {
590
-			$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
591
-		}
592
-		$data['mtime'] = $fileInfo->getMTime();
593
-		if ($fileInfo->isDirectory()) {
594
-			$data['size'] = -1; //unknown
595
-		} else {
596
-			$data['size'] = $fileInfo->getSize();
597
-		}
598
-		$data['etag'] = $this->getETag($fileInfo->getPath());
599
-		$data['storage_mtime'] = $data['mtime'];
600
-		$data['permissions'] = $permissions;
601
-		$data['name'] = $fileInfo->getName();
602
-
603
-		return $data;
604
-	}
605
-
606
-	public function opendir($path) {
607
-		try {
608
-			$files = $this->getFolderContents($path);
609
-		} catch (NotFoundException $e) {
610
-			return false;
611
-		} catch (NotPermittedException $e) {
612
-			return false;
613
-		}
614
-		$names = array_map(function ($info) {
615
-			/** @var \Icewind\SMB\IFileInfo $info */
616
-			return $info->getName();
617
-		}, iterator_to_array($files));
618
-		return IteratorDirectory::wrap($names);
619
-	}
620
-
621
-	public function getDirectoryContent($directory): \Traversable {
622
-		$files = $this->getFolderContents($directory);
623
-		foreach ($files as $file) {
624
-			yield $this->getMetaDataFromFileInfo($file);
625
-		}
626
-	}
627
-
628
-	public function filetype($path) {
629
-		try {
630
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
631
-		} catch (NotFoundException $e) {
632
-			return false;
633
-		} catch (ForbiddenException $e) {
634
-			return false;
635
-		}
636
-	}
637
-
638
-	public function mkdir($path) {
639
-		$path = $this->buildPath($path);
640
-		try {
641
-			$this->share->mkdir($path);
642
-			return true;
643
-		} catch (ConnectException $e) {
644
-			$this->logger->logException($e, ['message' => 'Error while creating folder']);
645
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
646
-		} catch (Exception $e) {
647
-			return false;
648
-		}
649
-	}
650
-
651
-	public function file_exists($path) {
652
-		try {
653
-			$this->getFileInfo($path);
654
-			return true;
655
-		} catch (NotFoundException $e) {
656
-			return false;
657
-		} catch (ForbiddenException $e) {
658
-			return false;
659
-		} catch (ConnectException $e) {
660
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
661
-		}
662
-	}
663
-
664
-	public function isReadable($path) {
665
-		try {
666
-			$info = $this->getFileInfo($path);
667
-			return $this->showHidden || !$info->isHidden();
668
-		} catch (NotFoundException $e) {
669
-			return false;
670
-		} catch (ForbiddenException $e) {
671
-			return false;
672
-		}
673
-	}
674
-
675
-	public function isUpdatable($path) {
676
-		try {
677
-			$info = $this->getFileInfo($path);
678
-			// following windows behaviour for read-only folders: they can be written into
679
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
680
-			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
681
-		} catch (NotFoundException $e) {
682
-			return false;
683
-		} catch (ForbiddenException $e) {
684
-			return false;
685
-		}
686
-	}
687
-
688
-	public function isDeletable($path) {
689
-		try {
690
-			$info = $this->getFileInfo($path);
691
-			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
692
-		} catch (NotFoundException $e) {
693
-			return false;
694
-		} catch (ForbiddenException $e) {
695
-			return false;
696
-		}
697
-	}
698
-
699
-	/**
700
-	 * check if smbclient is installed
701
-	 */
702
-	public static function checkDependencies() {
703
-		return (
704
-			(bool)\OC_Helper::findBinaryPath('smbclient')
705
-			|| NativeServer::available(new System())
706
-		) ? true : ['smbclient'];
707
-	}
708
-
709
-	/**
710
-	 * Test a storage for availability
711
-	 *
712
-	 * @return bool
713
-	 */
714
-	public function test() {
715
-		try {
716
-			return parent::test();
717
-		} catch (Exception $e) {
718
-			$this->logger->logException($e);
719
-			return false;
720
-		}
721
-	}
722
-
723
-	public function listen($path, callable $callback) {
724
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
725
-			if ($change instanceof IRenameChange) {
726
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
727
-			} else {
728
-				return $callback($change->getType(), $change->getPath());
729
-			}
730
-		});
731
-	}
732
-
733
-	public function notify($path) {
734
-		$path = '/' . ltrim($path, '/');
735
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
736
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
737
-	}
71
+    /**
72
+     * @var \Icewind\SMB\IServer
73
+     */
74
+    protected $server;
75
+
76
+    /**
77
+     * @var \Icewind\SMB\IShare
78
+     */
79
+    protected $share;
80
+
81
+    /**
82
+     * @var string
83
+     */
84
+    protected $root;
85
+
86
+    /**
87
+     * @var \Icewind\SMB\IFileInfo[]
88
+     */
89
+    protected $statCache;
90
+
91
+    /** @var ILogger */
92
+    protected $logger;
93
+
94
+    /** @var bool */
95
+    protected $showHidden;
96
+
97
+    /** @var bool */
98
+    protected $checkAcl;
99
+
100
+    public function __construct($params) {
101
+        if (!isset($params['host'])) {
102
+            throw new \Exception('Invalid configuration, no host provided');
103
+        }
104
+
105
+        if (isset($params['auth'])) {
106
+            $auth = $params['auth'];
107
+        } elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
108
+            [$workgroup, $user] = $this->splitUser($params['user']);
109
+            $auth = new BasicAuth($user, $workgroup, $params['password']);
110
+        } else {
111
+            throw new \Exception('Invalid configuration, no credentials provided');
112
+        }
113
+
114
+        if (isset($params['logger'])) {
115
+            $this->logger = $params['logger'];
116
+        } else {
117
+            $this->logger = \OC::$server->getLogger();
118
+        }
119
+
120
+        $options = new Options();
121
+        if (isset($params['timeout'])) {
122
+            $timeout = (int)$params['timeout'];
123
+            if ($timeout > 0) {
124
+                $options->setTimeout($timeout);
125
+            }
126
+        }
127
+        $serverFactory = new ServerFactory($options);
128
+        $this->server = $serverFactory->createServer($params['host'], $auth);
129
+        $this->share = $this->server->getShare(trim($params['share'], '/'));
130
+
131
+        $this->root = $params['root'] ?? '/';
132
+        $this->root = '/' . ltrim($this->root, '/');
133
+        $this->root = rtrim($this->root, '/') . '/';
134
+
135
+        $this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136
+        $this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
137
+
138
+        $this->statCache = new CappedMemoryCache();
139
+        parent::__construct($params);
140
+    }
141
+
142
+    private function splitUser($user) {
143
+        if (strpos($user, '/')) {
144
+            return explode('/', $user, 2);
145
+        } elseif (strpos($user, '\\')) {
146
+            return explode('\\', $user);
147
+        } else {
148
+            return [null, $user];
149
+        }
150
+    }
151
+
152
+    /**
153
+     * @return string
154
+     */
155
+    public function getId() {
156
+        // FIXME: double slash to keep compatible with the old storage ids,
157
+        // failure to do so will lead to creation of a new storage id and
158
+        // loss of shares from the storage
159
+        return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
160
+    }
161
+
162
+    /**
163
+     * @param string $path
164
+     * @return string
165
+     */
166
+    protected function buildPath($path) {
167
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
168
+    }
169
+
170
+    protected function relativePath($fullPath) {
171
+        if ($fullPath === $this->root) {
172
+            return '';
173
+        } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
174
+            return substr($fullPath, strlen($this->root));
175
+        } else {
176
+            return null;
177
+        }
178
+    }
179
+
180
+    /**
181
+     * @param string $path
182
+     * @return \Icewind\SMB\IFileInfo
183
+     * @throws StorageAuthException
184
+     */
185
+    protected function getFileInfo($path) {
186
+        try {
187
+            $path = $this->buildPath($path);
188
+            if (!isset($this->statCache[$path])) {
189
+                $this->statCache[$path] = $this->share->stat($path);
190
+            }
191
+            return $this->statCache[$path];
192
+        } catch (ConnectException $e) {
193
+            $this->throwUnavailable($e);
194
+        } catch (ForbiddenException $e) {
195
+            // with php-smbclient, this exceptions is thrown when the provided password is invalid.
196
+            // Possible is also ForbiddenException with a different error code, so we check it.
197
+            if ($e->getCode() === 1) {
198
+                $this->throwUnavailable($e);
199
+            }
200
+            throw $e;
201
+        }
202
+    }
203
+
204
+    /**
205
+     * @param \Exception $e
206
+     * @throws StorageAuthException
207
+     */
208
+    protected function throwUnavailable(\Exception $e) {
209
+        $this->logger->logException($e, ['message' => 'Error while getting file info']);
210
+        throw new StorageAuthException($e->getMessage(), $e);
211
+    }
212
+
213
+    /**
214
+     * get the acl from fileinfo that is relevant for the configured user
215
+     *
216
+     * @param IFileInfo $file
217
+     * @return ACL|null
218
+     */
219
+    private function getACL(IFileInfo $file): ?ACL {
220
+        $acls = $file->getAcls();
221
+        foreach ($acls as $user => $acl) {
222
+            [, $user] = explode('\\', $user); // strip domain
223
+            if ($user === $this->server->getAuth()->getUsername()) {
224
+                return $acl;
225
+            }
226
+        }
227
+
228
+        return null;
229
+    }
230
+
231
+    /**
232
+     * @param string $path
233
+     * @return \Icewind\SMB\IFileInfo[]
234
+     * @throws StorageNotAvailableException
235
+     */
236
+    protected function getFolderContents($path): iterable {
237
+        try {
238
+            $path = ltrim($this->buildPath($path), '/');
239
+            try {
240
+                $files = $this->share->dir($path);
241
+            } catch (ForbiddenException $e) {
242
+                throw new NotPermittedException();
243
+            }
244
+            foreach ($files as $file) {
245
+                $this->statCache[$path . '/' . $file->getName()] = $file;
246
+            }
247
+
248
+            foreach ($files as $file) {
249
+                try {
250
+                    // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
251
+                    // so we trigger the below exceptions where applicable
252
+                    $hide = $file->isHidden() && !$this->showHidden;
253
+
254
+                    if ($this->checkAcl && $acl = $this->getACL($file)) {
255
+                        // if there is no explicit deny, we assume it's allowed
256
+                        // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257
+                        // additionally, it's better to have false negatives here then false positives
258
+                        if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
+                            $this->logger->debug('Hiding non readable entry ' . $file->getName());
260
+                            return false;
261
+                        }
262
+                    }
263
+
264
+                    if ($hide) {
265
+                        $this->logger->debug('hiding hidden file ' . $file->getName());
266
+                    }
267
+                    if (!$hide) {
268
+                        yield $file;
269
+                    }
270
+                } catch (ForbiddenException $e) {
271
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
272
+                } catch (NotFoundException $e) {
273
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
274
+                }
275
+            }
276
+        } catch (ConnectException $e) {
277
+            $this->logger->logException($e, ['message' => 'Error while getting folder content']);
278
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
279
+        }
280
+    }
281
+
282
+    /**
283
+     * @param \Icewind\SMB\IFileInfo $info
284
+     * @return array
285
+     */
286
+    protected function formatInfo($info) {
287
+        $result = [
288
+            'size' => $info->getSize(),
289
+            'mtime' => $info->getMTime(),
290
+        ];
291
+        if ($info->isDirectory()) {
292
+            $result['type'] = 'dir';
293
+        } else {
294
+            $result['type'] = 'file';
295
+        }
296
+        return $result;
297
+    }
298
+
299
+    /**
300
+     * Rename the files. If the source or the target is the root, the rename won't happen.
301
+     *
302
+     * @param string $source the old name of the path
303
+     * @param string $target the new name of the path
304
+     * @return bool true if the rename is successful, false otherwise
305
+     */
306
+    public function rename($source, $target, $retry = true) {
307
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
308
+            return false;
309
+        }
310
+
311
+        $absoluteSource = $this->buildPath($source);
312
+        $absoluteTarget = $this->buildPath($target);
313
+        try {
314
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
315
+        } catch (AlreadyExistsException $e) {
316
+            if ($retry) {
317
+                $this->remove($target);
318
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
319
+            } else {
320
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
321
+                return false;
322
+            }
323
+        } catch (InvalidArgumentException $e) {
324
+            if ($retry) {
325
+                $this->remove($target);
326
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
327
+            } else {
328
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
329
+                return false;
330
+            }
331
+        } catch (\Exception $e) {
332
+            $this->logger->logException($e, ['level' => ILogger::WARN]);
333
+            return false;
334
+        }
335
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
336
+        return $result;
337
+    }
338
+
339
+    public function stat($path, $retry = true) {
340
+        try {
341
+            $result = $this->formatInfo($this->getFileInfo($path));
342
+        } catch (ForbiddenException $e) {
343
+            return false;
344
+        } catch (NotFoundException $e) {
345
+            return false;
346
+        } catch (TimedOutException $e) {
347
+            if ($retry) {
348
+                return $this->stat($path, false);
349
+            } else {
350
+                throw $e;
351
+            }
352
+        }
353
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
354
+            $result['mtime'] = $this->shareMTime();
355
+        }
356
+        return $result;
357
+    }
358
+
359
+    /**
360
+     * get the best guess for the modification time of the share
361
+     *
362
+     * @return int
363
+     */
364
+    private function shareMTime() {
365
+        $highestMTime = 0;
366
+        $files = $this->share->dir($this->root);
367
+        foreach ($files as $fileInfo) {
368
+            try {
369
+                if ($fileInfo->getMTime() > $highestMTime) {
370
+                    $highestMTime = $fileInfo->getMTime();
371
+                }
372
+            } catch (NotFoundException $e) {
373
+                // Ignore this, can happen on unavailable DFS shares
374
+            } catch (ForbiddenException $e) {
375
+                // Ignore this too - it's a symlink
376
+            }
377
+        }
378
+        return $highestMTime;
379
+    }
380
+
381
+    /**
382
+     * Check if the path is our root dir (not the smb one)
383
+     *
384
+     * @param string $path the path
385
+     * @return bool
386
+     */
387
+    private function isRootDir($path) {
388
+        return $path === '' || $path === '/' || $path === '.';
389
+    }
390
+
391
+    /**
392
+     * Check if our root points to a smb share
393
+     *
394
+     * @return bool true if our root points to a share false otherwise
395
+     */
396
+    private function remoteIsShare() {
397
+        return $this->share->getName() && (!$this->root || $this->root === '/');
398
+    }
399
+
400
+    /**
401
+     * @param string $path
402
+     * @return bool
403
+     */
404
+    public function unlink($path) {
405
+        if ($this->isRootDir($path)) {
406
+            return false;
407
+        }
408
+
409
+        try {
410
+            if ($this->is_dir($path)) {
411
+                return $this->rmdir($path);
412
+            } else {
413
+                $path = $this->buildPath($path);
414
+                unset($this->statCache[$path]);
415
+                $this->share->del($path);
416
+                return true;
417
+            }
418
+        } catch (NotFoundException $e) {
419
+            return false;
420
+        } catch (ForbiddenException $e) {
421
+            return false;
422
+        } catch (ConnectException $e) {
423
+            $this->logger->logException($e, ['message' => 'Error while deleting file']);
424
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
425
+        }
426
+    }
427
+
428
+    /**
429
+     * check if a file or folder has been updated since $time
430
+     *
431
+     * @param string $path
432
+     * @param int $time
433
+     * @return bool
434
+     */
435
+    public function hasUpdated($path, $time) {
436
+        if (!$path and $this->root === '/') {
437
+            // mtime doesn't work for shares, but giving the nature of the backend,
438
+            // doing a full update is still just fast enough
439
+            return true;
440
+        } else {
441
+            $actualTime = $this->filemtime($path);
442
+            return $actualTime > $time;
443
+        }
444
+    }
445
+
446
+    /**
447
+     * @param string $path
448
+     * @param string $mode
449
+     * @return resource|bool
450
+     */
451
+    public function fopen($path, $mode) {
452
+        $fullPath = $this->buildPath($path);
453
+        try {
454
+            switch ($mode) {
455
+                case 'r':
456
+                case 'rb':
457
+                    if (!$this->file_exists($path)) {
458
+                        return false;
459
+                    }
460
+                    return $this->share->read($fullPath);
461
+                case 'w':
462
+                case 'wb':
463
+                    $source = $this->share->write($fullPath);
464
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
465
+                        unset($this->statCache[$fullPath]);
466
+                    });
467
+                case 'a':
468
+                case 'ab':
469
+                case 'r+':
470
+                case 'w+':
471
+                case 'wb+':
472
+                case 'a+':
473
+                case 'x':
474
+                case 'x+':
475
+                case 'c':
476
+                case 'c+':
477
+                    //emulate these
478
+                    if (strrpos($path, '.') !== false) {
479
+                        $ext = substr($path, strrpos($path, '.'));
480
+                    } else {
481
+                        $ext = '';
482
+                    }
483
+                    if ($this->file_exists($path)) {
484
+                        if (!$this->isUpdatable($path)) {
485
+                            return false;
486
+                        }
487
+                        $tmpFile = $this->getCachedFile($path);
488
+                    } else {
489
+                        if (!$this->isCreatable(dirname($path))) {
490
+                            return false;
491
+                        }
492
+                        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
493
+                    }
494
+                    $source = fopen($tmpFile, $mode);
495
+                    $share = $this->share;
496
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
497
+                        unset($this->statCache[$fullPath]);
498
+                        $share->put($tmpFile, $fullPath);
499
+                        unlink($tmpFile);
500
+                    });
501
+            }
502
+            return false;
503
+        } catch (NotFoundException $e) {
504
+            return false;
505
+        } catch (ForbiddenException $e) {
506
+            return false;
507
+        } catch (OutOfSpaceException $e) {
508
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
509
+        } catch (ConnectException $e) {
510
+            $this->logger->logException($e, ['message' => 'Error while opening file']);
511
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
512
+        }
513
+    }
514
+
515
+    public function rmdir($path) {
516
+        if ($this->isRootDir($path)) {
517
+            return false;
518
+        }
519
+
520
+        try {
521
+            $this->statCache = [];
522
+            $content = $this->share->dir($this->buildPath($path));
523
+            foreach ($content as $file) {
524
+                if ($file->isDirectory()) {
525
+                    $this->rmdir($path . '/' . $file->getName());
526
+                } else {
527
+                    $this->share->del($file->getPath());
528
+                }
529
+            }
530
+            $this->share->rmdir($this->buildPath($path));
531
+            return true;
532
+        } catch (NotFoundException $e) {
533
+            return false;
534
+        } catch (ForbiddenException $e) {
535
+            return false;
536
+        } catch (ConnectException $e) {
537
+            $this->logger->logException($e, ['message' => 'Error while removing folder']);
538
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
539
+        }
540
+    }
541
+
542
+    public function touch($path, $mtime = null) {
543
+        try {
544
+            if (!$this->file_exists($path)) {
545
+                $fh = $this->share->write($this->buildPath($path));
546
+                fclose($fh);
547
+                return true;
548
+            }
549
+            return false;
550
+        } catch (OutOfSpaceException $e) {
551
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
552
+        } catch (ConnectException $e) {
553
+            $this->logger->logException($e, ['message' => 'Error while creating file']);
554
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
555
+        }
556
+    }
557
+
558
+    public function getMetaData($path) {
559
+        try {
560
+            $fileInfo = $this->getFileInfo($path);
561
+        } catch (NotFoundException $e) {
562
+            return null;
563
+        } catch (ForbiddenException $e) {
564
+            return null;
565
+        }
566
+        if (!$fileInfo) {
567
+            return null;
568
+        }
569
+
570
+        return $this->getMetaDataFromFileInfo($fileInfo);
571
+    }
572
+
573
+    private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
574
+        $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
575
+
576
+        if (
577
+            !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
578
+        ) {
579
+            $permissions += Constants::PERMISSION_DELETE;
580
+            $permissions += Constants::PERMISSION_UPDATE;
581
+            if ($fileInfo->isDirectory()) {
582
+                $permissions += Constants::PERMISSION_CREATE;
583
+            }
584
+        }
585
+
586
+        $data = [];
587
+        if ($fileInfo->isDirectory()) {
588
+            $data['mimetype'] = 'httpd/unix-directory';
589
+        } else {
590
+            $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
591
+        }
592
+        $data['mtime'] = $fileInfo->getMTime();
593
+        if ($fileInfo->isDirectory()) {
594
+            $data['size'] = -1; //unknown
595
+        } else {
596
+            $data['size'] = $fileInfo->getSize();
597
+        }
598
+        $data['etag'] = $this->getETag($fileInfo->getPath());
599
+        $data['storage_mtime'] = $data['mtime'];
600
+        $data['permissions'] = $permissions;
601
+        $data['name'] = $fileInfo->getName();
602
+
603
+        return $data;
604
+    }
605
+
606
+    public function opendir($path) {
607
+        try {
608
+            $files = $this->getFolderContents($path);
609
+        } catch (NotFoundException $e) {
610
+            return false;
611
+        } catch (NotPermittedException $e) {
612
+            return false;
613
+        }
614
+        $names = array_map(function ($info) {
615
+            /** @var \Icewind\SMB\IFileInfo $info */
616
+            return $info->getName();
617
+        }, iterator_to_array($files));
618
+        return IteratorDirectory::wrap($names);
619
+    }
620
+
621
+    public function getDirectoryContent($directory): \Traversable {
622
+        $files = $this->getFolderContents($directory);
623
+        foreach ($files as $file) {
624
+            yield $this->getMetaDataFromFileInfo($file);
625
+        }
626
+    }
627
+
628
+    public function filetype($path) {
629
+        try {
630
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
631
+        } catch (NotFoundException $e) {
632
+            return false;
633
+        } catch (ForbiddenException $e) {
634
+            return false;
635
+        }
636
+    }
637
+
638
+    public function mkdir($path) {
639
+        $path = $this->buildPath($path);
640
+        try {
641
+            $this->share->mkdir($path);
642
+            return true;
643
+        } catch (ConnectException $e) {
644
+            $this->logger->logException($e, ['message' => 'Error while creating folder']);
645
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
646
+        } catch (Exception $e) {
647
+            return false;
648
+        }
649
+    }
650
+
651
+    public function file_exists($path) {
652
+        try {
653
+            $this->getFileInfo($path);
654
+            return true;
655
+        } catch (NotFoundException $e) {
656
+            return false;
657
+        } catch (ForbiddenException $e) {
658
+            return false;
659
+        } catch (ConnectException $e) {
660
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
661
+        }
662
+    }
663
+
664
+    public function isReadable($path) {
665
+        try {
666
+            $info = $this->getFileInfo($path);
667
+            return $this->showHidden || !$info->isHidden();
668
+        } catch (NotFoundException $e) {
669
+            return false;
670
+        } catch (ForbiddenException $e) {
671
+            return false;
672
+        }
673
+    }
674
+
675
+    public function isUpdatable($path) {
676
+        try {
677
+            $info = $this->getFileInfo($path);
678
+            // following windows behaviour for read-only folders: they can be written into
679
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
680
+            return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
681
+        } catch (NotFoundException $e) {
682
+            return false;
683
+        } catch (ForbiddenException $e) {
684
+            return false;
685
+        }
686
+    }
687
+
688
+    public function isDeletable($path) {
689
+        try {
690
+            $info = $this->getFileInfo($path);
691
+            return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
692
+        } catch (NotFoundException $e) {
693
+            return false;
694
+        } catch (ForbiddenException $e) {
695
+            return false;
696
+        }
697
+    }
698
+
699
+    /**
700
+     * check if smbclient is installed
701
+     */
702
+    public static function checkDependencies() {
703
+        return (
704
+            (bool)\OC_Helper::findBinaryPath('smbclient')
705
+            || NativeServer::available(new System())
706
+        ) ? true : ['smbclient'];
707
+    }
708
+
709
+    /**
710
+     * Test a storage for availability
711
+     *
712
+     * @return bool
713
+     */
714
+    public function test() {
715
+        try {
716
+            return parent::test();
717
+        } catch (Exception $e) {
718
+            $this->logger->logException($e);
719
+            return false;
720
+        }
721
+    }
722
+
723
+    public function listen($path, callable $callback) {
724
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
725
+            if ($change instanceof IRenameChange) {
726
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
727
+            } else {
728
+                return $callback($change->getType(), $change->getPath());
729
+            }
730
+        });
731
+    }
732
+
733
+    public function notify($path) {
734
+        $path = '/' . ltrim($path, '/');
735
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
736
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
737
+    }
738 738
 }
Please login to merge, or discard this patch.