Passed
Push — master ( d06716...a6bc87 )
by John
25:35 queued 09:59
created

Storage::getDefaultPermissions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Daniel Kesselberg <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
namespace OCA\Files_Sharing\External;
33
34
use GuzzleHttp\Exception\ClientException;
35
use GuzzleHttp\Exception\ConnectException;
36
use GuzzleHttp\Exception\RequestException;
37
use OC\Files\Storage\DAV;
38
use OC\ForbiddenException;
39
use OCA\Files_Sharing\ISharedStorage;
40
use OCA\Files_Sharing\External\Manager as ExternalShareManager;
41
use OCP\AppFramework\Http;
42
use OCP\Constants;
43
use OCP\Federation\ICloudId;
44
use OCP\Files\NotFoundException;
45
use OCP\Files\Storage\IDisableEncryptionStorage;
46
use OCP\Files\StorageInvalidException;
47
use OCP\Files\StorageNotAvailableException;
48
use OCP\Http\Client\LocalServerException;
49
use OCP\Http\Client\IClientService;
50
51
class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage {
52
	/** @var ICloudId */
53
	private $cloudId;
54
	/** @var string */
55
	private $mountPoint;
56
	/** @var string */
57
	private $token;
58
	/** @var \OCP\ICacheFactory */
59
	private $memcacheFactory;
60
	/** @var \OCP\Http\Client\IClientService */
61
	private $httpClient;
62
	/** @var bool */
63
	private $updateChecked = false;
64
65
	/** @var ExternalShareManager */
66
	private $manager;
67
68
	/**
69
	 * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options
70
	 */
71
	public function __construct($options) {
72
		$this->memcacheFactory = \OC::$server->getMemCacheFactory();
73
		$this->httpClient = $options['HttpClientService'];
74
75
		$this->manager = $options['manager'];
76
		$this->cloudId = $options['cloudId'];
77
		$discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
78
79
		[$protocol, $remote] = explode('://', $this->cloudId->getRemote());
80
		if (strpos($remote, '/')) {
81
			[$host, $root] = explode('/', $remote, 2);
82
		} else {
83
			$host = $remote;
84
			$root = '';
85
		}
86
		$secure = $protocol === 'https';
87
		$federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
88
		$webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
89
		$root = rtrim($root, '/') . $webDavEndpoint;
90
		$this->mountPoint = $options['mountpoint'];
91
		$this->token = $options['token'];
92
93
		parent::__construct([
94
			'secure' => $secure,
95
			'host' => $host,
96
			'root' => $root,
97
			'user' => $options['token'],
98
			'password' => (string)$options['password']
99
		]);
100
	}
101
102
	public function getWatcher($path = '', $storage = null) {
103
		if (!$storage) {
104
			$storage = $this;
105
		}
106
		if (!isset($this->watcher)) {
107
			$this->watcher = new Watcher($storage);
108
			$this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
109
		}
110
		return $this->watcher;
111
	}
112
113
	public function getRemoteUser(): string {
114
		return $this->cloudId->getUser();
115
	}
116
117
	public function getRemote(): string {
118
		return $this->cloudId->getRemote();
119
	}
120
121
	public function getMountPoint(): string {
122
		return $this->mountPoint;
123
	}
124
125
	public function getToken(): string {
126
		return $this->token;
127
	}
128
129
	public function getPassword(): ?string {
130
		return $this->password;
131
	}
132
133
	/**
134
	 * Get id of the mount point.
135
	 * @return string
136
	 */
137
	public function getId() {
138
		return 'shared::' . md5($this->token . '@' . $this->getRemote());
139
	}
140
141
	public function getCache($path = '', $storage = null) {
142
		if (is_null($this->cache)) {
143
			$this->cache = new Cache($this, $this->cloudId);
144
		}
145
		return $this->cache;
146
	}
147
148
	/**
149
	 * @param string $path
150
	 * @param \OC\Files\Storage\Storage $storage
151
	 * @return \OCA\Files_Sharing\External\Scanner
152
	 */
153
	public function getScanner($path = '', $storage = null) {
154
		if (!$storage) {
155
			$storage = $this;
156
		}
157
		if (!isset($this->scanner)) {
158
			$this->scanner = new Scanner($storage);
159
		}
160
		return $this->scanner;
161
	}
162
163
	/**
164
	 * Check if a file or folder has been updated since $time
165
	 *
166
	 * @param string $path
167
	 * @param int $time
168
	 * @throws \OCP\Files\StorageNotAvailableException
169
	 * @throws \OCP\Files\StorageInvalidException
170
	 * @return bool
171
	 */
172
	public function hasUpdated($path, $time) {
173
		// since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
174
		// because of that we only do one check for the entire storage per request
175
		if ($this->updateChecked) {
176
			return false;
177
		}
178
		$this->updateChecked = true;
179
		try {
180
			return parent::hasUpdated('', $time);
181
		} catch (StorageInvalidException $e) {
182
			// check if it needs to be removed
183
			$this->checkStorageAvailability();
184
			throw $e;
185
		} catch (StorageNotAvailableException $e) {
186
			// check if it needs to be removed or just temp unavailable
187
			$this->checkStorageAvailability();
188
			throw $e;
189
		}
190
	}
191
192
	public function test() {
193
		try {
194
			return parent::test();
195
		} catch (StorageInvalidException $e) {
196
			// check if it needs to be removed
197
			$this->checkStorageAvailability();
198
			throw $e;
199
		} catch (StorageNotAvailableException $e) {
200
			// check if it needs to be removed or just temp unavailable
201
			$this->checkStorageAvailability();
202
			throw $e;
203
		}
204
	}
205
206
	/**
207
	 * Check whether this storage is permanently or temporarily
208
	 * unavailable
209
	 *
210
	 * @throws \OCP\Files\StorageNotAvailableException
211
	 * @throws \OCP\Files\StorageInvalidException
212
	 */
213
	public function checkStorageAvailability() {
214
		// see if we can find out why the share is unavailable
215
		try {
216
			$this->getShareInfo();
217
		} catch (NotFoundException $e) {
218
			// a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
219
			if ($this->testRemote()) {
220
				// valid Nextcloud instance means that the public share no longer exists
221
				// since this is permanent (re-sharing the file will create a new token)
222
				// we remove the invalid storage
223
				$this->manager->removeShare($this->mountPoint);
224
				$this->manager->getMountManager()->removeMount($this->mountPoint);
225
				throw new StorageInvalidException("Remote share not found", 0, $e);
226
			} else {
227
				// Nextcloud instance is gone, likely to be a temporary server configuration error
228
				throw new StorageNotAvailableException("No nextcloud instance found at remote", 0, $e);
229
			}
230
		} catch (ForbiddenException $e) {
231
			// auth error, remove share for now (provide a dialog in the future)
232
			$this->manager->removeShare($this->mountPoint);
233
			$this->manager->getMountManager()->removeMount($this->mountPoint);
234
			throw new StorageInvalidException("Auth error when getting remote share");
235
		} catch (\GuzzleHttp\Exception\ConnectException $e) {
236
			throw new StorageNotAvailableException("Failed to connect to remote instance", 0, $e);
237
		} catch (\GuzzleHttp\Exception\RequestException $e) {
238
			throw new StorageNotAvailableException("Error while sending request to remote instance", 0, $e);
239
		}
240
	}
241
242
	public function file_exists($path) {
243
		if ($path === '') {
244
			return true;
245
		} else {
246
			return parent::file_exists($path);
247
		}
248
	}
249
250
	/**
251
	 * Check if the configured remote is a valid federated share provider
252
	 *
253
	 * @return bool
254
	 */
255
	protected function testRemote(): bool {
256
		try {
257
			return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
258
				|| $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
259
				|| $this->testRemoteUrl($this->getRemote() . '/status.php');
260
		} catch (\Exception $e) {
261
			return false;
262
		}
263
	}
264
265
	private function testRemoteUrl(string $url): bool {
266
		$cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
267
		if ($cache->hasKey($url)) {
268
			return (bool)$cache->get($url);
269
		}
270
271
		$client = $this->httpClient->newClient();
272
		try {
273
			$result = $client->get($url, [
274
				'timeout' => 10,
275
				'connect_timeout' => 10,
276
			])->getBody();
277
			$data = json_decode($result);
278
			$returnValue = (is_object($data) && !empty($data->version));
279
		} catch (ConnectException $e) {
280
			$returnValue = false;
281
		} catch (ClientException $e) {
282
			$returnValue = false;
283
		} catch (RequestException $e) {
284
			$returnValue = false;
285
		}
286
287
		$cache->set($url, $returnValue, 60 * 60 * 24);
288
		return $returnValue;
289
	}
290
291
	/**
292
	 * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing
293
	 * features are not standardized.
294
	 *
295
	 * @throws LocalServerException
296
	 */
297
	public function remoteIsOwnCloud(): bool {
298
		if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
299
			return false;
300
		}
301
		return true;
302
	}
303
304
	/**
305
	 * @return mixed
306
	 * @throws ForbiddenException
307
	 * @throws NotFoundException
308
	 * @throws \Exception
309
	 */
310
	public function getShareInfo() {
311
		$remote = $this->getRemote();
312
		$token = $this->getToken();
313
		$password = $this->getPassword();
314
315
		try {
316
			// If remote is not an ownCloud do not try to get any share info
317
			if (!$this->remoteIsOwnCloud()) {
318
				return ['status' => 'unsupported'];
319
			}
320
		} catch (LocalServerException $e) {
321
			// throw this to be on the safe side: the share will still be visible
322
			// in the UI in case the failure is intermittent, and the user will
323
			// be able to decide whether to remove it if it's really gone
324
			throw new StorageNotAvailableException();
325
		}
326
327
		$url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
328
329
		// TODO: DI
330
		$client = \OC::$server->getHTTPClientService()->newClient();
331
		try {
332
			$response = $client->post($url, [
333
				'body' => ['password' => $password],
334
				'timeout' => 10,
335
				'connect_timeout' => 10,
336
			]);
337
		} catch (\GuzzleHttp\Exception\RequestException $e) {
338
			if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
339
				throw new ForbiddenException();
340
			}
341
			if ($e->getCode() === Http::STATUS_NOT_FOUND) {
342
				throw new NotFoundException();
343
			}
344
			// throw this to be on the safe side: the share will still be visible
345
			// in the UI in case the failure is intermittent, and the user will
346
			// be able to decide whether to remove it if it's really gone
347
			throw new StorageNotAvailableException();
348
		}
349
350
		return json_decode($response->getBody(), true);
351
	}
352
353
	public function getOwner($path) {
354
		return $this->cloudId->getDisplayId();
355
	}
356
357
	public function isSharable($path) {
358
		if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
359
			return false;
360
		}
361
		return ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getPermiss...tants::PERMISSION_SHARE returns the type integer which is incompatible with the return type mandated by OCP\Files\Storage::isSharable() 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...
362
	}
363
364
	public function getPermissions($path) {
365
		$response = $this->propfind($path);
366
		// old federated sharing permissions
367
		if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
368
			$permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
369
		} elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) {
370
			// permissions provided by the OCM API
371
			$permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path);
372
		} else {
373
			// use default permission if remote server doesn't provide the share permissions
374
			$permissions = $this->getDefaultPermissions($path);
375
		}
376
377
		return $permissions;
378
	}
379
380
	public function needsPartFile() {
381
		return false;
382
	}
383
384
	/**
385
	 * Translate OCM Permissions to Nextcloud permissions
386
	 *
387
	 * @param string $ocmPermissions json encoded OCM permissions
388
	 * @param string $path path to file
389
	 * @return int
390
	 */
391
	protected function ocmPermissions2ncPermissions(string $ocmPermissions, string $path): int {
392
		try {
393
			$ocmPermissions = json_decode($ocmPermissions);
394
			$ncPermissions = 0;
395
			foreach ($ocmPermissions as $permission) {
396
				switch (strtolower($permission)) {
397
					case 'read':
398
						$ncPermissions += Constants::PERMISSION_READ;
399
						break;
400
					case 'write':
401
						$ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
402
						break;
403
					case 'share':
404
						$ncPermissions += Constants::PERMISSION_SHARE;
405
						break;
406
					default:
407
						throw new \Exception();
408
				}
409
			}
410
		} catch (\Exception $e) {
411
			$ncPermissions = $this->getDefaultPermissions($path);
412
		}
413
414
		return $ncPermissions;
415
	}
416
417
	/**
418
	 * Calculate the default permissions in case no permissions are provided
419
	 */
420
	protected function getDefaultPermissions(string $path): int {
421
		if ($this->is_dir($path)) {
422
			$permissions = Constants::PERMISSION_ALL;
423
		} else {
424
			$permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
425
		}
426
427
		return $permissions;
428
	}
429
}
430