Completed
Pull Request — master (#551)
by Maxence
84:52
created

CirclesRemote::requestInstance()   F

Complexity

Conditions 17
Paths 2360

Size

Total Lines 157

Duplication

Lines 42
Ratio 26.75 %

Importance

Changes 0
Metric Value
dl 42
loc 157
rs 0.8399
c 0
b 0
f 0
cc 17
nc 2360
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
6
/**
7
 * Circles - Bring cloud-users closer together.
8
 *
9
 * This file is licensed under the Affero General Public License version 3 or
10
 * later. See the COPYING file.
11
 *
12
 * @author Maxence Lange <[email protected]>
13
 * @copyright 2021
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
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
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
31
32
namespace OCA\Circles\Command;
33
34
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
35
use daita\MySmallPhpTools\Exceptions\SignatoryException;
36
use daita\MySmallPhpTools\Exceptions\SignatureException;
37
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Request;
38
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21SignedRequest;
39
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21WellKnown;
40
use daita\MySmallPhpTools\Traits\TStringTools;
41
use Exception;
42
use OC\Core\Command\Base;
43
use OCA\Circles\AppInfo\Application;
44
use OCA\Circles\Db\RemoteRequest;
45
use OCA\Circles\Exceptions\RemoteNotFoundException;
46
use OCA\Circles\Exceptions\RemoteUidException;
47
use OCA\Circles\Model\Federated\RemoteInstance;
48
use OCA\Circles\Service\ConfigService;
49
use OCA\Circles\Service\GlobalScaleService;
50
use OCA\Circles\Service\RemoteStreamService;
51
use Symfony\Component\Console\Helper\Table;
52
use Symfony\Component\Console\Input\InputArgument;
53
use Symfony\Component\Console\Input\InputInterface;
54
use Symfony\Component\Console\Input\InputOption;
55
use Symfony\Component\Console\Output\ConsoleOutput;
56
use Symfony\Component\Console\Output\OutputInterface;
57
use Symfony\Component\Console\Question\ConfirmationQuestion;
58
59
60
/**
61
 * Class CirclesRemote
62
 *
63
 * @package OCA\Circles\Command
64
 */
65
class CirclesRemote extends Base {
66
67
68
	use TNC21WellKnown;
69
	use TStringTools;
70
71
72
	/** @var RemoteRequest */
73
	private $remoteRequest;
74
75
	/** @var GlobalScaleService */
76
	private $globalScaleService;
77
78
	/** @var RemoteStreamService */
79
	private $remoteStreamService;
80
81
	/** @var ConfigService */
82
	private $configService;
83
84
85
	/** @var InputInterface */
86
	private $input;
87
88
	/** @var OutputInterface */
89
	private $output;
90
91
92
	/**
93
	 * CirclesRemote constructor.
94
	 *
95
	 * @param RemoteRequest $remoteRequest
96
	 * @param GlobalScaleService $globalScaleService
97
	 * @param RemoteStreamService $remoteStreamService
98
	 * @param ConfigService $configService
99
	 */
100
	public function __construct(
101
		RemoteRequest $remoteRequest, GlobalScaleService $globalScaleService,
102
		RemoteStreamService $remoteStreamService,
103
		ConfigService $configService
104
	) {
105
		parent::__construct();
106
107
		$this->remoteRequest = $remoteRequest;
108
		$this->globalScaleService = $globalScaleService;
109
		$this->remoteStreamService = $remoteStreamService;
110
		$this->configService = $configService;
111
112
		$this->setup('app', 'circles');
113
	}
114
115
116
	/**
117
	 *
118
	 */
119
	protected function configure() {
120
		parent::configure();
121
		$this->setName('circles:remote')
122
			 ->setDescription('remote features')
123
			 ->addArgument('host', InputArgument::OPTIONAL, 'host of the remote instance of Nextcloud')
124
			 ->addOption(
125
				 'type', '', InputOption::VALUE_REQUIRED, 'set type of remote', RemoteInstance::TYPE_UNKNOWN
126
			 )
127
			 ->addOption('all', '', InputOption::VALUE_NONE, 'display all information');
128
	}
129
130
131
	/**
132
	 * @param InputInterface $input
133
	 * @param OutputInterface $output
134
	 *
135
	 * @return int
136
	 * @throws Exception
137
	 */
138
	protected function execute(InputInterface $input, OutputInterface $output): int {
139
		$host = $input->getArgument('host');
140
141
		$this->input = $input;
142
		$this->output = $output;
143
144
		if ($host) {
145
			$this->requestInstance($host);
146
		} else {
147
			$this->checkKnownInstance();
148
		}
149
150
		return 0;
151
	}
152
153
154
	/**
155
	 * @param string $host
156
	 *
157
	 * @throws Exception
158
	 */
159
	private function requestInstance(string $host): void {
160
		$remoteType = $this->getRemoteType();
161
162
		$webfinger = $this->getWebfinger($host, Application::APP_SUBJECT);
163 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
164
			$this->output->writeln('- Webfinger on <info>' . $host . '</info>');
165
			$this->output->writeln(json_encode($webfinger, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
166
			$this->output->writeln('');
167
		}
168
169 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
170
			$circleLink = $this->extractLink(Application::APP_REL, $webfinger);
171
			$this->output->writeln('- Information about Circles app on <info>' . $host . '</info>');
172
			$this->output->writeln(json_encode($circleLink, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
173
			$this->output->writeln('');
174
		}
175
176
		$this->output->writeln('- Available services on <info>' . $host . '</info>');
177
		foreach ($webfinger->getLinks() as $link) {
178
			$app = $link->getProperty('name');
179
			$ver = $link->getProperty('version');
180
			if ($app !== '') {
181
				$app .= ' ';
182
			}
183
			if ($ver !== '') {
184
				$ver = 'v' . $ver;
185
			}
186
187
			$this->output->writeln(' * ' . $link->getRel() . ' ' . $app . $ver);
188
		}
189
		$this->output->writeln('');
190
191
		$this->output->writeln('- Resources related to Circles on <info>' . $host . '</info>');
192
		$resource = $this->getResourceData($host, Application::APP_SUBJECT, Application::APP_REL);
193
		$this->output->writeln(json_encode($resource, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
194
		$this->output->writeln('');
195
196
197
		$tempUid = $resource->g('uid');
198
		$this->output->writeln(
199
			'- Confirming UID=' . $tempUid . ' from parsed Signatory at <info>' . $host . '</info>'
200
		);
201
202
		try {
203
			$remoteSignatory = $this->remoteStreamService->retrieveSignatory($resource->g('id'), true);
204
			$this->output->writeln(' * No SignatureException: <info>Identity authed</info>');
205
		} catch (SignatureException $e) {
206
			$this->output->writeln(
207
				'<error>' . $host . ' cannot auth its identity: ' . $e->getMessage() . '</error>'
208
			);
209
210
			return;
211
		}
212
213
		$this->output->writeln(' * Found <info>' . $remoteSignatory->getUid() . '</info>');
214
		if ($remoteSignatory->getUid(true) !== $tempUid) {
215
			$this->output->writeln('<error>looks like ' . $host . ' is faking its identity');
216
217
			return;
218
		}
219
220
		$this->output->writeln('');
221
222
		$testUrl = $resource->g('test');
223
		$this->output->writeln('- Testing signed payload on <info>' . $testUrl . '</info>');
224
225
		try {
226
			$localSignatory = $this->remoteStreamService->getAppSignatory();
227
		} catch (SignatoryException $e) {
228
			$this->output->writeln(
229
				'<error>Federated Circles not enabled locally. Please run ./occ circles:remote:init</error>'
230
			);
231
232
			return;
233
		}
234
235
		$payload = [
236
			'test'  => 42,
237
			'token' => $this->uuid()
238
		];
239
		$signedRequest = $this->outgoingTest($testUrl, $payload);
240
		$this->output->writeln(' * Payload: ');
241
		$this->output->writeln(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
242
		$this->output->writeln('');
243
244
		$this->output->writeln(' * Clear Signature: ');
245
		$this->output->writeln('<comment>' . $signedRequest->getClearSignature() . '</comment>');
246
		$this->output->writeln('');
247
248
		$this->output->writeln(' * Signed Signature (base64 encoded): ');
249
		$this->output->writeln(
250
			'<comment>' . base64_encode($signedRequest->getSignedSignature()) . '</comment>'
251
		);
252
		$this->output->writeln('');
253
254
		$result = $signedRequest->getOutgoingRequest()->getResult();
255
		$code = $result->getStatusCode();
256
		$this->output->writeln(' * Result: ' . (($code === 200) ? '<info>' . $code . '</info>' : $code));
257
		$this->output->writeln(
258
			json_encode(json_decode($result->getContent(), true), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
259
		);
260
		$this->output->writeln('');
261
262 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
			$this->output->writeln('');
264
			$this->output->writeln('<info>### Complete report ###</info>');
265
			$this->output->writeln(json_encode($signedRequest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
266
			$this->output->writeln('');
267
		}
268
269
		if ($remoteSignatory->getUid() !== $localSignatory->getUid()) {
270
			$remoteSignatory->setInstance($host);
271
			$remoteSignatory->setType($remoteType);
272
273
			try {
274
				$stored = new RemoteInstance();
275
				$this->remoteStreamService->confirmValidRemote($remoteSignatory, $stored);
276
				$this->output->writeln(
277
					'<info>The remote instance ' . $host
278
					. ' is already known with this current identity</info>'
279
				);
280
281 View Code Duplication
				if ($remoteSignatory->getType() !== $stored->getType()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
282
					$this->output->writeln(
283
						'- updating type from ' . $stored->getType() . ' to '
284
						. $remoteSignatory->getType()
285
					);
286
					$this->remoteStreamService->update(
287
						$remoteSignatory, RemoteStreamService::UPDATE_TYPE
288
					);
289
				}
290
291 View Code Duplication
				if ($remoteSignatory->getInstance() !== $stored->getInstance()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
					$this->output->writeln(
293
						'- updating host from ' . $stored->getInstance() . ' to '
294
						. $remoteSignatory->getInstance()
295
					);
296
					$this->remoteStreamService->update(
297
						$remoteSignatory, RemoteStreamService::UPDATE_INSTANCE
298
					);
299
				}
300 View Code Duplication
				if ($remoteSignatory->getId() !== $stored->getId()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
					$this->output->writeln(
302
						'- updating href/Id from ' . $stored->getId() . ' to '
303
						. $remoteSignatory->getId()
304
					);
305
					$this->remoteStreamService->update($remoteSignatory, RemoteStreamService::UPDATE_HREF);
306
				}
307
308
			} catch (RemoteUidException $e) {
309
				$this->updateRemote($remoteSignatory);
310
			} catch (RemoteNotFoundException $e) {
311
				$this->saveRemote($remoteSignatory);
312
			}
313
		}
314
315
	}
316
317
318
	/**
319
	 * @param RemoteInstance $remoteSignatory
320
	 *
321
	 * @throws RemoteUidException
322
	 */
323 View Code Duplication
	private function saveRemote(RemoteInstance $remoteSignatory) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
		$this->output->writeln('');
325
		$helper = $this->getHelper('question');
326
327
		$this->output->writeln(
328
			'The remote instance <info>' . $remoteSignatory->getInstance() . '</info> looks good.'
329
		);
330
		$question = new ConfirmationQuestion(
331
			'Would you like to identify this remote instance as \'' . $remoteSignatory->getType()
332
			. '\' ? (y/N) ',
333
			false,
334
			'/^(y|Y)/i'
335
		);
336
337
		if ($helper->ask($this->input, $this->output, $question)) {
338
			$this->remoteRequest->save($remoteSignatory);
339
			$this->output->writeln('<info>remote instance saved</info>');
340
		}
341
	}
342
343
344
	/**
345
	 * @param RemoteInstance $remoteSignatory
346
	 *
347
	 * @throws RemoteUidException
348
	 */
349 View Code Duplication
	private function updateRemote(RemoteInstance $remoteSignatory): void {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350
		$this->output->writeln('');
351
		$helper = $this->getHelper('question');
352
353
		$this->output->writeln(
354
			'The remote instance <info>' . $remoteSignatory->getInstance()
355
			. '</info> is known but <error>its identity has changed.</error>'
356
		);
357
		$this->output->writeln(
358
			'<comment>If you are not sure on why identity changed, please say No to the next question and contact the admin of the remote instance</comment>'
359
		);
360
		$question = new ConfirmationQuestion(
361
			'Do you consider this new identity as valid and update the entry in the database? (y/N) ',
362
			false,
363
			'/^(y|Y)/i'
364
		);
365
366
		if ($helper->ask($this->input, $this->output, $question)) {
367
			$this->remoteStreamService->update($remoteSignatory);
368
			$this->output->writeln('remote instance updated');
369
		}
370
	}
371
372
373
	/**
374
	 * @param string $remote
375
	 * @param array $payload
376
	 *
377
	 * @return NC21SignedRequest
378
	 * @throws RequestNetworkException
379
	 * @throws SignatoryException
380
	 */
381
	private function outgoingTest(string $remote, array $payload): NC21SignedRequest {
382
		$request = new NC21Request();
383
		$request->basedOnUrl($remote);
384
		$request->setFollowLocation(true);
385
		$request->setLocalAddressAllowed(true);
386
		$request->setTimeout(5);
387
		$request->setData($payload);
388
389
		$app = $this->remoteStreamService->getAppSignatory();
390
		$signedRequest = $this->remoteStreamService->signOutgoingRequest($request, $app);
391
		$this->doRequest($signedRequest->getOutgoingRequest());
392
393
		return $signedRequest;
394
	}
395
396
397
	/**
398
	 *
399
	 */
400
	private function checkKnownInstance(): void {
401
		$this->verifyGSInstances();
402
		$this->checkRemoteInstances();
403
	}
404
405
406
	/**
407
	 *
408
	 */
409
	private function verifyGSInstances(): void {
410
		$instances = $this->globalScaleService->getGlobalScaleInstances();
411
		$known = array_map(
412
			function(RemoteInstance $instance): string {
413
				return $instance->getInstance();
414
			}, $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBAL_SCALE)
415
		);
416
417
		$missing = array_diff($instances, $known);
418
		foreach ($missing as $instance) {
419
			$this->syncGSInstance($instance);
420
		}
421
	}
422
423
424
	/**
425
	 * @param string $instance
426
	 */
427
	private function syncGSInstance(string $instance): void {
428
		if ($this->configService->isLocalInstance($instance)) {
429
			return;
430
		}
431
		$this->output->write('Adding <comment>' . $instance . '</comment>: ');
432
		try {
433
			$this->remoteStreamService->addRemoteInstance($instance, RemoteInstance::TYPE_GLOBAL_SCALE, true);
434
			$this->output->writeln('<info>ok</info>');
435
		} catch (Exception $e) {
436
			$msg = ($e->getMessage() === '') ? '' : ' (' . $e->getMessage() . ')';
437
			$this->output->writeln('<error>' . get_class($e) . $msg . '</error>');
438
		}
439
	}
440
441
442
	private function checkRemoteInstances(): void {
443
		$instances = $this->remoteRequest->getAllInstances();
444
445
		$output = new ConsoleOutput();
446
		$output = $output->section();
447
		$table = new Table($output);
448
		$table->setHeaders(['instance', 'type', 'UID', 'Authed']);
449
		$table->render();
450
451
		foreach ($instances as $instance) {
452
			try {
453
				$current = $this->remoteStreamService->retrieveRemoteInstance($instance->getInstance());
454
				if ($current->getUid(true) === $instance->getUid(true)) {
455
					$currentUid = '<info>' . $current->getUid(true) . '</info>';
456
				} else {
457
					$currentUid = '<error>' . $current->getUid(true) . '</error>';
458
				}
459
			} catch (Exception $e) {
460
				$currentUid = '<error>' . $e->getMessage() . '</error>';
461
			}
462
463
			$table->appendRow(
464
				[
465
					$instance->getInstance(),
466
					$instance->getType(),
467
					$instance->getUid(),
468
					$currentUid
469
				]
470
			);
471
		}
472
	}
473
474
475
	/**
476
	 * @throws Exception
477
	 */
478
	private function getRemoteType(): string {
479
		$type = ucfirst(strtolower($this->input->getOption('type')));
480
481
		if (!in_array($type, RemoteInstance::$LIST_TYPE)) {
482
			throw new Exception('Unknown type: ' . implode(', ', RemoteInstance::$LIST_TYPE));
483
		}
484
485
		return $type;
486
	}
487
488
}
489
490