Passed
Push — master ( 17cdcf...aa80aa )
by John
09:42 queued 11s
created

VerifyUserData::run()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 8
nop 1
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Bjoern Schiessle <[email protected]>
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Patrik Kernstock <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
 *
24
 */
25
26
27
namespace OCA\Settings\BackgroundJobs;
28
29
30
use OC\Accounts\AccountManager;
31
use OC\BackgroundJob\Job;
32
use OC\BackgroundJob\JobList;
33
use OCP\AppFramework\Http;
34
use OCP\BackgroundJob\IJobList;
35
use OCP\Http\Client\IClientService;
36
use OCP\IConfig;
37
use OCP\ILogger;
38
use OCP\IUserManager;
39
40
class VerifyUserData extends Job {
41
42
	/** @var  bool */
43
	private $retainJob = true;
44
45
	/** @var int max number of attempts to send the request */
46
	private $maxTry = 24;
47
48
	/** @var int how much time should be between two tries (1 hour) */
49
	private $interval = 3600;
50
51
	/** @var AccountManager */
52
	private $accountManager;
53
54
	/** @var IUserManager */
55
	private $userManager;
56
57
	/** @var IClientService */
58
	private $httpClientService;
59
60
	/** @var ILogger */
61
	private $logger;
62
63
	/** @var string */
64
	private $lookupServerUrl;
65
66
	/** @var IConfig */
67
	private $config;
68
69
	/**
70
	 * VerifyUserData constructor.
71
	 *
72
	 * @param AccountManager $accountManager
73
	 * @param IUserManager $userManager
74
	 * @param IClientService $clientService
75
	 * @param ILogger $logger
76
	 * @param IConfig $config
77
	 */
78
	public function __construct(AccountManager $accountManager,
79
								IUserManager $userManager,
80
								IClientService $clientService,
81
								ILogger $logger,
82
								IConfig $config
83
	) {
84
		$this->accountManager = $accountManager;
85
		$this->userManager = $userManager;
86
		$this->httpClientService = $clientService;
87
		$this->logger = $logger;
88
89
		$lookupServerUrl = $config->getSystemValue('lookup_server', 'https://lookup.nextcloud.com');
90
		$this->lookupServerUrl = rtrim($lookupServerUrl, '/');
91
		$this->config = $config;
92
	}
93
94
	/**
95
	 * run the job, then remove it from the jobList
96
	 *
97
	 * @param JobList $jobList
98
	 * @param ILogger|null $logger
99
	 */
100
	public function execute($jobList, ILogger $logger = null) {
101
102
		if ($this->shouldRun($this->argument)) {
103
			parent::execute($jobList, $logger);
104
			$jobList->remove($this, $this->argument);
105
			if ($this->retainJob) {
106
				$this->reAddJob($jobList, $this->argument);
107
			} else {
108
				$this->resetVerificationState();
109
			}
110
		}
111
112
	}
113
114
	protected function run($argument) {
115
116
		$try = (int)$argument['try'] + 1;
117
118
		switch($argument['type']) {
119
			case AccountManager::PROPERTY_WEBSITE:
120
				$result = $this->verifyWebsite($argument);
121
				break;
122
			case AccountManager::PROPERTY_TWITTER:
123
			case AccountManager::PROPERTY_EMAIL:
124
				$result = $this->verifyViaLookupServer($argument, $argument['type']);
125
				break;
126
			default:
127
				// no valid type given, no need to retry
128
				$this->logger->error($argument['type'] . ' is no valid type for user account data.');
129
				$result = true;
130
		}
131
132
		if ($result === true || $try > $this->maxTry) {
133
			$this->retainJob = false;
134
		}
135
	}
136
137
	/**
138
	 * verify web page
139
	 *
140
	 * @param array $argument
141
	 * @return bool true if we could check the verification code, otherwise false
142
	 */
143
	protected function verifyWebsite(array $argument) {
144
145
		$result = false;
146
147
		$url = rtrim($argument['data'], '/') . '/.well-known/' . 'CloudIdVerificationCode.txt';
148
149
		$client = $this->httpClientService->newClient();
150
		try {
151
			$response = $client->get($url);
152
		} catch (\Exception $e) {
153
			return false;
154
		}
155
156
		if ($response->getStatusCode() === Http::STATUS_OK) {
157
			$result = true;
158
			$publishedCode = $response->getBody();
159
			// remove new lines and spaces
160
			$publishedCodeSanitized = trim(preg_replace('/\s\s+/', ' ', $publishedCode));
161
			$user = $this->userManager->get($argument['uid']);
162
			// we don't check a valid user -> give up
163
			if ($user === null) {
164
				$this->logger->error($argument['uid'] . ' doesn\'t exist, can\'t verify user data.');
165
				return $result;
166
			}
167
			$userData = $this->accountManager->getUser($user);
168
169
			if ($publishedCodeSanitized === $argument['verificationCode']) {
170
				$userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFIED;
171
			} else {
172
				$userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::NOT_VERIFIED;
173
			}
174
175
			$this->accountManager->updateUser($user, $userData);
176
		}
177
178
		return $result;
179
	}
180
181
	/**
182
	 * verify email address
183
	 *
184
	 * @param array $argument
185
	 * @param string $dataType
186
	 * @return bool true if we could check the verification code, otherwise false
187
	 */
188
	protected function verifyViaLookupServer(array $argument, $dataType) {
189
		if(empty($this->lookupServerUrl) ||
190
			$this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes') !== 'yes' ||
191
			$this->config->getSystemValue('has_internet_connection', true) === false) {
192
			return false;
193
		}
194
195
		$user = $this->userManager->get($argument['uid']);
196
197
		// we don't check a valid user -> give up
198
		if ($user === null) {
199
			$this->logger->info($argument['uid'] . ' doesn\'t exist, can\'t verify user data.');
200
			return true;
201
		}
202
203
		$localUserData = $this->accountManager->getUser($user);
204
		$cloudId = $user->getCloudId();
205
206
		// ask lookup-server for user data
207
		$lookupServerData = $this->queryLookupServer($cloudId);
208
209
		// for some reasons we couldn't read any data from the lookup server, try again later
210
		if (empty($lookupServerData) || empty($lookupServerData[$dataType])) {
211
			return false;
212
		}
213
214
		// lookup server has verification data for wrong user data (e.g. email address), try again later
215
		if ($lookupServerData[$dataType]['value'] !== $argument['data']) {
216
			return false;
217
		}
218
219
		// lookup server hasn't verified the email address so far, try again later
220
		if ($lookupServerData[$dataType]['verified'] === AccountManager::NOT_VERIFIED) {
221
			return false;
222
		}
223
224
		$localUserData[$dataType]['verified'] = AccountManager::VERIFIED;
225
		$this->accountManager->updateUser($user, $localUserData);
226
227
		return true;
228
	}
229
230
	/**
231
	 * @param string $cloudId
232
	 * @return array
233
	 */
234
	protected function queryLookupServer($cloudId) {
235
		try {
236
			$client = $this->httpClientService->newClient();
237
			$response = $client->get(
238
				$this->lookupServerUrl . '/users?search=' . urlencode($cloudId) . '&exactCloudId=1',
239
				[
240
					'timeout' => 10,
241
					'connect_timeout' => 3,
242
				]
243
			);
244
245
			$body = json_decode($response->getBody(), true);
246
247
			if (is_array($body) && isset($body['federationId']) && $body['federationId'] === $cloudId) {
248
				return $body;
249
			}
250
251
		} catch (\Exception $e) {
252
			// do nothing, we will just re-try later
253
		}
254
255
		return [];
256
	}
257
258
	/**
259
	 * re-add background job with new arguments
260
	 *
261
	 * @param IJobList $jobList
262
	 * @param array $argument
263
	 */
264
	protected function reAddJob(IJobList $jobList, array $argument) {
265
		$jobList->add(VerifyUserData::class,
266
			[
267
				'verificationCode' => $argument['verificationCode'],
268
				'data' => $argument['data'],
269
				'type' => $argument['type'],
270
				'uid' => $argument['uid'],
271
				'try' => (int)$argument['try'] + 1,
272
				'lastRun' => time()
273
			]
274
		);
275
	}
276
277
	/**
278
	 * test if it is time for the next run
279
	 *
280
	 * @param array $argument
281
	 * @return bool
282
	 */
283
	protected function shouldRun(array $argument) {
284
		$lastRun = (int)$argument['lastRun'];
285
		return ((time() - $lastRun) > $this->interval);
286
	}
287
288
289
	/**
290
	 * reset verification state after max tries are reached
291
	 */
292
	protected function resetVerificationState() {
293
		$user = $this->userManager->get($this->argument['uid']);
294
		if ($user !== null) {
295
			$accountData = $this->accountManager->getUser($user);
296
			$accountData[$this->argument['type']]['verified'] = AccountManager::NOT_VERIFIED;
297
			$this->accountManager->updateUser($user, $accountData);
298
		}
299
	}
300
301
}
302