Passed
Push — master ( 451d39...c709dd )
by Roeland
29:09 queued 17:38
created
lib/private/Files/Storage/DAV.php 2 patches
Indentation   +812 added lines, -812 removed lines patch added patch discarded remove patch
@@ -63,816 +63,816 @@
 block discarded – undo
63 63
  * @package OC\Files\Storage
64 64
  */
65 65
 class DAV extends Common {
66
-	/** @var string */
67
-	protected $password;
68
-	/** @var string */
69
-	protected $user;
70
-	/** @var string */
71
-	protected $authType;
72
-	/** @var string */
73
-	protected $host;
74
-	/** @var bool */
75
-	protected $secure;
76
-	/** @var string */
77
-	protected $root;
78
-	/** @var string */
79
-	protected $certPath;
80
-	/** @var bool */
81
-	protected $ready;
82
-	/** @var Client */
83
-	protected $client;
84
-	/** @var ArrayCache */
85
-	protected $statCache;
86
-	/** @var IClientService */
87
-	protected $httpClientService;
88
-	/** @var ICertificateManager */
89
-	protected $certManager;
90
-
91
-	/**
92
-	 * @param array $params
93
-	 * @throws \Exception
94
-	 */
95
-	public function __construct($params) {
96
-		$this->statCache = new ArrayCache();
97
-		$this->httpClientService = \OC::$server->getHTTPClientService();
98
-		if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
99
-			$host = $params['host'];
100
-			//remove leading http[s], will be generated in createBaseUri()
101
-			if (substr($host, 0, 8) == "https://") {
102
-				$host = substr($host, 8);
103
-			} elseif (substr($host, 0, 7) == "http://") {
104
-				$host = substr($host, 7);
105
-			}
106
-			$this->host = $host;
107
-			$this->user = $params['user'];
108
-			$this->password = $params['password'];
109
-			if (isset($params['authType'])) {
110
-				$this->authType = $params['authType'];
111
-			}
112
-			if (isset($params['secure'])) {
113
-				if (is_string($params['secure'])) {
114
-					$this->secure = ($params['secure'] === 'true');
115
-				} else {
116
-					$this->secure = (bool)$params['secure'];
117
-				}
118
-			} else {
119
-				$this->secure = false;
120
-			}
121
-			if ($this->secure === true) {
122
-				// inject mock for testing
123
-				$this->certManager = \OC::$server->getCertificateManager();
124
-				if (is_null($this->certManager)) { //no user
125
-					$this->certManager = \OC::$server->getCertificateManager(null);
126
-				}
127
-			}
128
-			$this->root = $params['root'] ?? '/';
129
-			$this->root = '/' . ltrim($this->root, '/');
130
-			$this->root = rtrim($this->root, '/') . '/';
131
-		} else {
132
-			throw new \Exception('Invalid webdav storage configuration');
133
-		}
134
-	}
135
-
136
-	protected function init() {
137
-		if ($this->ready) {
138
-			return;
139
-		}
140
-		$this->ready = true;
141
-
142
-		$settings = [
143
-			'baseUri' => $this->createBaseUri(),
144
-			'userName' => $this->user,
145
-			'password' => $this->password,
146
-		];
147
-		if (isset($this->authType)) {
148
-			$settings['authType'] = $this->authType;
149
-		}
150
-
151
-		$proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
152
-		if ($proxy !== '') {
153
-			$settings['proxy'] = $proxy;
154
-		}
155
-
156
-		$this->client = new Client($settings);
157
-		$this->client->setThrowExceptions(true);
158
-
159
-		if ($this->secure === true) {
160
-			$certPath = $this->certManager->getAbsoluteBundlePath();
161
-			if (file_exists($certPath)) {
162
-				$this->certPath = $certPath;
163
-			}
164
-			if ($this->certPath) {
165
-				$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
166
-			}
167
-		}
168
-	}
169
-
170
-	/**
171
-	 * Clear the stat cache
172
-	 */
173
-	public function clearStatCache() {
174
-		$this->statCache->clear();
175
-	}
176
-
177
-	/** {@inheritdoc} */
178
-	public function getId() {
179
-		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
180
-	}
181
-
182
-	/** {@inheritdoc} */
183
-	public function createBaseUri() {
184
-		$baseUri = 'http';
185
-		if ($this->secure) {
186
-			$baseUri .= 's';
187
-		}
188
-		$baseUri .= '://' . $this->host . $this->root;
189
-		return $baseUri;
190
-	}
191
-
192
-	/** {@inheritdoc} */
193
-	public function mkdir($path) {
194
-		$this->init();
195
-		$path = $this->cleanPath($path);
196
-		$result = $this->simpleResponse('MKCOL', $path, null, 201);
197
-		if ($result) {
198
-			$this->statCache->set($path, true);
199
-		}
200
-		return $result;
201
-	}
202
-
203
-	/** {@inheritdoc} */
204
-	public function rmdir($path) {
205
-		$this->init();
206
-		$path = $this->cleanPath($path);
207
-		// FIXME: some WebDAV impl return 403 when trying to DELETE
208
-		// a non-empty folder
209
-		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
210
-		$this->statCache->clear($path . '/');
211
-		$this->statCache->remove($path);
212
-		return $result;
213
-	}
214
-
215
-	/** {@inheritdoc} */
216
-	public function opendir($path) {
217
-		$this->init();
218
-		$path = $this->cleanPath($path);
219
-		try {
220
-			$response = $this->client->propFind(
221
-				$this->encodePath($path),
222
-				['{DAV:}getetag'],
223
-				1
224
-			);
225
-			if ($response === false) {
226
-				return false;
227
-			}
228
-			$content = [];
229
-			$files = array_keys($response);
230
-			array_shift($files); //the first entry is the current directory
231
-
232
-			if (!$this->statCache->hasKey($path)) {
233
-				$this->statCache->set($path, true);
234
-			}
235
-			foreach ($files as $file) {
236
-				$file = urldecode($file);
237
-				// do not store the real entry, we might not have all properties
238
-				if (!$this->statCache->hasKey($path)) {
239
-					$this->statCache->set($file, true);
240
-				}
241
-				$file = basename($file);
242
-				$content[] = $file;
243
-			}
244
-			return IteratorDirectory::wrap($content);
245
-		} catch (\Exception $e) {
246
-			$this->convertException($e, $path);
247
-		}
248
-		return false;
249
-	}
250
-
251
-	/**
252
-	 * Propfind call with cache handling.
253
-	 *
254
-	 * First checks if information is cached.
255
-	 * If not, request it from the server then store to cache.
256
-	 *
257
-	 * @param string $path path to propfind
258
-	 *
259
-	 * @return array|boolean propfind response or false if the entry was not found
260
-	 *
261
-	 * @throws ClientHttpException
262
-	 */
263
-	protected function propfind($path) {
264
-		$path = $this->cleanPath($path);
265
-		$cachedResponse = $this->statCache->get($path);
266
-		// we either don't know it, or we know it exists but need more details
267
-		if (is_null($cachedResponse) || $cachedResponse === true) {
268
-			$this->init();
269
-			try {
270
-				$response = $this->client->propFind(
271
-					$this->encodePath($path),
272
-					[
273
-						'{DAV:}getlastmodified',
274
-						'{DAV:}getcontentlength',
275
-						'{DAV:}getcontenttype',
276
-						'{http://owncloud.org/ns}permissions',
277
-						'{http://open-collaboration-services.org/ns}share-permissions',
278
-						'{DAV:}resourcetype',
279
-						'{DAV:}getetag',
280
-					]
281
-				);
282
-				$this->statCache->set($path, $response);
283
-			} catch (ClientHttpException $e) {
284
-				if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
285
-					$this->statCache->clear($path . '/');
286
-					$this->statCache->set($path, false);
287
-					return false;
288
-				}
289
-				$this->convertException($e, $path);
290
-			} catch (\Exception $e) {
291
-				$this->convertException($e, $path);
292
-			}
293
-		} else {
294
-			$response = $cachedResponse;
295
-		}
296
-		return $response;
297
-	}
298
-
299
-	/** {@inheritdoc} */
300
-	public function filetype($path) {
301
-		try {
302
-			$response = $this->propfind($path);
303
-			if ($response === false) {
304
-				return false;
305
-			}
306
-			$responseType = [];
307
-			if (isset($response["{DAV:}resourcetype"])) {
308
-				/** @var ResourceType[] $response */
309
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
310
-			}
311
-			return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
312
-		} catch (\Exception $e) {
313
-			$this->convertException($e, $path);
314
-		}
315
-		return false;
316
-	}
317
-
318
-	/** {@inheritdoc} */
319
-	public function file_exists($path) {
320
-		try {
321
-			$path = $this->cleanPath($path);
322
-			$cachedState = $this->statCache->get($path);
323
-			if ($cachedState === false) {
324
-				// we know the file doesn't exist
325
-				return false;
326
-			} elseif (!is_null($cachedState)) {
327
-				return true;
328
-			}
329
-			// need to get from server
330
-			return ($this->propfind($path) !== false);
331
-		} catch (\Exception $e) {
332
-			$this->convertException($e, $path);
333
-		}
334
-		return false;
335
-	}
336
-
337
-	/** {@inheritdoc} */
338
-	public function unlink($path) {
339
-		$this->init();
340
-		$path = $this->cleanPath($path);
341
-		$result = $this->simpleResponse('DELETE', $path, null, 204);
342
-		$this->statCache->clear($path . '/');
343
-		$this->statCache->remove($path);
344
-		return $result;
345
-	}
346
-
347
-	/** {@inheritdoc} */
348
-	public function fopen($path, $mode) {
349
-		$this->init();
350
-		$path = $this->cleanPath($path);
351
-		switch ($mode) {
352
-			case 'r':
353
-			case 'rb':
354
-				try {
355
-					$response = $this->httpClientService
356
-						->newClient()
357
-						->get($this->createBaseUri() . $this->encodePath($path), [
358
-							'auth' => [$this->user, $this->password],
359
-							'stream' => true
360
-						]);
361
-				} catch (\GuzzleHttp\Exception\ClientException $e) {
362
-					if ($e->getResponse() instanceof ResponseInterface
363
-						&& $e->getResponse()->getStatusCode() === 404) {
364
-						return false;
365
-					} else {
366
-						throw $e;
367
-					}
368
-				}
369
-
370
-				if ($response->getStatusCode() !== Http::STATUS_OK) {
371
-					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
372
-						throw new \OCP\Lock\LockedException($path);
373
-					} else {
374
-						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
375
-					}
376
-				}
377
-
378
-				return $response->getBody();
379
-			case 'w':
380
-			case 'wb':
381
-			case 'a':
382
-			case 'ab':
383
-			case 'r+':
384
-			case 'w+':
385
-			case 'wb+':
386
-			case 'a+':
387
-			case 'x':
388
-			case 'x+':
389
-			case 'c':
390
-			case 'c+':
391
-				//emulate these
392
-				$tempManager = \OC::$server->getTempManager();
393
-				if (strrpos($path, '.') !== false) {
394
-					$ext = substr($path, strrpos($path, '.'));
395
-				} else {
396
-					$ext = '';
397
-				}
398
-				if ($this->file_exists($path)) {
399
-					if (!$this->isUpdatable($path)) {
400
-						return false;
401
-					}
402
-					if ($mode === 'w' or $mode === 'w+') {
403
-						$tmpFile = $tempManager->getTemporaryFile($ext);
404
-					} else {
405
-						$tmpFile = $this->getCachedFile($path);
406
-					}
407
-				} else {
408
-					if (!$this->isCreatable(dirname($path))) {
409
-						return false;
410
-					}
411
-					$tmpFile = $tempManager->getTemporaryFile($ext);
412
-				}
413
-				$handle = fopen($tmpFile, $mode);
414
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
415
-					$this->writeBack($tmpFile, $path);
416
-				});
417
-		}
418
-	}
419
-
420
-	/**
421
-	 * @param string $tmpFile
422
-	 */
423
-	public function writeBack($tmpFile, $path) {
424
-		$this->uploadFile($tmpFile, $path);
425
-		unlink($tmpFile);
426
-	}
427
-
428
-	/** {@inheritdoc} */
429
-	public function free_space($path) {
430
-		$this->init();
431
-		$path = $this->cleanPath($path);
432
-		try {
433
-			// TODO: cacheable ?
434
-			$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
435
-			if ($response === false) {
436
-				return FileInfo::SPACE_UNKNOWN;
437
-			}
438
-			if (isset($response['{DAV:}quota-available-bytes'])) {
439
-				return (int)$response['{DAV:}quota-available-bytes'];
440
-			} else {
441
-				return FileInfo::SPACE_UNKNOWN;
442
-			}
443
-		} catch (\Exception $e) {
444
-			return FileInfo::SPACE_UNKNOWN;
445
-		}
446
-	}
447
-
448
-	/** {@inheritdoc} */
449
-	public function touch($path, $mtime = null) {
450
-		$this->init();
451
-		if (is_null($mtime)) {
452
-			$mtime = time();
453
-		}
454
-		$path = $this->cleanPath($path);
455
-
456
-		// if file exists, update the mtime, else create a new empty file
457
-		if ($this->file_exists($path)) {
458
-			try {
459
-				$this->statCache->remove($path);
460
-				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
461
-				// non-owncloud clients might not have accepted the property, need to recheck it
462
-				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
463
-				if ($response === false) {
464
-					return false;
465
-				}
466
-				if (isset($response['{DAV:}getlastmodified'])) {
467
-					$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
468
-					if ($remoteMtime !== $mtime) {
469
-						// server has not accepted the mtime
470
-						return false;
471
-					}
472
-				}
473
-			} catch (ClientHttpException $e) {
474
-				if ($e->getHttpStatus() === 501) {
475
-					return false;
476
-				}
477
-				$this->convertException($e, $path);
478
-				return false;
479
-			} catch (\Exception $e) {
480
-				$this->convertException($e, $path);
481
-				return false;
482
-			}
483
-		} else {
484
-			$this->file_put_contents($path, '');
485
-		}
486
-		return true;
487
-	}
488
-
489
-	/**
490
-	 * @param string $path
491
-	 * @param string $data
492
-	 * @return int
493
-	 */
494
-	public function file_put_contents($path, $data) {
495
-		$path = $this->cleanPath($path);
496
-		$result = parent::file_put_contents($path, $data);
497
-		$this->statCache->remove($path);
498
-		return $result;
499
-	}
500
-
501
-	/**
502
-	 * @param string $path
503
-	 * @param string $target
504
-	 */
505
-	protected function uploadFile($path, $target) {
506
-		$this->init();
507
-
508
-		// invalidate
509
-		$target = $this->cleanPath($target);
510
-		$this->statCache->remove($target);
511
-		$source = fopen($path, 'r');
512
-
513
-		$this->httpClientService
514
-			->newClient()
515
-			->put($this->createBaseUri() . $this->encodePath($target), [
516
-				'body' => $source,
517
-				'auth' => [$this->user, $this->password]
518
-			]);
519
-
520
-		$this->removeCachedFile($target);
521
-	}
522
-
523
-	/** {@inheritdoc} */
524
-	public function rename($path1, $path2) {
525
-		$this->init();
526
-		$path1 = $this->cleanPath($path1);
527
-		$path2 = $this->cleanPath($path2);
528
-		try {
529
-			// overwrite directory ?
530
-			if ($this->is_dir($path2)) {
531
-				// needs trailing slash in destination
532
-				$path2 = rtrim($path2, '/') . '/';
533
-			}
534
-			$this->client->request(
535
-				'MOVE',
536
-				$this->encodePath($path1),
537
-				null,
538
-				[
539
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
540
-				]
541
-			);
542
-			$this->statCache->clear($path1 . '/');
543
-			$this->statCache->clear($path2 . '/');
544
-			$this->statCache->set($path1, false);
545
-			$this->statCache->set($path2, true);
546
-			$this->removeCachedFile($path1);
547
-			$this->removeCachedFile($path2);
548
-			return true;
549
-		} catch (\Exception $e) {
550
-			$this->convertException($e);
551
-		}
552
-		return false;
553
-	}
554
-
555
-	/** {@inheritdoc} */
556
-	public function copy($path1, $path2) {
557
-		$this->init();
558
-		$path1 = $this->cleanPath($path1);
559
-		$path2 = $this->cleanPath($path2);
560
-		try {
561
-			// overwrite directory ?
562
-			if ($this->is_dir($path2)) {
563
-				// needs trailing slash in destination
564
-				$path2 = rtrim($path2, '/') . '/';
565
-			}
566
-			$this->client->request(
567
-				'COPY',
568
-				$this->encodePath($path1),
569
-				null,
570
-				[
571
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
572
-				]
573
-			);
574
-			$this->statCache->clear($path2 . '/');
575
-			$this->statCache->set($path2, true);
576
-			$this->removeCachedFile($path2);
577
-			return true;
578
-		} catch (\Exception $e) {
579
-			$this->convertException($e);
580
-		}
581
-		return false;
582
-	}
583
-
584
-	/** {@inheritdoc} */
585
-	public function stat($path) {
586
-		try {
587
-			$response = $this->propfind($path);
588
-			if (!$response) {
589
-				return false;
590
-			}
591
-			return [
592
-				'mtime' => strtotime($response['{DAV:}getlastmodified']),
593
-				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
594
-			];
595
-		} catch (\Exception $e) {
596
-			$this->convertException($e, $path);
597
-		}
598
-		return [];
599
-	}
600
-
601
-	/** {@inheritdoc} */
602
-	public function getMimeType($path) {
603
-		$remoteMimetype = $this->getMimeTypeFromRemote($path);
604
-		if ($remoteMimetype === 'application/octet-stream') {
605
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
606
-		} else {
607
-			return $remoteMimetype;
608
-		}
609
-	}
610
-
611
-	public function getMimeTypeFromRemote($path) {
612
-		try {
613
-			$response = $this->propfind($path);
614
-			if ($response === false) {
615
-				return false;
616
-			}
617
-			$responseType = [];
618
-			if (isset($response["{DAV:}resourcetype"])) {
619
-				/** @var ResourceType[] $response */
620
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
621
-			}
622
-			$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
623
-			if ($type == 'dir') {
624
-				return 'httpd/unix-directory';
625
-			} elseif (isset($response['{DAV:}getcontenttype'])) {
626
-				return $response['{DAV:}getcontenttype'];
627
-			} else {
628
-				return 'application/octet-stream';
629
-			}
630
-		} catch (\Exception $e) {
631
-			return false;
632
-		}
633
-	}
634
-
635
-	/**
636
-	 * @param string $path
637
-	 * @return string
638
-	 */
639
-	public function cleanPath($path) {
640
-		if ($path === '') {
641
-			return $path;
642
-		}
643
-		$path = Filesystem::normalizePath($path);
644
-		// remove leading slash
645
-		return substr($path, 1);
646
-	}
647
-
648
-	/**
649
-	 * URL encodes the given path but keeps the slashes
650
-	 *
651
-	 * @param string $path to encode
652
-	 * @return string encoded path
653
-	 */
654
-	protected function encodePath($path) {
655
-		// slashes need to stay
656
-		return str_replace('%2F', '/', rawurlencode($path));
657
-	}
658
-
659
-	/**
660
-	 * @param string $method
661
-	 * @param string $path
662
-	 * @param string|resource|null $body
663
-	 * @param int $expected
664
-	 * @return bool
665
-	 * @throws StorageInvalidException
666
-	 * @throws StorageNotAvailableException
667
-	 */
668
-	protected function simpleResponse($method, $path, $body, $expected) {
669
-		$path = $this->cleanPath($path);
670
-		try {
671
-			$response = $this->client->request($method, $this->encodePath($path), $body);
672
-			return $response['statusCode'] == $expected;
673
-		} catch (ClientHttpException $e) {
674
-			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
675
-				$this->statCache->clear($path . '/');
676
-				$this->statCache->set($path, false);
677
-				return false;
678
-			}
679
-
680
-			$this->convertException($e, $path);
681
-		} catch (\Exception $e) {
682
-			$this->convertException($e, $path);
683
-		}
684
-		return false;
685
-	}
686
-
687
-	/**
688
-	 * check if curl is installed
689
-	 */
690
-	public static function checkDependencies() {
691
-		return true;
692
-	}
693
-
694
-	/** {@inheritdoc} */
695
-	public function isUpdatable($path) {
696
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
697
-	}
698
-
699
-	/** {@inheritdoc} */
700
-	public function isCreatable($path) {
701
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
702
-	}
703
-
704
-	/** {@inheritdoc} */
705
-	public function isSharable($path) {
706
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
707
-	}
708
-
709
-	/** {@inheritdoc} */
710
-	public function isDeletable($path) {
711
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
712
-	}
713
-
714
-	/** {@inheritdoc} */
715
-	public function getPermissions($path) {
716
-		$this->init();
717
-		$path = $this->cleanPath($path);
718
-		$response = $this->propfind($path);
719
-		if ($response === false) {
720
-			return 0;
721
-		}
722
-		if (isset($response['{http://owncloud.org/ns}permissions'])) {
723
-			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
724
-		} elseif ($this->is_dir($path)) {
725
-			return Constants::PERMISSION_ALL;
726
-		} elseif ($this->file_exists($path)) {
727
-			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
728
-		} else {
729
-			return 0;
730
-		}
731
-	}
732
-
733
-	/** {@inheritdoc} */
734
-	public function getETag($path) {
735
-		$this->init();
736
-		$path = $this->cleanPath($path);
737
-		$response = $this->propfind($path);
738
-		if ($response === false) {
739
-			return null;
740
-		}
741
-		if (isset($response['{DAV:}getetag'])) {
742
-			$etag = trim($response['{DAV:}getetag'], '"');
743
-			if (strlen($etag) > 40) {
744
-				$etag = md5($etag);
745
-			}
746
-			return $etag;
747
-		}
748
-		return parent::getEtag($path);
749
-	}
750
-
751
-	/**
752
-	 * @param string $permissionsString
753
-	 * @return int
754
-	 */
755
-	protected function parsePermissions($permissionsString) {
756
-		$permissions = Constants::PERMISSION_READ;
757
-		if (strpos($permissionsString, 'R') !== false) {
758
-			$permissions |= Constants::PERMISSION_SHARE;
759
-		}
760
-		if (strpos($permissionsString, 'D') !== false) {
761
-			$permissions |= Constants::PERMISSION_DELETE;
762
-		}
763
-		if (strpos($permissionsString, 'W') !== false) {
764
-			$permissions |= Constants::PERMISSION_UPDATE;
765
-		}
766
-		if (strpos($permissionsString, 'CK') !== false) {
767
-			$permissions |= Constants::PERMISSION_CREATE;
768
-			$permissions |= Constants::PERMISSION_UPDATE;
769
-		}
770
-		return $permissions;
771
-	}
772
-
773
-	/**
774
-	 * check if a file or folder has been updated since $time
775
-	 *
776
-	 * @param string $path
777
-	 * @param int $time
778
-	 * @throws \OCP\Files\StorageNotAvailableException
779
-	 * @return bool
780
-	 */
781
-	public function hasUpdated($path, $time) {
782
-		$this->init();
783
-		$path = $this->cleanPath($path);
784
-		try {
785
-			// force refresh for $path
786
-			$this->statCache->remove($path);
787
-			$response = $this->propfind($path);
788
-			if ($response === false) {
789
-				if ($path === '') {
790
-					// if root is gone it means the storage is not available
791
-					throw new StorageNotAvailableException('root is gone');
792
-				}
793
-				return false;
794
-			}
795
-			if (isset($response['{DAV:}getetag'])) {
796
-				$cachedData = $this->getCache()->get($path);
797
-				$etag = null;
798
-				if (isset($response['{DAV:}getetag'])) {
799
-					$etag = trim($response['{DAV:}getetag'], '"');
800
-				}
801
-				if (!empty($etag) && $cachedData['etag'] !== $etag) {
802
-					return true;
803
-				} elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
804
-					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
805
-					return $sharePermissions !== $cachedData['permissions'];
806
-				} elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
807
-					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
808
-					return $permissions !== $cachedData['permissions'];
809
-				} else {
810
-					return false;
811
-				}
812
-			} else {
813
-				$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
814
-				return $remoteMtime > $time;
815
-			}
816
-		} catch (ClientHttpException $e) {
817
-			if ($e->getHttpStatus() === 405) {
818
-				if ($path === '') {
819
-					// if root is gone it means the storage is not available
820
-					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
821
-				}
822
-				return false;
823
-			}
824
-			$this->convertException($e, $path);
825
-			return false;
826
-		} catch (\Exception $e) {
827
-			$this->convertException($e, $path);
828
-			return false;
829
-		}
830
-	}
831
-
832
-	/**
833
-	 * Interpret the given exception and decide whether it is due to an
834
-	 * unavailable storage, invalid storage or other.
835
-	 * This will either throw StorageInvalidException, StorageNotAvailableException
836
-	 * or do nothing.
837
-	 *
838
-	 * @param Exception $e sabre exception
839
-	 * @param string $path optional path from the operation
840
-	 *
841
-	 * @throws StorageInvalidException if the storage is invalid, for example
842
-	 * when the authentication expired or is invalid
843
-	 * @throws StorageNotAvailableException if the storage is not available,
844
-	 * which might be temporary
845
-	 * @throws ForbiddenException if the action is not allowed
846
-	 */
847
-	protected function convertException(Exception $e, $path = '') {
848
-		\OC::$server->getLogger()->logException($e, ['app' => 'files_external', 'level' => ILogger::DEBUG]);
849
-		if ($e instanceof ClientHttpException) {
850
-			if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
851
-				throw new \OCP\Lock\LockedException($path);
852
-			}
853
-			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
854
-				// either password was changed or was invalid all along
855
-				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
856
-			} elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
857
-				// ignore exception for MethodNotAllowed, false will be returned
858
-				return;
859
-			} elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
860
-				// The operation is forbidden. Fail somewhat gracefully
861
-				throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
862
-			}
863
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
864
-		} elseif ($e instanceof ClientException) {
865
-			// connection timeout or refused, server could be temporarily down
866
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
867
-		} elseif ($e instanceof \InvalidArgumentException) {
868
-			// parse error because the server returned HTML instead of XML,
869
-			// possibly temporarily down
870
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
871
-		} elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
872
-			// rethrow
873
-			throw $e;
874
-		}
875
-
876
-		// TODO: only log for now, but in the future need to wrap/rethrow exception
877
-	}
66
+    /** @var string */
67
+    protected $password;
68
+    /** @var string */
69
+    protected $user;
70
+    /** @var string */
71
+    protected $authType;
72
+    /** @var string */
73
+    protected $host;
74
+    /** @var bool */
75
+    protected $secure;
76
+    /** @var string */
77
+    protected $root;
78
+    /** @var string */
79
+    protected $certPath;
80
+    /** @var bool */
81
+    protected $ready;
82
+    /** @var Client */
83
+    protected $client;
84
+    /** @var ArrayCache */
85
+    protected $statCache;
86
+    /** @var IClientService */
87
+    protected $httpClientService;
88
+    /** @var ICertificateManager */
89
+    protected $certManager;
90
+
91
+    /**
92
+     * @param array $params
93
+     * @throws \Exception
94
+     */
95
+    public function __construct($params) {
96
+        $this->statCache = new ArrayCache();
97
+        $this->httpClientService = \OC::$server->getHTTPClientService();
98
+        if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
99
+            $host = $params['host'];
100
+            //remove leading http[s], will be generated in createBaseUri()
101
+            if (substr($host, 0, 8) == "https://") {
102
+                $host = substr($host, 8);
103
+            } elseif (substr($host, 0, 7) == "http://") {
104
+                $host = substr($host, 7);
105
+            }
106
+            $this->host = $host;
107
+            $this->user = $params['user'];
108
+            $this->password = $params['password'];
109
+            if (isset($params['authType'])) {
110
+                $this->authType = $params['authType'];
111
+            }
112
+            if (isset($params['secure'])) {
113
+                if (is_string($params['secure'])) {
114
+                    $this->secure = ($params['secure'] === 'true');
115
+                } else {
116
+                    $this->secure = (bool)$params['secure'];
117
+                }
118
+            } else {
119
+                $this->secure = false;
120
+            }
121
+            if ($this->secure === true) {
122
+                // inject mock for testing
123
+                $this->certManager = \OC::$server->getCertificateManager();
124
+                if (is_null($this->certManager)) { //no user
125
+                    $this->certManager = \OC::$server->getCertificateManager(null);
126
+                }
127
+            }
128
+            $this->root = $params['root'] ?? '/';
129
+            $this->root = '/' . ltrim($this->root, '/');
130
+            $this->root = rtrim($this->root, '/') . '/';
131
+        } else {
132
+            throw new \Exception('Invalid webdav storage configuration');
133
+        }
134
+    }
135
+
136
+    protected function init() {
137
+        if ($this->ready) {
138
+            return;
139
+        }
140
+        $this->ready = true;
141
+
142
+        $settings = [
143
+            'baseUri' => $this->createBaseUri(),
144
+            'userName' => $this->user,
145
+            'password' => $this->password,
146
+        ];
147
+        if (isset($this->authType)) {
148
+            $settings['authType'] = $this->authType;
149
+        }
150
+
151
+        $proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
152
+        if ($proxy !== '') {
153
+            $settings['proxy'] = $proxy;
154
+        }
155
+
156
+        $this->client = new Client($settings);
157
+        $this->client->setThrowExceptions(true);
158
+
159
+        if ($this->secure === true) {
160
+            $certPath = $this->certManager->getAbsoluteBundlePath();
161
+            if (file_exists($certPath)) {
162
+                $this->certPath = $certPath;
163
+            }
164
+            if ($this->certPath) {
165
+                $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
166
+            }
167
+        }
168
+    }
169
+
170
+    /**
171
+     * Clear the stat cache
172
+     */
173
+    public function clearStatCache() {
174
+        $this->statCache->clear();
175
+    }
176
+
177
+    /** {@inheritdoc} */
178
+    public function getId() {
179
+        return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
180
+    }
181
+
182
+    /** {@inheritdoc} */
183
+    public function createBaseUri() {
184
+        $baseUri = 'http';
185
+        if ($this->secure) {
186
+            $baseUri .= 's';
187
+        }
188
+        $baseUri .= '://' . $this->host . $this->root;
189
+        return $baseUri;
190
+    }
191
+
192
+    /** {@inheritdoc} */
193
+    public function mkdir($path) {
194
+        $this->init();
195
+        $path = $this->cleanPath($path);
196
+        $result = $this->simpleResponse('MKCOL', $path, null, 201);
197
+        if ($result) {
198
+            $this->statCache->set($path, true);
199
+        }
200
+        return $result;
201
+    }
202
+
203
+    /** {@inheritdoc} */
204
+    public function rmdir($path) {
205
+        $this->init();
206
+        $path = $this->cleanPath($path);
207
+        // FIXME: some WebDAV impl return 403 when trying to DELETE
208
+        // a non-empty folder
209
+        $result = $this->simpleResponse('DELETE', $path . '/', null, 204);
210
+        $this->statCache->clear($path . '/');
211
+        $this->statCache->remove($path);
212
+        return $result;
213
+    }
214
+
215
+    /** {@inheritdoc} */
216
+    public function opendir($path) {
217
+        $this->init();
218
+        $path = $this->cleanPath($path);
219
+        try {
220
+            $response = $this->client->propFind(
221
+                $this->encodePath($path),
222
+                ['{DAV:}getetag'],
223
+                1
224
+            );
225
+            if ($response === false) {
226
+                return false;
227
+            }
228
+            $content = [];
229
+            $files = array_keys($response);
230
+            array_shift($files); //the first entry is the current directory
231
+
232
+            if (!$this->statCache->hasKey($path)) {
233
+                $this->statCache->set($path, true);
234
+            }
235
+            foreach ($files as $file) {
236
+                $file = urldecode($file);
237
+                // do not store the real entry, we might not have all properties
238
+                if (!$this->statCache->hasKey($path)) {
239
+                    $this->statCache->set($file, true);
240
+                }
241
+                $file = basename($file);
242
+                $content[] = $file;
243
+            }
244
+            return IteratorDirectory::wrap($content);
245
+        } catch (\Exception $e) {
246
+            $this->convertException($e, $path);
247
+        }
248
+        return false;
249
+    }
250
+
251
+    /**
252
+     * Propfind call with cache handling.
253
+     *
254
+     * First checks if information is cached.
255
+     * If not, request it from the server then store to cache.
256
+     *
257
+     * @param string $path path to propfind
258
+     *
259
+     * @return array|boolean propfind response or false if the entry was not found
260
+     *
261
+     * @throws ClientHttpException
262
+     */
263
+    protected function propfind($path) {
264
+        $path = $this->cleanPath($path);
265
+        $cachedResponse = $this->statCache->get($path);
266
+        // we either don't know it, or we know it exists but need more details
267
+        if (is_null($cachedResponse) || $cachedResponse === true) {
268
+            $this->init();
269
+            try {
270
+                $response = $this->client->propFind(
271
+                    $this->encodePath($path),
272
+                    [
273
+                        '{DAV:}getlastmodified',
274
+                        '{DAV:}getcontentlength',
275
+                        '{DAV:}getcontenttype',
276
+                        '{http://owncloud.org/ns}permissions',
277
+                        '{http://open-collaboration-services.org/ns}share-permissions',
278
+                        '{DAV:}resourcetype',
279
+                        '{DAV:}getetag',
280
+                    ]
281
+                );
282
+                $this->statCache->set($path, $response);
283
+            } catch (ClientHttpException $e) {
284
+                if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
285
+                    $this->statCache->clear($path . '/');
286
+                    $this->statCache->set($path, false);
287
+                    return false;
288
+                }
289
+                $this->convertException($e, $path);
290
+            } catch (\Exception $e) {
291
+                $this->convertException($e, $path);
292
+            }
293
+        } else {
294
+            $response = $cachedResponse;
295
+        }
296
+        return $response;
297
+    }
298
+
299
+    /** {@inheritdoc} */
300
+    public function filetype($path) {
301
+        try {
302
+            $response = $this->propfind($path);
303
+            if ($response === false) {
304
+                return false;
305
+            }
306
+            $responseType = [];
307
+            if (isset($response["{DAV:}resourcetype"])) {
308
+                /** @var ResourceType[] $response */
309
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
310
+            }
311
+            return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
312
+        } catch (\Exception $e) {
313
+            $this->convertException($e, $path);
314
+        }
315
+        return false;
316
+    }
317
+
318
+    /** {@inheritdoc} */
319
+    public function file_exists($path) {
320
+        try {
321
+            $path = $this->cleanPath($path);
322
+            $cachedState = $this->statCache->get($path);
323
+            if ($cachedState === false) {
324
+                // we know the file doesn't exist
325
+                return false;
326
+            } elseif (!is_null($cachedState)) {
327
+                return true;
328
+            }
329
+            // need to get from server
330
+            return ($this->propfind($path) !== false);
331
+        } catch (\Exception $e) {
332
+            $this->convertException($e, $path);
333
+        }
334
+        return false;
335
+    }
336
+
337
+    /** {@inheritdoc} */
338
+    public function unlink($path) {
339
+        $this->init();
340
+        $path = $this->cleanPath($path);
341
+        $result = $this->simpleResponse('DELETE', $path, null, 204);
342
+        $this->statCache->clear($path . '/');
343
+        $this->statCache->remove($path);
344
+        return $result;
345
+    }
346
+
347
+    /** {@inheritdoc} */
348
+    public function fopen($path, $mode) {
349
+        $this->init();
350
+        $path = $this->cleanPath($path);
351
+        switch ($mode) {
352
+            case 'r':
353
+            case 'rb':
354
+                try {
355
+                    $response = $this->httpClientService
356
+                        ->newClient()
357
+                        ->get($this->createBaseUri() . $this->encodePath($path), [
358
+                            'auth' => [$this->user, $this->password],
359
+                            'stream' => true
360
+                        ]);
361
+                } catch (\GuzzleHttp\Exception\ClientException $e) {
362
+                    if ($e->getResponse() instanceof ResponseInterface
363
+                        && $e->getResponse()->getStatusCode() === 404) {
364
+                        return false;
365
+                    } else {
366
+                        throw $e;
367
+                    }
368
+                }
369
+
370
+                if ($response->getStatusCode() !== Http::STATUS_OK) {
371
+                    if ($response->getStatusCode() === Http::STATUS_LOCKED) {
372
+                        throw new \OCP\Lock\LockedException($path);
373
+                    } else {
374
+                        Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
375
+                    }
376
+                }
377
+
378
+                return $response->getBody();
379
+            case 'w':
380
+            case 'wb':
381
+            case 'a':
382
+            case 'ab':
383
+            case 'r+':
384
+            case 'w+':
385
+            case 'wb+':
386
+            case 'a+':
387
+            case 'x':
388
+            case 'x+':
389
+            case 'c':
390
+            case 'c+':
391
+                //emulate these
392
+                $tempManager = \OC::$server->getTempManager();
393
+                if (strrpos($path, '.') !== false) {
394
+                    $ext = substr($path, strrpos($path, '.'));
395
+                } else {
396
+                    $ext = '';
397
+                }
398
+                if ($this->file_exists($path)) {
399
+                    if (!$this->isUpdatable($path)) {
400
+                        return false;
401
+                    }
402
+                    if ($mode === 'w' or $mode === 'w+') {
403
+                        $tmpFile = $tempManager->getTemporaryFile($ext);
404
+                    } else {
405
+                        $tmpFile = $this->getCachedFile($path);
406
+                    }
407
+                } else {
408
+                    if (!$this->isCreatable(dirname($path))) {
409
+                        return false;
410
+                    }
411
+                    $tmpFile = $tempManager->getTemporaryFile($ext);
412
+                }
413
+                $handle = fopen($tmpFile, $mode);
414
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
415
+                    $this->writeBack($tmpFile, $path);
416
+                });
417
+        }
418
+    }
419
+
420
+    /**
421
+     * @param string $tmpFile
422
+     */
423
+    public function writeBack($tmpFile, $path) {
424
+        $this->uploadFile($tmpFile, $path);
425
+        unlink($tmpFile);
426
+    }
427
+
428
+    /** {@inheritdoc} */
429
+    public function free_space($path) {
430
+        $this->init();
431
+        $path = $this->cleanPath($path);
432
+        try {
433
+            // TODO: cacheable ?
434
+            $response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
435
+            if ($response === false) {
436
+                return FileInfo::SPACE_UNKNOWN;
437
+            }
438
+            if (isset($response['{DAV:}quota-available-bytes'])) {
439
+                return (int)$response['{DAV:}quota-available-bytes'];
440
+            } else {
441
+                return FileInfo::SPACE_UNKNOWN;
442
+            }
443
+        } catch (\Exception $e) {
444
+            return FileInfo::SPACE_UNKNOWN;
445
+        }
446
+    }
447
+
448
+    /** {@inheritdoc} */
449
+    public function touch($path, $mtime = null) {
450
+        $this->init();
451
+        if (is_null($mtime)) {
452
+            $mtime = time();
453
+        }
454
+        $path = $this->cleanPath($path);
455
+
456
+        // if file exists, update the mtime, else create a new empty file
457
+        if ($this->file_exists($path)) {
458
+            try {
459
+                $this->statCache->remove($path);
460
+                $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
461
+                // non-owncloud clients might not have accepted the property, need to recheck it
462
+                $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
463
+                if ($response === false) {
464
+                    return false;
465
+                }
466
+                if (isset($response['{DAV:}getlastmodified'])) {
467
+                    $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
468
+                    if ($remoteMtime !== $mtime) {
469
+                        // server has not accepted the mtime
470
+                        return false;
471
+                    }
472
+                }
473
+            } catch (ClientHttpException $e) {
474
+                if ($e->getHttpStatus() === 501) {
475
+                    return false;
476
+                }
477
+                $this->convertException($e, $path);
478
+                return false;
479
+            } catch (\Exception $e) {
480
+                $this->convertException($e, $path);
481
+                return false;
482
+            }
483
+        } else {
484
+            $this->file_put_contents($path, '');
485
+        }
486
+        return true;
487
+    }
488
+
489
+    /**
490
+     * @param string $path
491
+     * @param string $data
492
+     * @return int
493
+     */
494
+    public function file_put_contents($path, $data) {
495
+        $path = $this->cleanPath($path);
496
+        $result = parent::file_put_contents($path, $data);
497
+        $this->statCache->remove($path);
498
+        return $result;
499
+    }
500
+
501
+    /**
502
+     * @param string $path
503
+     * @param string $target
504
+     */
505
+    protected function uploadFile($path, $target) {
506
+        $this->init();
507
+
508
+        // invalidate
509
+        $target = $this->cleanPath($target);
510
+        $this->statCache->remove($target);
511
+        $source = fopen($path, 'r');
512
+
513
+        $this->httpClientService
514
+            ->newClient()
515
+            ->put($this->createBaseUri() . $this->encodePath($target), [
516
+                'body' => $source,
517
+                'auth' => [$this->user, $this->password]
518
+            ]);
519
+
520
+        $this->removeCachedFile($target);
521
+    }
522
+
523
+    /** {@inheritdoc} */
524
+    public function rename($path1, $path2) {
525
+        $this->init();
526
+        $path1 = $this->cleanPath($path1);
527
+        $path2 = $this->cleanPath($path2);
528
+        try {
529
+            // overwrite directory ?
530
+            if ($this->is_dir($path2)) {
531
+                // needs trailing slash in destination
532
+                $path2 = rtrim($path2, '/') . '/';
533
+            }
534
+            $this->client->request(
535
+                'MOVE',
536
+                $this->encodePath($path1),
537
+                null,
538
+                [
539
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
540
+                ]
541
+            );
542
+            $this->statCache->clear($path1 . '/');
543
+            $this->statCache->clear($path2 . '/');
544
+            $this->statCache->set($path1, false);
545
+            $this->statCache->set($path2, true);
546
+            $this->removeCachedFile($path1);
547
+            $this->removeCachedFile($path2);
548
+            return true;
549
+        } catch (\Exception $e) {
550
+            $this->convertException($e);
551
+        }
552
+        return false;
553
+    }
554
+
555
+    /** {@inheritdoc} */
556
+    public function copy($path1, $path2) {
557
+        $this->init();
558
+        $path1 = $this->cleanPath($path1);
559
+        $path2 = $this->cleanPath($path2);
560
+        try {
561
+            // overwrite directory ?
562
+            if ($this->is_dir($path2)) {
563
+                // needs trailing slash in destination
564
+                $path2 = rtrim($path2, '/') . '/';
565
+            }
566
+            $this->client->request(
567
+                'COPY',
568
+                $this->encodePath($path1),
569
+                null,
570
+                [
571
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
572
+                ]
573
+            );
574
+            $this->statCache->clear($path2 . '/');
575
+            $this->statCache->set($path2, true);
576
+            $this->removeCachedFile($path2);
577
+            return true;
578
+        } catch (\Exception $e) {
579
+            $this->convertException($e);
580
+        }
581
+        return false;
582
+    }
583
+
584
+    /** {@inheritdoc} */
585
+    public function stat($path) {
586
+        try {
587
+            $response = $this->propfind($path);
588
+            if (!$response) {
589
+                return false;
590
+            }
591
+            return [
592
+                'mtime' => strtotime($response['{DAV:}getlastmodified']),
593
+                'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
594
+            ];
595
+        } catch (\Exception $e) {
596
+            $this->convertException($e, $path);
597
+        }
598
+        return [];
599
+    }
600
+
601
+    /** {@inheritdoc} */
602
+    public function getMimeType($path) {
603
+        $remoteMimetype = $this->getMimeTypeFromRemote($path);
604
+        if ($remoteMimetype === 'application/octet-stream') {
605
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
606
+        } else {
607
+            return $remoteMimetype;
608
+        }
609
+    }
610
+
611
+    public function getMimeTypeFromRemote($path) {
612
+        try {
613
+            $response = $this->propfind($path);
614
+            if ($response === false) {
615
+                return false;
616
+            }
617
+            $responseType = [];
618
+            if (isset($response["{DAV:}resourcetype"])) {
619
+                /** @var ResourceType[] $response */
620
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
621
+            }
622
+            $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
623
+            if ($type == 'dir') {
624
+                return 'httpd/unix-directory';
625
+            } elseif (isset($response['{DAV:}getcontenttype'])) {
626
+                return $response['{DAV:}getcontenttype'];
627
+            } else {
628
+                return 'application/octet-stream';
629
+            }
630
+        } catch (\Exception $e) {
631
+            return false;
632
+        }
633
+    }
634
+
635
+    /**
636
+     * @param string $path
637
+     * @return string
638
+     */
639
+    public function cleanPath($path) {
640
+        if ($path === '') {
641
+            return $path;
642
+        }
643
+        $path = Filesystem::normalizePath($path);
644
+        // remove leading slash
645
+        return substr($path, 1);
646
+    }
647
+
648
+    /**
649
+     * URL encodes the given path but keeps the slashes
650
+     *
651
+     * @param string $path to encode
652
+     * @return string encoded path
653
+     */
654
+    protected function encodePath($path) {
655
+        // slashes need to stay
656
+        return str_replace('%2F', '/', rawurlencode($path));
657
+    }
658
+
659
+    /**
660
+     * @param string $method
661
+     * @param string $path
662
+     * @param string|resource|null $body
663
+     * @param int $expected
664
+     * @return bool
665
+     * @throws StorageInvalidException
666
+     * @throws StorageNotAvailableException
667
+     */
668
+    protected function simpleResponse($method, $path, $body, $expected) {
669
+        $path = $this->cleanPath($path);
670
+        try {
671
+            $response = $this->client->request($method, $this->encodePath($path), $body);
672
+            return $response['statusCode'] == $expected;
673
+        } catch (ClientHttpException $e) {
674
+            if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
675
+                $this->statCache->clear($path . '/');
676
+                $this->statCache->set($path, false);
677
+                return false;
678
+            }
679
+
680
+            $this->convertException($e, $path);
681
+        } catch (\Exception $e) {
682
+            $this->convertException($e, $path);
683
+        }
684
+        return false;
685
+    }
686
+
687
+    /**
688
+     * check if curl is installed
689
+     */
690
+    public static function checkDependencies() {
691
+        return true;
692
+    }
693
+
694
+    /** {@inheritdoc} */
695
+    public function isUpdatable($path) {
696
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
697
+    }
698
+
699
+    /** {@inheritdoc} */
700
+    public function isCreatable($path) {
701
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
702
+    }
703
+
704
+    /** {@inheritdoc} */
705
+    public function isSharable($path) {
706
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
707
+    }
708
+
709
+    /** {@inheritdoc} */
710
+    public function isDeletable($path) {
711
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
712
+    }
713
+
714
+    /** {@inheritdoc} */
715
+    public function getPermissions($path) {
716
+        $this->init();
717
+        $path = $this->cleanPath($path);
718
+        $response = $this->propfind($path);
719
+        if ($response === false) {
720
+            return 0;
721
+        }
722
+        if (isset($response['{http://owncloud.org/ns}permissions'])) {
723
+            return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
724
+        } elseif ($this->is_dir($path)) {
725
+            return Constants::PERMISSION_ALL;
726
+        } elseif ($this->file_exists($path)) {
727
+            return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
728
+        } else {
729
+            return 0;
730
+        }
731
+    }
732
+
733
+    /** {@inheritdoc} */
734
+    public function getETag($path) {
735
+        $this->init();
736
+        $path = $this->cleanPath($path);
737
+        $response = $this->propfind($path);
738
+        if ($response === false) {
739
+            return null;
740
+        }
741
+        if (isset($response['{DAV:}getetag'])) {
742
+            $etag = trim($response['{DAV:}getetag'], '"');
743
+            if (strlen($etag) > 40) {
744
+                $etag = md5($etag);
745
+            }
746
+            return $etag;
747
+        }
748
+        return parent::getEtag($path);
749
+    }
750
+
751
+    /**
752
+     * @param string $permissionsString
753
+     * @return int
754
+     */
755
+    protected function parsePermissions($permissionsString) {
756
+        $permissions = Constants::PERMISSION_READ;
757
+        if (strpos($permissionsString, 'R') !== false) {
758
+            $permissions |= Constants::PERMISSION_SHARE;
759
+        }
760
+        if (strpos($permissionsString, 'D') !== false) {
761
+            $permissions |= Constants::PERMISSION_DELETE;
762
+        }
763
+        if (strpos($permissionsString, 'W') !== false) {
764
+            $permissions |= Constants::PERMISSION_UPDATE;
765
+        }
766
+        if (strpos($permissionsString, 'CK') !== false) {
767
+            $permissions |= Constants::PERMISSION_CREATE;
768
+            $permissions |= Constants::PERMISSION_UPDATE;
769
+        }
770
+        return $permissions;
771
+    }
772
+
773
+    /**
774
+     * check if a file or folder has been updated since $time
775
+     *
776
+     * @param string $path
777
+     * @param int $time
778
+     * @throws \OCP\Files\StorageNotAvailableException
779
+     * @return bool
780
+     */
781
+    public function hasUpdated($path, $time) {
782
+        $this->init();
783
+        $path = $this->cleanPath($path);
784
+        try {
785
+            // force refresh for $path
786
+            $this->statCache->remove($path);
787
+            $response = $this->propfind($path);
788
+            if ($response === false) {
789
+                if ($path === '') {
790
+                    // if root is gone it means the storage is not available
791
+                    throw new StorageNotAvailableException('root is gone');
792
+                }
793
+                return false;
794
+            }
795
+            if (isset($response['{DAV:}getetag'])) {
796
+                $cachedData = $this->getCache()->get($path);
797
+                $etag = null;
798
+                if (isset($response['{DAV:}getetag'])) {
799
+                    $etag = trim($response['{DAV:}getetag'], '"');
800
+                }
801
+                if (!empty($etag) && $cachedData['etag'] !== $etag) {
802
+                    return true;
803
+                } elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
804
+                    $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
805
+                    return $sharePermissions !== $cachedData['permissions'];
806
+                } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
807
+                    $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
808
+                    return $permissions !== $cachedData['permissions'];
809
+                } else {
810
+                    return false;
811
+                }
812
+            } else {
813
+                $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
814
+                return $remoteMtime > $time;
815
+            }
816
+        } catch (ClientHttpException $e) {
817
+            if ($e->getHttpStatus() === 405) {
818
+                if ($path === '') {
819
+                    // if root is gone it means the storage is not available
820
+                    throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
821
+                }
822
+                return false;
823
+            }
824
+            $this->convertException($e, $path);
825
+            return false;
826
+        } catch (\Exception $e) {
827
+            $this->convertException($e, $path);
828
+            return false;
829
+        }
830
+    }
831
+
832
+    /**
833
+     * Interpret the given exception and decide whether it is due to an
834
+     * unavailable storage, invalid storage or other.
835
+     * This will either throw StorageInvalidException, StorageNotAvailableException
836
+     * or do nothing.
837
+     *
838
+     * @param Exception $e sabre exception
839
+     * @param string $path optional path from the operation
840
+     *
841
+     * @throws StorageInvalidException if the storage is invalid, for example
842
+     * when the authentication expired or is invalid
843
+     * @throws StorageNotAvailableException if the storage is not available,
844
+     * which might be temporary
845
+     * @throws ForbiddenException if the action is not allowed
846
+     */
847
+    protected function convertException(Exception $e, $path = '') {
848
+        \OC::$server->getLogger()->logException($e, ['app' => 'files_external', 'level' => ILogger::DEBUG]);
849
+        if ($e instanceof ClientHttpException) {
850
+            if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
851
+                throw new \OCP\Lock\LockedException($path);
852
+            }
853
+            if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
854
+                // either password was changed or was invalid all along
855
+                throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
856
+            } elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
857
+                // ignore exception for MethodNotAllowed, false will be returned
858
+                return;
859
+            } elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
860
+                // The operation is forbidden. Fail somewhat gracefully
861
+                throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
862
+            }
863
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
864
+        } elseif ($e instanceof ClientException) {
865
+            // connection timeout or refused, server could be temporarily down
866
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
867
+        } elseif ($e instanceof \InvalidArgumentException) {
868
+            // parse error because the server returned HTML instead of XML,
869
+            // possibly temporarily down
870
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
871
+        } elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
872
+            // rethrow
873
+            throw $e;
874
+        }
875
+
876
+        // TODO: only log for now, but in the future need to wrap/rethrow exception
877
+    }
878 878
 }
Please login to merge, or discard this patch.
Spacing   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -113,7 +113,7 @@  discard block
 block discarded – undo
113 113
 				if (is_string($params['secure'])) {
114 114
 					$this->secure = ($params['secure'] === 'true');
115 115
 				} else {
116
-					$this->secure = (bool)$params['secure'];
116
+					$this->secure = (bool) $params['secure'];
117 117
 				}
118 118
 			} else {
119 119
 				$this->secure = false;
@@ -126,8 +126,8 @@  discard block
 block discarded – undo
126 126
 				}
127 127
 			}
128 128
 			$this->root = $params['root'] ?? '/';
129
-			$this->root = '/' . ltrim($this->root, '/');
130
-			$this->root = rtrim($this->root, '/') . '/';
129
+			$this->root = '/'.ltrim($this->root, '/');
130
+			$this->root = rtrim($this->root, '/').'/';
131 131
 		} else {
132 132
 			throw new \Exception('Invalid webdav storage configuration');
133 133
 		}
@@ -176,7 +176,7 @@  discard block
 block discarded – undo
176 176
 
177 177
 	/** {@inheritdoc} */
178 178
 	public function getId() {
179
-		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
179
+		return 'webdav::'.$this->user.'@'.$this->host.'/'.$this->root;
180 180
 	}
181 181
 
182 182
 	/** {@inheritdoc} */
@@ -185,7 +185,7 @@  discard block
 block discarded – undo
185 185
 		if ($this->secure) {
186 186
 			$baseUri .= 's';
187 187
 		}
188
-		$baseUri .= '://' . $this->host . $this->root;
188
+		$baseUri .= '://'.$this->host.$this->root;
189 189
 		return $baseUri;
190 190
 	}
191 191
 
@@ -206,8 +206,8 @@  discard block
 block discarded – undo
206 206
 		$path = $this->cleanPath($path);
207 207
 		// FIXME: some WebDAV impl return 403 when trying to DELETE
208 208
 		// a non-empty folder
209
-		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
210
-		$this->statCache->clear($path . '/');
209
+		$result = $this->simpleResponse('DELETE', $path.'/', null, 204);
210
+		$this->statCache->clear($path.'/');
211 211
 		$this->statCache->remove($path);
212 212
 		return $result;
213 213
 	}
@@ -282,7 +282,7 @@  discard block
 block discarded – undo
282 282
 				$this->statCache->set($path, $response);
283 283
 			} catch (ClientHttpException $e) {
284 284
 				if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
285
-					$this->statCache->clear($path . '/');
285
+					$this->statCache->clear($path.'/');
286 286
 					$this->statCache->set($path, false);
287 287
 					return false;
288 288
 				}
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 		$this->init();
340 340
 		$path = $this->cleanPath($path);
341 341
 		$result = $this->simpleResponse('DELETE', $path, null, 204);
342
-		$this->statCache->clear($path . '/');
342
+		$this->statCache->clear($path.'/');
343 343
 		$this->statCache->remove($path);
344 344
 		return $result;
345 345
 	}
@@ -354,7 +354,7 @@  discard block
 block discarded – undo
354 354
 				try {
355 355
 					$response = $this->httpClientService
356 356
 						->newClient()
357
-						->get($this->createBaseUri() . $this->encodePath($path), [
357
+						->get($this->createBaseUri().$this->encodePath($path), [
358 358
 							'auth' => [$this->user, $this->password],
359 359
 							'stream' => true
360 360
 						]);
@@ -371,7 +371,7 @@  discard block
 block discarded – undo
371 371
 					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
372 372
 						throw new \OCP\Lock\LockedException($path);
373 373
 					} else {
374
-						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
374
+						Util::writeLog("webdav client", 'Guzzle get returned status code '.$response->getStatusCode(), ILogger::ERROR);
375 375
 					}
376 376
 				}
377 377
 
@@ -411,7 +411,7 @@  discard block
 block discarded – undo
411 411
 					$tmpFile = $tempManager->getTemporaryFile($ext);
412 412
 				}
413 413
 				$handle = fopen($tmpFile, $mode);
414
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
414
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
415 415
 					$this->writeBack($tmpFile, $path);
416 416
 				});
417 417
 		}
@@ -436,7 +436,7 @@  discard block
 block discarded – undo
436 436
 				return FileInfo::SPACE_UNKNOWN;
437 437
 			}
438 438
 			if (isset($response['{DAV:}quota-available-bytes'])) {
439
-				return (int)$response['{DAV:}quota-available-bytes'];
439
+				return (int) $response['{DAV:}quota-available-bytes'];
440 440
 			} else {
441 441
 				return FileInfo::SPACE_UNKNOWN;
442 442
 			}
@@ -512,7 +512,7 @@  discard block
 block discarded – undo
512 512
 
513 513
 		$this->httpClientService
514 514
 			->newClient()
515
-			->put($this->createBaseUri() . $this->encodePath($target), [
515
+			->put($this->createBaseUri().$this->encodePath($target), [
516 516
 				'body' => $source,
517 517
 				'auth' => [$this->user, $this->password]
518 518
 			]);
@@ -529,18 +529,18 @@  discard block
 block discarded – undo
529 529
 			// overwrite directory ?
530 530
 			if ($this->is_dir($path2)) {
531 531
 				// needs trailing slash in destination
532
-				$path2 = rtrim($path2, '/') . '/';
532
+				$path2 = rtrim($path2, '/').'/';
533 533
 			}
534 534
 			$this->client->request(
535 535
 				'MOVE',
536 536
 				$this->encodePath($path1),
537 537
 				null,
538 538
 				[
539
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
539
+					'Destination' => $this->createBaseUri().$this->encodePath($path2),
540 540
 				]
541 541
 			);
542
-			$this->statCache->clear($path1 . '/');
543
-			$this->statCache->clear($path2 . '/');
542
+			$this->statCache->clear($path1.'/');
543
+			$this->statCache->clear($path2.'/');
544 544
 			$this->statCache->set($path1, false);
545 545
 			$this->statCache->set($path2, true);
546 546
 			$this->removeCachedFile($path1);
@@ -561,17 +561,17 @@  discard block
 block discarded – undo
561 561
 			// overwrite directory ?
562 562
 			if ($this->is_dir($path2)) {
563 563
 				// needs trailing slash in destination
564
-				$path2 = rtrim($path2, '/') . '/';
564
+				$path2 = rtrim($path2, '/').'/';
565 565
 			}
566 566
 			$this->client->request(
567 567
 				'COPY',
568 568
 				$this->encodePath($path1),
569 569
 				null,
570 570
 				[
571
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
571
+					'Destination' => $this->createBaseUri().$this->encodePath($path2),
572 572
 				]
573 573
 			);
574
-			$this->statCache->clear($path2 . '/');
574
+			$this->statCache->clear($path2.'/');
575 575
 			$this->statCache->set($path2, true);
576 576
 			$this->removeCachedFile($path2);
577 577
 			return true;
@@ -590,7 +590,7 @@  discard block
 block discarded – undo
590 590
 			}
591 591
 			return [
592 592
 				'mtime' => strtotime($response['{DAV:}getlastmodified']),
593
-				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
593
+				'size' => (int) isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
594 594
 			];
595 595
 		} catch (\Exception $e) {
596 596
 			$this->convertException($e, $path);
@@ -672,7 +672,7 @@  discard block
 block discarded – undo
672 672
 			return $response['statusCode'] == $expected;
673 673
 		} catch (ClientHttpException $e) {
674 674
 			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
675
-				$this->statCache->clear($path . '/');
675
+				$this->statCache->clear($path.'/');
676 676
 				$this->statCache->set($path, false);
677 677
 				return false;
678 678
 			}
@@ -693,22 +693,22 @@  discard block
 block discarded – undo
693 693
 
694 694
 	/** {@inheritdoc} */
695 695
 	public function isUpdatable($path) {
696
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
696
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
697 697
 	}
698 698
 
699 699
 	/** {@inheritdoc} */
700 700
 	public function isCreatable($path) {
701
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
701
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_CREATE);
702 702
 	}
703 703
 
704 704
 	/** {@inheritdoc} */
705 705
 	public function isSharable($path) {
706
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
706
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
707 707
 	}
708 708
 
709 709
 	/** {@inheritdoc} */
710 710
 	public function isDeletable($path) {
711
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
711
+		return (bool) ($this->getPermissions($path) & Constants::PERMISSION_DELETE);
712 712
 	}
713 713
 
714 714
 	/** {@inheritdoc} */
@@ -801,7 +801,7 @@  discard block
 block discarded – undo
801 801
 				if (!empty($etag) && $cachedData['etag'] !== $etag) {
802 802
 					return true;
803 803
 				} elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
804
-					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
804
+					$sharePermissions = (int) $response['{http://open-collaboration-services.org/ns}share-permissions'];
805 805
 					return $sharePermissions !== $cachedData['permissions'];
806 806
 				} elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
807 807
 					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
@@ -817,7 +817,7 @@  discard block
 block discarded – undo
817 817
 			if ($e->getHttpStatus() === 405) {
818 818
 				if ($path === '') {
819 819
 					// if root is gone it means the storage is not available
820
-					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
820
+					throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
821 821
 				}
822 822
 				return false;
823 823
 			}
@@ -852,22 +852,22 @@  discard block
 block discarded – undo
852 852
 			}
853 853
 			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
854 854
 				// either password was changed or was invalid all along
855
-				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
855
+				throw new StorageInvalidException(get_class($e).': '.$e->getMessage());
856 856
 			} elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
857 857
 				// ignore exception for MethodNotAllowed, false will be returned
858 858
 				return;
859 859
 			} elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
860 860
 				// The operation is forbidden. Fail somewhat gracefully
861
-				throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
861
+				throw new ForbiddenException(get_class($e).':'.$e->getMessage(), false);
862 862
 			}
863
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
863
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
864 864
 		} elseif ($e instanceof ClientException) {
865 865
 			// connection timeout or refused, server could be temporarily down
866
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
866
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
867 867
 		} elseif ($e instanceof \InvalidArgumentException) {
868 868
 			// parse error because the server returned HTML instead of XML,
869 869
 			// possibly temporarily down
870
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
870
+			throw new StorageNotAvailableException(get_class($e).': '.$e->getMessage());
871 871
 		} elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
872 872
 			// rethrow
873 873
 			throw $e;
Please login to merge, or discard this patch.