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