Completed
Push — master ( dfe6d6...a56ec1 )
by Morris
28:57
created

Storage::getRemoteUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
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\Federation\ICloudId;
40
use OCP\Files\NotFoundException;
41
use OCP\Files\StorageInvalidException;
42
use OCP\Files\StorageNotAvailableException;
43
44
class Storage extends DAV implements ISharedStorage {
45
	/** @var ICloudId */
46
	private $cloudId;
47
	/** @var string */
48
	private $mountPoint;
49
	/** @var string */
50
	private $token;
51
	/** @var \OCP\ICacheFactory */
52
	private $memcacheFactory;
53
	/** @var \OCP\Http\Client\IClientService */
54
	private $httpClient;
55
	/** @var bool */
56
	private $updateChecked = false;
57
58
	/**
59
	 * @var \OCA\Files_Sharing\External\Manager
60
	 */
61
	private $manager;
62
63
	public function __construct($options) {
64
		$this->memcacheFactory = \OC::$server->getMemCacheFactory();
65
		$this->httpClient = $options['HttpClientService'];
66
67
		$this->manager = $options['manager'];
68
		$this->cloudId = $options['cloudId'];
69
		$discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
70
71
		list($protocol, $remote) = explode('://', $this->cloudId->getRemote());
72
		if (strpos($remote, '/')) {
73
			list($host, $root) = explode('/', $remote, 2);
74
		} else {
75
			$host = $remote;
76
			$root = '';
77
		}
78
		$secure = $protocol === 'https';
79
		$federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
80
		$webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
81
		$root = rtrim($root, '/') . $webDavEndpoint;
82
		$this->mountPoint = $options['mountpoint'];
83
		$this->token = $options['token'];
84
85
		parent::__construct(array(
86
			'secure' => $secure,
87
			'host' => $host,
88
			'root' => $root,
89
			'user' => $options['token'],
90
			'password' => (string)$options['password']
91
		));
92
	}
93
94
	public function getWatcher($path = '', $storage = null) {
95
		if (!$storage) {
96
			$storage = $this;
97
		}
98
		if (!isset($this->watcher)) {
99
			$this->watcher = new Watcher($storage);
100
			$this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
101
		}
102
		return $this->watcher;
103
	}
104
105
	public function getRemoteUser() {
106
		return $this->cloudId->getUser();
107
	}
108
109
	public function getRemote() {
110
		return $this->cloudId->getRemote();
111
	}
112
113
	public function getMountPoint() {
114
		return $this->mountPoint;
115
	}
116
117
	public function getToken() {
118
		return $this->token;
119
	}
120
121
	public function getPassword() {
122
		return $this->password;
123
	}
124
125
	/**
126
	 * @brief get id of the mount point
127
	 * @return string
128
	 */
129
	public function getId() {
130
		return 'shared::' . md5($this->token . '@' . $this->getRemote());
131
	}
132
133
	public function getCache($path = '', $storage = null) {
134
		if (is_null($this->cache)) {
135
			$this->cache = new Cache($this, $this->cloudId);
136
		}
137
		return $this->cache;
138
	}
139
140
	/**
141
	 * @param string $path
142
	 * @param \OC\Files\Storage\Storage $storage
143
	 * @return \OCA\Files_Sharing\External\Scanner
144
	 */
145 View Code Duplication
	public function getScanner($path = '', $storage = null) {
146
		if (!$storage) {
147
			$storage = $this;
148
		}
149
		if (!isset($this->scanner)) {
150
			$this->scanner = new Scanner($storage);
151
		}
152
		return $this->scanner;
153
	}
154
155
	/**
156
	 * check if a file or folder has been updated since $time
157
	 *
158
	 * @param string $path
159
	 * @param int $time
160
	 * @throws \OCP\Files\StorageNotAvailableException
161
	 * @throws \OCP\Files\StorageInvalidException
162
	 * @return bool
163
	 */
164
	public function hasUpdated($path, $time) {
165
		// since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
166
		// because of that we only do one check for the entire storage per request
167
		if ($this->updateChecked) {
168
			return false;
169
		}
170
		$this->updateChecked = true;
171
		try {
172
			return parent::hasUpdated('', $time);
173
		} catch (StorageInvalidException $e) {
174
			// check if it needs to be removed
175
			$this->checkStorageAvailability();
176
			throw $e;
177
		} catch (StorageNotAvailableException $e) {
178
			// check if it needs to be removed or just temp unavailable
179
			$this->checkStorageAvailability();
180
			throw $e;
181
		}
182
	}
183
184
	public function test() {
185
		try {
186
			return parent::test();
187
		} catch (StorageInvalidException $e) {
188
			// check if it needs to be removed
189
			$this->checkStorageAvailability();
190
			throw $e;
191
		} catch (StorageNotAvailableException $e) {
192
			// check if it needs to be removed or just temp unavailable
193
			$this->checkStorageAvailability();
194
			throw $e;
195
		}
196
	}
197
198
	/**
199
	 * Check whether this storage is permanently or temporarily
200
	 * unavailable
201
	 *
202
	 * @throws \OCP\Files\StorageNotAvailableException
203
	 * @throws \OCP\Files\StorageInvalidException
204
	 */
205
	public function checkStorageAvailability() {
206
		// see if we can find out why the share is unavailable
207
		try {
208
			$this->getShareInfo();
209
		} catch (NotFoundException $e) {
210
			// a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
211
			if ($this->testRemote()) {
212
				// valid Nextcloud instance means that the public share no longer exists
213
				// since this is permanent (re-sharing the file will create a new token)
214
				// we remove the invalid storage
215
				$this->manager->removeShare($this->mountPoint);
216
				$this->manager->getMountManager()->removeMount($this->mountPoint);
217
				throw new StorageInvalidException();
218
			} else {
219
				// Nextcloud instance is gone, likely to be a temporary server configuration error
220
				throw new StorageNotAvailableException();
221
			}
222
		} catch (ForbiddenException $e) {
223
			// auth error, remove share for now (provide a dialog in the future)
224
			$this->manager->removeShare($this->mountPoint);
225
			$this->manager->getMountManager()->removeMount($this->mountPoint);
226
			throw new StorageInvalidException();
227
		} catch (\GuzzleHttp\Exception\ConnectException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\ConnectException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
228
			throw new StorageNotAvailableException();
229
		} catch (\GuzzleHttp\Exception\RequestException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\RequestException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
230
			throw new StorageNotAvailableException();
231
		} catch (\Exception $e) {
232
			throw $e;
233
		}
234
	}
235
236
	public function file_exists($path) {
237
		if ($path === '') {
238
			return true;
239
		} else {
240
			return parent::file_exists($path);
241
		}
242
	}
243
244
	/**
245
	 * check if the configured remote is a valid federated share provider
246
	 *
247
	 * @return bool
248
	 */
249
	protected function testRemote() {
250
		try {
251
			return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
252
				|| $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
253
				|| $this->testRemoteUrl($this->getRemote() . '/status.php');
254
		} catch (\Exception $e) {
255
			return false;
256
		}
257
	}
258
259
	/**
260
	 * @param string $url
261
	 * @return bool
262
	 */
263
	private function testRemoteUrl($url) {
264
		$cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
265
		if($cache->hasKey($url)) {
266
			return (bool)$cache->get($url);
267
		}
268
269
		$client = $this->httpClient->newClient();
270
		try {
271
			$result = $client->get($url, [
272
				'timeout' => 10,
273
				'connect_timeout' => 10,
274
			])->getBody();
275
			$data = json_decode($result);
276
			$returnValue = (is_object($data) && !empty($data->version));
277
		} catch (ConnectException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\ConnectException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
278
			$returnValue = false;
279
		} catch (ClientException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\ClientException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
280
			$returnValue = false;
281
		}
282
283
		$cache->set($url, $returnValue, 60*60*24);
284
		return $returnValue;
285
	}
286
287
	/**
288
	 * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
289
	 * standardized. Let's use this to detect whether to use it.
290
	 *
291
	 * @return bool
292
	 */
293
	public function remoteIsOwnCloud() {
294
		if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
295
			return false;
296
		}
297
		return true;
298
	}
299
300
	/**
301
	 * @return mixed
302
	 * @throws ForbiddenException
303
	 * @throws NotFoundException
304
	 * @throws \Exception
305
	 */
306
	public function getShareInfo() {
307
		$remote = $this->getRemote();
308
		$token = $this->getToken();
309
		$password = $this->getPassword();
310
311
		// If remote is not an ownCloud do not try to get any share info
312
		if(!$this->remoteIsOwnCloud()) {
313
			return ['status' => 'unsupported'];
314
		}
315
316
		$url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
317
318
		// TODO: DI
319
		$client = \OC::$server->getHTTPClientService()->newClient();
320
		try {
321
			$response = $client->post($url, [
322
				'body' => ['password' => $password],
323
				'timeout' => 10,
324
				'connect_timeout' => 10,
325
			]);
326
		} catch (\GuzzleHttp\Exception\RequestException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\RequestException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
327
			if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
328
				throw new ForbiddenException();
329
			}
330
			if ($e->getCode() === Http::STATUS_NOT_FOUND) {
331
				throw new NotFoundException();
332
			}
333
			// throw this to be on the safe side: the share will still be visible
334
			// in the UI in case the failure is intermittent, and the user will
335
			// be able to decide whether to remove it if it's really gone
336
			throw new StorageNotAvailableException();
337
		}
338
339
		return json_decode($response->getBody(), true);
340
	}
341
342
	public function getOwner($path) {
343
		return $this->cloudId->getDisplayId();
344
	}
345
346
	public function isSharable($path) {
347
		if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
348
			return false;
349
		}
350
		return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE);
351
	}
352
353
	public function getPermissions($path) {
354
		$response = $this->propfind($path);
355
		if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
356
			$permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
357
		} else {
358
			// use default permission if remote server doesn't provide the share permissions
359
			if ($this->is_dir($path)) {
360
				$permissions = \OCP\Constants::PERMISSION_ALL;
361
			} else {
362
				$permissions = \OCP\Constants::PERMISSION_ALL & ~\OCP\Constants::PERMISSION_CREATE;
363
			}
364
		}
365
366
		return $permissions;
367
	}
368
369
	public function needsPartFile() {
370
		return false;
371
	}
372
}
373