Passed
Push — master ( 60f946...983435 )
by Robin
12:53 queued 12s
created

Storage   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 181
dl 0
loc 383
rs 2.8
c 1
b 0
f 0
wmc 70

25 Methods

Rating   Name   Duplication   Size   Complexity  
A remoteIsOwnCloud() 0 5 3
A testRemoteUrl() 0 24 6
A needsPartFile() 0 2 1
A getOwner() 0 2 1
A testRemote() 0 7 4
A getMountPoint() 0 2 1
A isSharable() 0 5 3
A getRemote() 0 2 1
A getId() 0 2 1
A test() 0 11 3
A getCache() 0 5 2
A ocmPermissions2ncPermissions() 0 24 6
A getPermissions() 0 16 4
A getDefaultPermissions() 0 8 2
A getScanner() 0 8 3
A free_space() 0 2 1
A __construct() 0 28 3
B getShareInfo() 0 41 7
A checkStorageAvailability() 0 26 6
A getPassword() 0 2 1
A getToken() 0 2 1
A getRemoteUser() 0 2 1
A file_exists() 0 5 2
A hasUpdated() 0 17 4
A getWatcher() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Storage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Storage, and based on these observations, apply Extract Interface, too.

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