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