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