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