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