Passed
Push — master ( 5cdc85...37718d )
by Morris
38:53 queued 21:57
created

Storage::getId()   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 0
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 Bjoern Schiessle <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Vincent Petry <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OCA\Files_Sharing\External;
32
33
use GuzzleHttp\Exception\ClientException;
34
use GuzzleHttp\Exception\ConnectException;
35
use OC\Files\Storage\DAV;
36
use OC\ForbiddenException;
37
use OCA\Files_Sharing\ISharedStorage;
38
use OCP\AppFramework\Http;
39
use OCP\Constants;
40
use OCP\Federation\ICloudId;
41
use OCP\Files\NotFoundException;
42
use OCP\Files\StorageInvalidException;
43
use OCP\Files\StorageNotAvailableException;
44
45
class Storage extends DAV implements ISharedStorage {
46
	/** @var ICloudId */
47
	private $cloudId;
48
	/** @var string */
49
	private $mountPoint;
50
	/** @var string */
51
	private $token;
52
	/** @var \OCP\ICacheFactory */
53
	private $memcacheFactory;
54
	/** @var \OCP\Http\Client\IClientService */
55
	private $httpClient;
56
	/** @var bool */
57
	private $updateChecked = false;
58
59
	/**
60
	 * @var \OCA\Files_Sharing\External\Manager
61
	 */
62
	private $manager;
63
64
	public function __construct($options) {
65
		$this->memcacheFactory = \OC::$server->getMemCacheFactory();
66
		$this->httpClient = $options['HttpClientService'];
67
68
		$this->manager = $options['manager'];
69
		$this->cloudId = $options['cloudId'];
70
		$discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
71
72
		list($protocol, $remote) = explode('://', $this->cloudId->getRemote());
73
		if (strpos($remote, '/')) {
74
			list($host, $root) = explode('/', $remote, 2);
75
		} else {
76
			$host = $remote;
77
			$root = '';
78
		}
79
		$secure = $protocol === 'https';
80
		$federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
0 ignored issues
show
Bug introduced by
The method discover() does not exist on stdClass. ( Ignorable by Annotation )

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

80
		/** @scrutinizer ignore-call */ 
81
  $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
81
		$webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
82
		$root = rtrim($root, '/') . $webDavEndpoint;
83
		$this->mountPoint = $options['mountpoint'];
84
		$this->token = $options['token'];
85
86
		parent::__construct(array(
87
			'secure' => $secure,
88
			'host' => $host,
89
			'root' => $root,
90
			'user' => $options['token'],
91
			'password' => (string)$options['password']
92
		));
93
	}
94
95
	public function getWatcher($path = '', $storage = null) {
96
		if (!$storage) {
97
			$storage = $this;
98
		}
99
		if (!isset($this->watcher)) {
100
			$this->watcher = new Watcher($storage);
101
			$this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
102
		}
103
		return $this->watcher;
104
	}
105
106
	public function getRemoteUser() {
107
		return $this->cloudId->getUser();
108
	}
109
110
	public function getRemote() {
111
		return $this->cloudId->getRemote();
112
	}
113
114
	public function getMountPoint() {
115
		return $this->mountPoint;
116
	}
117
118
	public function getToken() {
119
		return $this->token;
120
	}
121
122
	public function getPassword() {
123
		return $this->password;
124
	}
125
126
	/**
127
	 * @brief get id of the mount point
128
	 * @return string
129
	 */
130
	public function getId() {
131
		return 'shared::' . md5($this->token . '@' . $this->getRemote());
132
	}
133
134
	public function getCache($path = '', $storage = null) {
135
		if (is_null($this->cache)) {
136
			$this->cache = new Cache($this, $this->cloudId);
137
		}
138
		return $this->cache;
139
	}
140
141
	/**
142
	 * @param string $path
143
	 * @param \OC\Files\Storage\Storage $storage
144
	 * @return \OCA\Files_Sharing\External\Scanner
145
	 */
146
	public function getScanner($path = '', $storage = null) {
147
		if (!$storage) {
148
			$storage = $this;
149
		}
150
		if (!isset($this->scanner)) {
151
			$this->scanner = new Scanner($storage);
152
		}
153
		return $this->scanner;
154
	}
155
156
	/**
157
	 * check if a file or folder has been updated since $time
158
	 *
159
	 * @param string $path
160
	 * @param int $time
161
	 * @throws \OCP\Files\StorageNotAvailableException
162
	 * @throws \OCP\Files\StorageInvalidException
163
	 * @return bool
164
	 */
165
	public function hasUpdated($path, $time) {
166
		// since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
167
		// because of that we only do one check for the entire storage per request
168
		if ($this->updateChecked) {
169
			return false;
170
		}
171
		$this->updateChecked = true;
172
		try {
173
			return parent::hasUpdated('', $time);
174
		} catch (StorageInvalidException $e) {
175
			// check if it needs to be removed
176
			$this->checkStorageAvailability();
177
			throw $e;
178
		} catch (StorageNotAvailableException $e) {
179
			// check if it needs to be removed or just temp unavailable
180
			$this->checkStorageAvailability();
181
			throw $e;
182
		}
183
	}
184
185
	public function test() {
186
		try {
187
			return parent::test();
188
		} catch (StorageInvalidException $e) {
189
			// check if it needs to be removed
190
			$this->checkStorageAvailability();
191
			throw $e;
192
		} catch (StorageNotAvailableException $e) {
193
			// check if it needs to be removed or just temp unavailable
194
			$this->checkStorageAvailability();
195
			throw $e;
196
		}
197
	}
198
199
	/**
200
	 * Check whether this storage is permanently or temporarily
201
	 * unavailable
202
	 *
203
	 * @throws \OCP\Files\StorageNotAvailableException
204
	 * @throws \OCP\Files\StorageInvalidException
205
	 */
206
	public function checkStorageAvailability() {
207
		// see if we can find out why the share is unavailable
208
		try {
209
			$this->getShareInfo();
210
		} catch (NotFoundException $e) {
211
			// a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
212
			if ($this->testRemote()) {
213
				// valid Nextcloud instance means that the public share no longer exists
214
				// since this is permanent (re-sharing the file will create a new token)
215
				// we remove the invalid storage
216
				$this->manager->removeShare($this->mountPoint);
217
				$this->manager->getMountManager()->removeMount($this->mountPoint);
218
				throw new StorageInvalidException();
219
			} else {
220
				// Nextcloud instance is gone, likely to be a temporary server configuration error
221
				throw new StorageNotAvailableException();
222
			}
223
		} catch (ForbiddenException $e) {
224
			// auth error, remove share for now (provide a dialog in the future)
225
			$this->manager->removeShare($this->mountPoint);
226
			$this->manager->getMountManager()->removeMount($this->mountPoint);
227
			throw new StorageInvalidException();
228
		} catch (\GuzzleHttp\Exception\ConnectException $e) {
229
			throw new StorageNotAvailableException();
230
		} catch (\GuzzleHttp\Exception\RequestException $e) {
231
			throw new StorageNotAvailableException();
232
		} catch (\Exception $e) {
233
			throw $e;
234
		}
235
	}
236
237
	public function file_exists($path) {
238
		if ($path === '') {
239
			return true;
240
		} else {
241
			return parent::file_exists($path);
242
		}
243
	}
244
245
	/**
246
	 * check if the configured remote is a valid federated share provider
247
	 *
248
	 * @return bool
249
	 */
250
	protected function testRemote() {
251
		try {
252
			return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
253
				|| $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
254
				|| $this->testRemoteUrl($this->getRemote() . '/status.php');
255
		} catch (\Exception $e) {
256
			return false;
257
		}
258
	}
259
260
	/**
261
	 * @param string $url
262
	 * @return bool
263
	 */
264
	private function testRemoteUrl($url) {
265
		$cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
266
		if($cache->hasKey($url)) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\ICache::hasKey() has been deprecated: 9.1.0 Directly read from GET to prevent race conditions ( Ignorable by Annotation )

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

266
		if(/** @scrutinizer ignore-deprecated */ $cache->hasKey($url)) {

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...
267
			return (bool)$cache->get($url);
268
		}
269
270
		$client = $this->httpClient->newClient();
271
		try {
272
			$result = $client->get($url, [
273
				'timeout' => 10,
274
				'connect_timeout' => 10,
275
			])->getBody();
276
			$data = json_decode($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type resource; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

276
			$data = json_decode(/** @scrutinizer ignore-type */ $result);
Loading history...
277
			$returnValue = (is_object($data) && !empty($data->version));
278
		} catch (ConnectException $e) {
279
			$returnValue = false;
280
		} catch (ClientException $e) {
281
			$returnValue = false;
282
		}
283
284
		$cache->set($url, $returnValue, 60*60*24);
285
		return $returnValue;
286
	}
287
288
	/**
289
	 * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
290
	 * standardized. Let's use this to detect whether to use it.
291
	 *
292
	 * @return bool
293
	 */
294
	public function remoteIsOwnCloud() {
295
		if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
296
			return false;
297
		}
298
		return true;
299
	}
300
301
	/**
302
	 * @return mixed
303
	 * @throws ForbiddenException
304
	 * @throws NotFoundException
305
	 * @throws \Exception
306
	 */
307
	public function getShareInfo() {
308
		$remote = $this->getRemote();
309
		$token = $this->getToken();
310
		$password = $this->getPassword();
311
312
		// If remote is not an ownCloud do not try to get any share info
313
		if(!$this->remoteIsOwnCloud()) {
314
			return ['status' => 'unsupported'];
315
		}
316
317
		$url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
318
319
		// TODO: DI
320
		$client = \OC::$server->getHTTPClientService()->newClient();
321
		try {
322
			$response = $client->post($url, [
323
				'body' => ['password' => $password],
324
				'timeout' => 10,
325
				'connect_timeout' => 10,
326
			]);
327
		} catch (\GuzzleHttp\Exception\RequestException $e) {
328
			if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
329
				throw new ForbiddenException();
330
			}
331
			if ($e->getCode() === Http::STATUS_NOT_FOUND) {
332
				throw new NotFoundException();
333
			}
334
			// throw this to be on the safe side: the share will still be visible
335
			// in the UI in case the failure is intermittent, and the user will
336
			// be able to decide whether to remove it if it's really gone
337
			throw new StorageNotAvailableException();
338
		}
339
340
		return json_decode($response->getBody(), true);
0 ignored issues
show
Bug introduced by
It seems like $response->getBody() can also be of type resource; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

340
		return json_decode(/** @scrutinizer ignore-type */ $response->getBody(), true);
Loading history...
341
	}
342
343
	public function getOwner($path) {
344
		return $this->cloudId->getDisplayId();
345
	}
346
347
	public function isSharable($path) {
348
		if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::isSharingDisabledForUser() has been deprecated: 9.1.0 Use \OC::$server->getShareManager()->sharingDisabledForUser ( Ignorable by Annotation )

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

348
		if (/** @scrutinizer ignore-deprecated */ \OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {

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...
349
			return false;
350
		}
351
		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...
352
	}
353
354
	public function getPermissions($path) {
355
		$response = $this->propfind($path);
356
		// old federated sharing permissions
357
		if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
358
			$permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
359
		} else if (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) {
360
			// permissions provided by the OCM API
361
			$permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions']);
0 ignored issues
show
Bug introduced by
The call to OCA\Files_Sharing\Extern...issions2ncPermissions() has too few arguments starting with path. ( Ignorable by Annotation )

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

361
			/** @scrutinizer ignore-call */ 
362
   $permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions']);

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...
362
		} else {
363
			// use default permission if remote server doesn't provide the share permissions
364
			$permissions = $this->getDefaultPermissions($path);
365
		}
366
367
		return $permissions;
368
	}
369
370
	public function needsPartFile() {
371
		return false;
372
	}
373
374
	/**
375
	 * translate OCM Permissions to Nextcloud permissions
376
	 *
377
	 * @param string $ocmPermissions json encoded OCM permissions
378
	 * @param string $path path to file
379
	 * @return int
380
	 */
381
	protected function ocmPermissions2ncPermissions($ocmPermissions, $path) {
382
		try {
383
			$ocmPermissions = json_decode($ocmPermissions);
384
			$ncPermissions = 0;
385
			foreach($ocmPermissions as $permission) {
386
				switch (strtolower($permission)) {
387
					case 'read':
388
						$ncPermissions += Constants::PERMISSION_READ;
389
						break;
390
					case 'write':
391
						$ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
392
						break;
393
					case 'share':
394
						$ncPermissions += Constants::PERMISSION_SHARE;
395
						break;
396
					default:
397
						throw new \Exception();
398
				}
399
			}
400
		} catch (\Exception $e) {
401
			$ncPermissions = $this->getDefaultPermissions($path);
402
		}
403
404
		return $ncPermissions;
405
	}
406
407
	/**
408
	 * calculate default permissions in case no permissions are provided
409
	 *
410
	 * @param $path
411
	 * @return int
412
	 */
413
	protected function getDefaultPermissions($path) {
414
		if ($this->is_dir($path)) {
415
			$permissions = Constants::PERMISSION_ALL;
416
		} else {
417
			$permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
418
		}
419
420
		return $permissions;
421
	}
422
}
423