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