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