Passed
Push — master ( 09c1d8...6a940d )
by Roeland
22:24 queued 11:06
created

DAV::isCreatable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Carlos Cerrillo <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Michael Gapczynski <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Philipp Kapfer <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 * @author vkuimov "vkuimov@nextcloud"
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\Files\Storage;
35
36
use Exception;
37
use Icewind\Streams\CallbackWrapper;
38
use Icewind\Streams\IteratorDirectory;
39
use OC\Files\Filesystem;
40
use OC\MemCache\ArrayCache;
41
use OCP\AppFramework\Http;
42
use OCP\Constants;
43
use OCP\Files\FileInfo;
44
use OCP\Files\ForbiddenException;
45
use OCP\Files\StorageInvalidException;
46
use OCP\Files\StorageNotAvailableException;
47
use OCP\Http\Client\IClientService;
48
use OCP\ICertificateManager;
49
use OCP\ILogger;
50
use OCP\Util;
51
use Psr\Http\Message\ResponseInterface;
52
use Sabre\DAV\Client;
53
use Sabre\DAV\Xml\Property\ResourceType;
54
use Sabre\HTTP\ClientException;
55
use Sabre\HTTP\ClientHttpException;
56
57
/**
58
 * Class DAV
59
 *
60
 * @package OC\Files\Storage
61
 */
62
class DAV extends Common {
63
	/** @var string */
64
	protected $password;
65
	/** @var string */
66
	protected $user;
67
	/** @var string */
68
	protected $authType;
69
	/** @var string */
70
	protected $host;
71
	/** @var bool */
72
	protected $secure;
73
	/** @var string */
74
	protected $root;
75
	/** @var string */
76
	protected $certPath;
77
	/** @var bool */
78
	protected $ready;
79
	/** @var Client */
80
	protected $client;
81
	/** @var ArrayCache */
82
	protected $statCache;
83
	/** @var IClientService */
84
	protected $httpClientService;
85
	/** @var ICertificateManager */
86
	protected $certManager;
87
88
	/**
89
	 * @param array $params
90
	 * @throws \Exception
91
	 */
92
	public function __construct($params) {
93
		$this->statCache = new ArrayCache();
94
		$this->httpClientService = \OC::$server->getHTTPClientService();
95
		if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
96
			$host = $params['host'];
97
			//remove leading http[s], will be generated in createBaseUri()
98
			if (substr($host, 0, 8) == "https://") $host = substr($host, 8);
99
			else if (substr($host, 0, 7) == "http://") $host = substr($host, 7);
100
			$this->host = $host;
101
			$this->user = $params['user'];
102
			$this->password = $params['password'];
103
			if (isset($params['authType'])) {
104
				$this->authType = $params['authType'];
105
			}
106
			if (isset($params['secure'])) {
107
				if (is_string($params['secure'])) {
108
					$this->secure = ($params['secure'] === 'true');
109
				} else {
110
					$this->secure = (bool)$params['secure'];
111
				}
112
			} else {
113
				$this->secure = false;
114
			}
115
			if ($this->secure === true) {
116
				// inject mock for testing
117
				$this->certManager = \OC::$server->getCertificateManager();
118
				if (is_null($this->certManager)) { //no user
119
					$this->certManager = \OC::$server->getCertificateManager(null);
120
				}
121
			}
122
			$this->root = $params['root'] ?? '/';
123
			$this->root = '/' . ltrim($this->root, '/');
124
			$this->root = rtrim($this->root, '/') . '/';
125
		} else {
126
			throw new \Exception('Invalid webdav storage configuration');
127
		}
128
	}
129
130
	protected function init() {
131
		if ($this->ready) {
132
			return;
133
		}
134
		$this->ready = true;
135
136
		$settings = [
137
			'baseUri' => $this->createBaseUri(),
138
			'userName' => $this->user,
139
			'password' => $this->password,
140
		];
141
		if (isset($this->authType)) {
142
			$settings['authType'] = $this->authType;
143
		}
144
145
		$proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
146
		if ($proxy !== '') {
147
			$settings['proxy'] = $proxy;
148
		}
149
150
		$this->client = new Client($settings);
151
		$this->client->setThrowExceptions(true);
152
153
		if($this->secure === true) {
154
			$certPath = $this->certManager->getAbsoluteBundlePath();
155
			if (file_exists($certPath)) {
156
				$this->certPath = $certPath;
157
			}
158
			if ($this->certPath) {
159
				$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
160
			}
161
		}
162
	}
163
164
	/**
165
	 * Clear the stat cache
166
	 */
167
	public function clearStatCache() {
168
		$this->statCache->clear();
169
	}
170
171
	/** {@inheritdoc} */
172
	public function getId() {
173
		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
174
	}
175
176
	/** {@inheritdoc} */
177
	public function createBaseUri() {
178
		$baseUri = 'http';
179
		if ($this->secure) {
180
			$baseUri .= 's';
181
		}
182
		$baseUri .= '://' . $this->host . $this->root;
183
		return $baseUri;
184
	}
185
186
	/** {@inheritdoc} */
187
	public function mkdir($path) {
188
		$this->init();
189
		$path = $this->cleanPath($path);
190
		$result = $this->simpleResponse('MKCOL', $path, null, 201);
191
		if ($result) {
192
			$this->statCache->set($path, true);
193
		}
194
		return $result;
195
	}
196
197
	/** {@inheritdoc} */
198
	public function rmdir($path) {
199
		$this->init();
200
		$path = $this->cleanPath($path);
201
		// FIXME: some WebDAV impl return 403 when trying to DELETE
202
		// a non-empty folder
203
		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
204
		$this->statCache->clear($path . '/');
205
		$this->statCache->remove($path);
206
		return $result;
207
	}
208
209
	/** {@inheritdoc} */
210
	public function opendir($path) {
211
		$this->init();
212
		$path = $this->cleanPath($path);
213
		try {
214
			$response = $this->client->propFind(
215
				$this->encodePath($path),
216
				['{DAV:}getetag'],
217
				1
218
			);
219
			if ($response === false) {
0 ignored issues
show
introduced by
The condition $response === false is always false.
Loading history...
220
				return false;
221
			}
222
			$content = [];
223
			$files = array_keys($response);
224
			array_shift($files); //the first entry is the current directory
225
226
			if (!$this->statCache->hasKey($path)) {
227
				$this->statCache->set($path, true);
228
			}
229
			foreach ($files as $file) {
230
				$file = urldecode($file);
231
				// do not store the real entry, we might not have all properties
232
				if (!$this->statCache->hasKey($path)) {
233
					$this->statCache->set($file, true);
234
				}
235
				$file = basename($file);
236
				$content[] = $file;
237
			}
238
			return IteratorDirectory::wrap($content);
239
		} catch (\Exception $e) {
240
			$this->convertException($e, $path);
241
		}
242
		return false;
243
	}
244
245
	/**
246
	 * Propfind call with cache handling.
247
	 *
248
	 * First checks if information is cached.
249
	 * If not, request it from the server then store to cache.
250
	 *
251
	 * @param string $path path to propfind
252
	 *
253
	 * @return array|boolean propfind response or false if the entry was not found
254
	 *
255
	 * @throws ClientHttpException
256
	 */
257
	protected function propfind($path) {
258
		$path = $this->cleanPath($path);
259
		$cachedResponse = $this->statCache->get($path);
260
		// we either don't know it, or we know it exists but need more details
261
		if (is_null($cachedResponse) || $cachedResponse === true) {
262
			$this->init();
263
			try {
264
				$response = $this->client->propFind(
265
					$this->encodePath($path),
266
					array(
267
						'{DAV:}getlastmodified',
268
						'{DAV:}getcontentlength',
269
						'{DAV:}getcontenttype',
270
						'{http://owncloud.org/ns}permissions',
271
						'{http://open-collaboration-services.org/ns}share-permissions',
272
						'{DAV:}resourcetype',
273
						'{DAV:}getetag',
274
					)
275
				);
276
				$this->statCache->set($path, $response);
277
			} catch (ClientHttpException $e) {
278
				if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
279
					$this->statCache->clear($path . '/');
280
					$this->statCache->set($path, false);
281
					return false;
282
				}
283
				$this->convertException($e, $path);
284
			} catch (\Exception $e) {
285
				$this->convertException($e, $path);
286
			}
287
		} else {
288
			$response = $cachedResponse;
289
		}
290
		return $response;
291
	}
292
293
	/** {@inheritdoc} */
294
	public function filetype($path) {
295
		try {
296
			$response = $this->propfind($path);
297
			if ($response === false) {
298
				return false;
299
			}
300
			$responseType = [];
301
			if (isset($response["{DAV:}resourcetype"])) {
302
				/** @var ResourceType[] $response */
303
				$responseType = $response["{DAV:}resourcetype"]->getValue();
304
			}
305
			return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
306
		} catch (\Exception $e) {
307
			$this->convertException($e, $path);
308
		}
309
		return false;
310
	}
311
312
	/** {@inheritdoc} */
313
	public function file_exists($path) {
314
		try {
315
			$path = $this->cleanPath($path);
316
			$cachedState = $this->statCache->get($path);
317
			if ($cachedState === false) {
318
				// we know the file doesn't exist
319
				return false;
320
			} else if (!is_null($cachedState)) {
321
				return true;
322
			}
323
			// need to get from server
324
			return ($this->propfind($path) !== false);
325
		} catch (\Exception $e) {
326
			$this->convertException($e, $path);
327
		}
328
		return false;
329
	}
330
331
	/** {@inheritdoc} */
332
	public function unlink($path) {
333
		$this->init();
334
		$path = $this->cleanPath($path);
335
		$result = $this->simpleResponse('DELETE', $path, null, 204);
336
		$this->statCache->clear($path . '/');
337
		$this->statCache->remove($path);
338
		return $result;
339
	}
340
341
	/** {@inheritdoc} */
342
	public function fopen($path, $mode) {
343
		$this->init();
344
		$path = $this->cleanPath($path);
345
		switch ($mode) {
346
			case 'r':
347
			case 'rb':
348
				try {
349
					$response = $this->httpClientService
350
						->newClient()
351
						->get($this->createBaseUri() . $this->encodePath($path), [
352
							'auth' => [$this->user, $this->password],
353
							'stream' => true
354
						]);
355
				} catch (\GuzzleHttp\Exception\ClientException $e) {
356
					if ($e->getResponse() instanceof ResponseInterface
357
						&& $e->getResponse()->getStatusCode() === 404) {
358
						return false;
359
					} else {
360
						throw $e;
361
					}
362
				}
363
364
				if ($response->getStatusCode() !== Http::STATUS_OK) {
365
					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
366
						throw new \OCP\Lock\LockedException($path);
367
					} else {
368
						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

368
						/** @scrutinizer ignore-deprecated */ Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
369
					}
370
				}
371
372
				return $response->getBody();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->getBody() also could return the type string which is incompatible with the return type mandated by OCP\Files\Storage::fopen() of false|resource.
Loading history...
373
			case 'w':
374
			case 'wb':
375
			case 'a':
376
			case 'ab':
377
			case 'r+':
378
			case 'w+':
379
			case 'wb+':
380
			case 'a+':
381
			case 'x':
382
			case 'x+':
383
			case 'c':
384
			case 'c+':
385
				//emulate these
386
				$tempManager = \OC::$server->getTempManager();
387
				if (strrpos($path, '.') !== false) {
388
					$ext = substr($path, strrpos($path, '.'));
389
				} else {
390
					$ext = '';
391
				}
392
				if ($this->file_exists($path)) {
393
					if (!$this->isUpdatable($path)) {
394
						return false;
395
					}
396
					if ($mode === 'w' or $mode === 'w+') {
397
						$tmpFile = $tempManager->getTemporaryFile($ext);
398
					} else {
399
						$tmpFile = $this->getCachedFile($path);
400
					}
401
				} else {
402
					if (!$this->isCreatable(dirname($path))) {
403
						return false;
404
					}
405
					$tmpFile = $tempManager->getTemporaryFile($ext);
406
				}
407
				$handle = fopen($tmpFile, $mode);
408
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
409
					$this->writeBack($tmpFile, $path);
410
				});
411
		}
412
	}
413
414
	/**
415
	 * @param string $tmpFile
416
	 */
417
	public function writeBack($tmpFile, $path) {
418
		$this->uploadFile($tmpFile, $path);
419
		unlink($tmpFile);
420
	}
421
422
	/** {@inheritdoc} */
423
	public function free_space($path) {
424
		$this->init();
425
		$path = $this->cleanPath($path);
426
		try {
427
			// TODO: cacheable ?
428
			$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
429
			if ($response === false) {
0 ignored issues
show
introduced by
The condition $response === false is always false.
Loading history...
430
				return FileInfo::SPACE_UNKNOWN;
431
			}
432
			if (isset($response['{DAV:}quota-available-bytes'])) {
433
				return (int)$response['{DAV:}quota-available-bytes'];
434
			} else {
435
				return FileInfo::SPACE_UNKNOWN;
436
			}
437
		} catch (\Exception $e) {
438
			return FileInfo::SPACE_UNKNOWN;
439
		}
440
	}
441
442
	/** {@inheritdoc} */
443
	public function touch($path, $mtime = null) {
444
		$this->init();
445
		if (is_null($mtime)) {
446
			$mtime = time();
447
		}
448
		$path = $this->cleanPath($path);
449
450
		// if file exists, update the mtime, else create a new empty file
451
		if ($this->file_exists($path)) {
452
			try {
453
				$this->statCache->remove($path);
454
				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
455
				// non-owncloud clients might not have accepted the property, need to recheck it
456
				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
457
				if ($response === false) {
0 ignored issues
show
introduced by
The condition $response === false is always false.
Loading history...
458
					return false;
459
				}
460
				if (isset($response['{DAV:}getlastmodified'])) {
461
					$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
462
					if ($remoteMtime !== $mtime) {
463
						// server has not accepted the mtime
464
						return false;
465
					}
466
				}
467
			} catch (ClientHttpException $e) {
468
				if ($e->getHttpStatus() === 501) {
469
					return false;
470
				}
471
				$this->convertException($e, $path);
472
				return false;
473
			} catch (\Exception $e) {
474
				$this->convertException($e, $path);
475
				return false;
476
			}
477
		} else {
478
			$this->file_put_contents($path, '');
479
		}
480
		return true;
481
	}
482
483
	/**
484
	 * @param string $path
485
	 * @param string $data
486
	 * @return int
487
	 */
488
	public function file_put_contents($path, $data) {
489
		$path = $this->cleanPath($path);
490
		$result = parent::file_put_contents($path, $data);
491
		$this->statCache->remove($path);
492
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type integer which is incompatible with the return type mandated by OCP\Files\Storage::file_put_contents() of boolean.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
493
	}
494
495
	/**
496
	 * @param string $path
497
	 * @param string $target
498
	 */
499
	protected function uploadFile($path, $target) {
500
		$this->init();
501
502
		// invalidate
503
		$target = $this->cleanPath($target);
504
		$this->statCache->remove($target);
505
		$source = fopen($path, 'r');
506
507
		$this->httpClientService
508
			->newClient()
509
			->put($this->createBaseUri() . $this->encodePath($target), [
510
				'body' => $source,
511
				'auth' => [$this->user, $this->password]
512
			]);
513
514
		$this->removeCachedFile($target);
515
	}
516
517
	/** {@inheritdoc} */
518
	public function rename($path1, $path2) {
519
		$this->init();
520
		$path1 = $this->cleanPath($path1);
521
		$path2 = $this->cleanPath($path2);
522
		try {
523
			// overwrite directory ?
524
			if ($this->is_dir($path2)) {
525
				// needs trailing slash in destination
526
				$path2 = rtrim($path2, '/') . '/';
527
			}
528
			$this->client->request(
529
				'MOVE',
530
				$this->encodePath($path1),
531
				null,
532
				[
533
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
534
				]
535
			);
536
			$this->statCache->clear($path1 . '/');
537
			$this->statCache->clear($path2 . '/');
538
			$this->statCache->set($path1, false);
539
			$this->statCache->set($path2, true);
540
			$this->removeCachedFile($path1);
541
			$this->removeCachedFile($path2);
542
			return true;
543
		} catch (\Exception $e) {
544
			$this->convertException($e);
545
		}
546
		return false;
547
	}
548
549
	/** {@inheritdoc} */
550
	public function copy($path1, $path2) {
551
		$this->init();
552
		$path1 = $this->cleanPath($path1);
553
		$path2 = $this->cleanPath($path2);
554
		try {
555
			// overwrite directory ?
556
			if ($this->is_dir($path2)) {
557
				// needs trailing slash in destination
558
				$path2 = rtrim($path2, '/') . '/';
559
			}
560
			$this->client->request(
561
				'COPY',
562
				$this->encodePath($path1),
563
				null,
564
				[
565
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
566
				]
567
			);
568
			$this->statCache->clear($path2 . '/');
569
			$this->statCache->set($path2, true);
570
			$this->removeCachedFile($path2);
571
			return true;
572
		} catch (\Exception $e) {
573
			$this->convertException($e);
574
		}
575
		return false;
576
	}
577
578
	/** {@inheritdoc} */
579
	public function stat($path) {
580
		try {
581
			$response = $this->propfind($path);
582
			if (!$response) {
583
				return false;
584
			}
585
			return [
586
				'mtime' => strtotime($response['{DAV:}getlastmodified']),
587
				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
588
			];
589
		} catch (\Exception $e) {
590
			$this->convertException($e, $path);
591
		}
592
		return array();
593
	}
594
595
	/** {@inheritdoc} */
596
	public function getMimeType($path) {
597
		$remoteMimetype = $this->getMimeTypeFromRemote($path);
598
		if ($remoteMimetype === 'application/octet-stream') {
599
			return \OC::$server->getMimeTypeDetector()->detectPath($path);
600
		} else {
601
			return $remoteMimetype;
602
		}
603
	}
604
605
	public function getMimeTypeFromRemote($path) {
606
		try {
607
			$response = $this->propfind($path);
608
			if ($response === false) {
609
				return false;
610
			}
611
			$responseType = [];
612
			if (isset($response["{DAV:}resourcetype"])) {
613
				/** @var ResourceType[] $response */
614
				$responseType = $response["{DAV:}resourcetype"]->getValue();
615
			}
616
			$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
617
			if ($type == 'dir') {
618
				return 'httpd/unix-directory';
619
			} elseif (isset($response['{DAV:}getcontenttype'])) {
620
				return $response['{DAV:}getcontenttype'];
621
			} else {
622
				return 'application/octet-stream';
623
			}
624
		} catch (\Exception $e) {
625
			return false;
626
		}
627
	}
628
629
	/**
630
	 * @param string $path
631
	 * @return string
632
	 */
633
	public function cleanPath($path) {
634
		if ($path === '') {
635
			return $path;
636
		}
637
		$path = Filesystem::normalizePath($path);
638
		// remove leading slash
639
		return substr($path, 1);
640
	}
641
642
	/**
643
	 * URL encodes the given path but keeps the slashes
644
	 *
645
	 * @param string $path to encode
646
	 * @return string encoded path
647
	 */
648
	protected function encodePath($path) {
649
		// slashes need to stay
650
		return str_replace('%2F', '/', rawurlencode($path));
651
	}
652
653
	/**
654
	 * @param string $method
655
	 * @param string $path
656
	 * @param string|resource|null $body
657
	 * @param int $expected
658
	 * @return bool
659
	 * @throws StorageInvalidException
660
	 * @throws StorageNotAvailableException
661
	 */
662
	protected function simpleResponse($method, $path, $body, $expected) {
663
		$path = $this->cleanPath($path);
664
		try {
665
			$response = $this->client->request($method, $this->encodePath($path), $body);
666
			return $response['statusCode'] == $expected;
667
		} catch (ClientHttpException $e) {
668
			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
669
				$this->statCache->clear($path . '/');
670
				$this->statCache->set($path, false);
671
				return false;
672
			}
673
674
			$this->convertException($e, $path);
675
		} catch (\Exception $e) {
676
			$this->convertException($e, $path);
677
		}
678
		return false;
679
	}
680
681
	/**
682
	 * check if curl is installed
683
	 */
684
	public static function checkDependencies() {
685
		return true;
686
	}
687
688
	/** {@inheritdoc} */
689
	public function isUpdatable($path) {
690
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
691
	}
692
693
	/** {@inheritdoc} */
694
	public function isCreatable($path) {
695
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
696
	}
697
698
	/** {@inheritdoc} */
699
	public function isSharable($path) {
700
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
701
	}
702
703
	/** {@inheritdoc} */
704
	public function isDeletable($path) {
705
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
706
	}
707
708
	/** {@inheritdoc} */
709
	public function getPermissions($path) {
710
		$this->init();
711
		$path = $this->cleanPath($path);
712
		$response = $this->propfind($path);
713
		if ($response === false) {
714
			return 0;
715
		}
716
		if (isset($response['{http://owncloud.org/ns}permissions'])) {
717
			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
718
		} else if ($this->is_dir($path)) {
719
			return Constants::PERMISSION_ALL;
720
		} else if ($this->file_exists($path)) {
721
			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
722
		} else {
723
			return 0;
724
		}
725
	}
726
727
	/** {@inheritdoc} */
728
	public function getETag($path) {
729
		$this->init();
730
		$path = $this->cleanPath($path);
731
		$response = $this->propfind($path);
732
		if ($response === false) {
733
			return null;
734
		}
735
		if (isset($response['{DAV:}getetag'])) {
736
			$etag = trim($response['{DAV:}getetag'], '"');
737
			if (strlen($etag) > 40) {
738
				$etag = md5($etag);
739
			}
740
			return $etag;
741
		}
742
		return parent::getEtag($path);
743
	}
744
745
	/**
746
	 * @param string $permissionsString
747
	 * @return int
748
	 */
749
	protected function parsePermissions($permissionsString) {
750
		$permissions = Constants::PERMISSION_READ;
751
		if (strpos($permissionsString, 'R') !== false) {
752
			$permissions |= Constants::PERMISSION_SHARE;
753
		}
754
		if (strpos($permissionsString, 'D') !== false) {
755
			$permissions |= Constants::PERMISSION_DELETE;
756
		}
757
		if (strpos($permissionsString, 'W') !== false) {
758
			$permissions |= Constants::PERMISSION_UPDATE;
759
		}
760
		if (strpos($permissionsString, 'CK') !== false) {
761
			$permissions |= Constants::PERMISSION_CREATE;
762
			$permissions |= Constants::PERMISSION_UPDATE;
763
		}
764
		return $permissions;
765
	}
766
767
	/**
768
	 * check if a file or folder has been updated since $time
769
	 *
770
	 * @param string $path
771
	 * @param int $time
772
	 * @throws \OCP\Files\StorageNotAvailableException
773
	 * @return bool
774
	 */
775
	public function hasUpdated($path, $time) {
776
		$this->init();
777
		$path = $this->cleanPath($path);
778
		try {
779
			// force refresh for $path
780
			$this->statCache->remove($path);
781
			$response = $this->propfind($path);
782
			if ($response === false) {
783
				if ($path === '') {
784
					// if root is gone it means the storage is not available
785
					throw new StorageNotAvailableException('root is gone');
786
				}
787
				return false;
788
			}
789
			if (isset($response['{DAV:}getetag'])) {
790
				$cachedData = $this->getCache()->get($path);
791
				$etag = null;
792
				if (isset($response['{DAV:}getetag'])) {
793
					$etag = trim($response['{DAV:}getetag'], '"');
794
				}
795
				if (!empty($etag) && $cachedData['etag'] !== $etag) {
796
					return true;
797
				} else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
798
					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
799
					return $sharePermissions !== $cachedData['permissions'];
800
				} else if (isset($response['{http://owncloud.org/ns}permissions'])) {
801
					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
802
					return $permissions !== $cachedData['permissions'];
803
				} else {
804
					return false;
805
				}
806
			} else {
807
				$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
808
				return $remoteMtime > $time;
809
			}
810
		} catch (ClientHttpException $e) {
811
			if ($e->getHttpStatus() === 405) {
812
				if ($path === '') {
813
					// if root is gone it means the storage is not available
814
					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
815
				}
816
				return false;
817
			}
818
			$this->convertException($e, $path);
819
			return false;
820
		} catch (\Exception $e) {
821
			$this->convertException($e, $path);
822
			return false;
823
		}
824
	}
825
826
	/**
827
	 * Interpret the given exception and decide whether it is due to an
828
	 * unavailable storage, invalid storage or other.
829
	 * This will either throw StorageInvalidException, StorageNotAvailableException
830
	 * or do nothing.
831
	 *
832
	 * @param Exception $e sabre exception
833
	 * @param string $path optional path from the operation
834
	 *
835
	 * @throws StorageInvalidException if the storage is invalid, for example
836
	 * when the authentication expired or is invalid
837
	 * @throws StorageNotAvailableException if the storage is not available,
838
	 * which might be temporary
839
	 * @throws ForbiddenException if the action is not allowed
840
	 */
841
	protected function convertException(Exception $e, $path = '') {
842
		\OC::$server->getLogger()->logException($e, ['app' => 'files_external', 'level' => ILogger::DEBUG]);
843
		if ($e instanceof ClientHttpException) {
844
			if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
845
				throw new \OCP\Lock\LockedException($path);
846
			}
847
			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
848
				// either password was changed or was invalid all along
849
				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
850
			} else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
851
				// ignore exception for MethodNotAllowed, false will be returned
852
				return;
853
			} else if ($e->getHttpStatus() === Http::STATUS_FORBIDDEN){
854
				// The operation is forbidden. Fail somewhat gracefully
855
				throw new ForbiddenException(get_class($e) . ':' . $e->getMessage());
0 ignored issues
show
Bug introduced by
The call to OCP\Files\ForbiddenException::__construct() has too few arguments starting with retry. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

855
				throw /** @scrutinizer ignore-call */ new ForbiddenException(get_class($e) . ':' . $e->getMessage());

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
856
			}
857
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
858
		} else if ($e instanceof ClientException) {
859
			// connection timeout or refused, server could be temporarily down
860
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
861
		} else if ($e instanceof \InvalidArgumentException) {
862
			// parse error because the server returned HTML instead of XML,
863
			// possibly temporarily down
864
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
865
		} else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
866
			// rethrow
867
			throw $e;
868
		}
869
870
		// TODO: only log for now, but in the future need to wrap/rethrow exception
871
	}
872
}
873