%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/thread-self/root/www/varak.net/nextcloud.varak.net/apps/circles/lib/Command/
Upload File :
Create Path :
Current File : //proc/thread-self/root/www/varak.net/nextcloud.varak.net/apps/circles/lib/Command/CirclesCheck.php

<?php

declare(strict_types=1);
/**
 * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\Circles\Command;

use Exception;
use OC\Core\Command\Base;
use OCA\Circles\AppInfo\Application;
use OCA\Circles\AppInfo\Capabilities;
use OCA\Circles\Exceptions\FederatedEventException;
use OCA\Circles\Exceptions\FederatedItemException;
use OCA\Circles\Exceptions\InitiatorNotConfirmedException;
use OCA\Circles\Exceptions\OwnerNotFoundException;
use OCA\Circles\Exceptions\RemoteInstanceException;
use OCA\Circles\Exceptions\RemoteNotFoundException;
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\UnknownInterfaceException;
use OCA\Circles\Exceptions\UnknownRemoteException;
use OCA\Circles\FederatedItems\LoopbackTest;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\FederatedEventService;
use OCA\Circles\Service\InterfaceService;
use OCA\Circles\Service\RemoteService;
use OCA\Circles\Service\RemoteStreamService;
use OCA\Circles\Service\RemoteUpstreamService;
use OCA\Circles\Tools\Exceptions\RequestNetworkException;
use OCA\Circles\Tools\Exceptions\SignatoryException;
use OCA\Circles\Tools\Model\NCRequest;
use OCA\Circles\Tools\Model\Request;
use OCA\Circles\Tools\Model\SimpleDataStore;
use OCA\Circles\Tools\Traits\TArrayTools;
use OCA\Circles\Tools\Traits\TNCRequest;
use OCA\Circles\Tools\Traits\TStringTools;
use OCP\IAppConfig;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

/**
 * Class CirclesCheck
 *
 * @package OCA\Circles\Command
 */
class CirclesCheck extends Base {
	use TStringTools;
	use TArrayTools;
	use TNCRequest;

	public static array $checks = [
		'internal',
		'frontal',
		'loopback'
	];

	private array $sessions = [];

	public function __construct(
		private Capabilities $capabilities,
		private IAppConfig $appConfig,
		private InterfaceService $interfaceService,
		private FederatedEventService $federatedEventService,
		private RemoteService $remoteService,
		private RemoteStreamService $remoteStreamService,
		private RemoteUpstreamService $remoteUpstreamService,
		private ConfigService $configService
	) {
		parent::__construct();
	}

	protected function configure() {
		parent::configure();
		$this->setName('circles:check')
			 ->setDescription('Checking your configuration')
			 ->addOption('capabilities', '', InputOption::VALUE_NONE, 'listing app\'s capabilities')
			 ->addOption('type', '', InputOption::VALUE_REQUIRED, 'configuration to check', '')
			 ->addOption('alpha', '', InputOption::VALUE_NONE, 'allow ALPHA features')
			 ->addOption('test', '', InputOption::VALUE_REQUIRED, 'specify an url to test', '');
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 *
	 * @return int
	 * @throws Exception
	 */
	protected function execute(InputInterface $input, OutputInterface $output): int {
		if ($input->getOption('capabilities')) {
			$capabilities = $this->getArray('circles', $this->capabilities->getCapabilities(true));
			$output->writeln(json_encode($capabilities, JSON_PRETTY_PRINT));

			return 0;
		}

		$this->configService->setAppValue(ConfigService::TEST_NC_BASE, '');
		$test = $input->getOption('test');
		$type = $input->getOption('type');
		if ($test !== '' && $type === '') {
			throw new Exception('Please specify a --type for the test');
		}
		if ($test !== '' && !in_array($type, self::$checks)) {
			throw new Exception('Unknown type: ' . implode(', ', self::$checks));
		}

		//		$this->configService->setAppValue(ConfigService::TEST_NC_BASE, $test);

		if ($type === '' || $type === 'loopback') {
			$output->writeln('### Checking <info>loopback</info> address.');
			$this->checkLoopback($input, $output, $test);
			$output->writeln('');
			$output->writeln('');
		}

		if ($type === '' || $type === 'internal') {
			$output->writeln('### Testing <info>internal</info> address.');
			$this->checkInternal($input, $output, $test);
			$output->writeln('');
			$output->writeln('');
		}

		if (!$input->getOption('alpha')) {
			return 0;
		}

		if ($type === '' || $type === 'frontal') {
			$output->writeln('### Testing <info>frontal</info> address.');
			$this->checkFrontal($input, $output, $test);
			$output->writeln('');
		}

		return 0;
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $test
	 *
	 * @throws Exception
	 */
	private function checkLoopback(InputInterface $input, OutputInterface $output, string $test = ''): void {
		$output->writeln('. The <info>loopback</info> setting is mandatory and can be checked locally.');
		$output->writeln(
			'. The address you need to define here must be a reachable url of your Nextcloud from the hosting server itself.'
		);
		$output->writeln(
			'. By default, the App will use the entry \'overwrite.cli.url\' from \'config/config.php\'.'
		);

		$notDefault = false;
		if ($test === '') {
			$test = $this->configService->getLoopbackPath();
		} else {
			$notDefault = true;
		}

		$output->writeln('');
		$output->writeln('* testing current address: ' . $test);

		try {
			$this->setupLoopback($input, $output, $test);
			$output->writeln('* <info>Loopback</info> address looks good');
			if ($notDefault) {
				$this->saveLoopback($input, $output, $test);
			}

			return;
		} catch (Exception $e) {
		}

		$output->writeln('');
		$output->writeln('- <comment>You do not have a valid loopback address setup right now.</comment>');
		$output->writeln('');

		$helper = $this->getHelper('question');
		while (true) {
			$question = new Question('<info>Please write down a new loopback address to test</info>: ', '');

			$loopback = $helper->ask($input, $output, $question);
			if (is_null($loopback) || $loopback === '') {
				$output->writeln('exiting.');
				throw new Exception('Your Circles App is not fully configured.');
			}

			try {
				[$scheme, $cloudId, $path] = $this->parseAddress($loopback);
			} catch (Exception $e) {
				$output->writeln('<error>format must be http[s]://domain.name[:post][/path]</error>');
				continue;
			}

			$loopback = rtrim($scheme . '://' . $cloudId . $path, '/');
			$output->writeln('* testing address: ' . $loopback . ' ');

			try {
				$this->setupLoopback($input, $output, $loopback);
				$this->saveLoopback($input, $output, $loopback);

				return;
			} catch (Exception $e) {
				$output->writeln('');
			}
		}
	}

	/**
	 * @throws Exception
	 */
	private function setupLoopback(InputInterface $input, OutputInterface $output, string $address): void {
		$e = null;
		try {
			[$scheme, $cloudId, $path] = $this->parseAddress($address);

			$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_SCHEME, $scheme);
			$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_ID, $cloudId);
			$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_PATH, $path);
			if (!$this->testLoopback($input, $output)) {
				throw new Exception();
			}
		} catch (Exception $e) {
		}

		$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_SCHEME, '');
		$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_ID, '');
		$this->configService->setAppValue(ConfigService::LOOPBACK_TMP_PATH, '');

		if (!is_null($e)) {
			throw $e;
		}
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 *
	 * @return bool
	 * @throws FederatedEventException
	 * @throws FederatedItemException
	 * @throws InitiatorNotConfirmedException
	 * @throws OwnerNotFoundException
	 * @throws RemoteInstanceException
	 * @throws RemoteNotFoundException
	 * @throws RemoteResourceNotFoundException
	 * @throws RequestBuilderException
	 * @throws UnknownRemoteException
	 */
	private function testLoopback(InputInterface $input, OutputInterface $output): bool {
		if (!$this->testRequest($output, 'GET', 'core.CSRFToken.index')) {
			return false;
		}

		if (!$this->testRequest(
			$output, 'POST', 'circles.EventWrapper.asyncBroadcast',
			['token' => 'test-dummy-token']
		)) {
			return false;
		}

		$timer = round(microtime(true) * 1000);
		$output->write('- Creating async FederatedEvent ');
		$test = new FederatedEvent(LoopbackTest::class);
		$this->federatedEventService->newEvent($test);
		$output->writeln(
			'<info>' . $test->getWrapperToken() . '</info> ' .
			'(took ' . (round(microtime(true) * 1000) - $timer) . 'ms)'
		);

		$output->writeln('- Waiting for async process to finish (5s)');
		sleep(5);

		$output->write('- Checking status on FederatedEvent ');
		$wrappers = $this->remoteUpstreamService->getEventsByToken($test->getWrapperToken());

		if (count($wrappers) !== 1) {
			$output->writeln('<error>Event created too many Wrappers</error>');

			return false;
		}

		$wrapper = array_shift($wrappers);

		$checkVerify = $wrapper->getEvent()->getData()->gInt('verify');
		if ($checkVerify === LoopbackTest::VERIFY) {
			$output->write('<info>verify=' . $checkVerify . '</info> ');
		} else {
			$output->writeln('<error>verify=' . $checkVerify . '</error>');

			return false;
		}

		$checkManage = $wrapper->getResult()->gInt('manage');
		if ($checkManage === LoopbackTest::MANAGE) {
			$output->write('<info>manage=' . $checkManage . '</info> ');
		} else {
			$output->writeln('<error>manage=' . $checkManage . '</error>');

			return false;
		}

		$output->writeln('');

		return true;
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $loopback
	 *
	 * @throws Exception
	 */
	private function saveLoopback(InputInterface $input, OutputInterface $output, string $loopback): void {
		[$scheme, $cloudId, $path] = $this->parseAddress($loopback);

		$question = new ConfirmationQuestion(
			'- Do you want to save <info>'
			. $loopback
			. '</info> as your <info>loopback</info> address ? (y/N) ',
			false, '/^(y|Y)/i'
		);

		$helper = $this->getHelper('question');
		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('skipping.');

			return;
		}

		$this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_SCHEME, $scheme);
		$this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_ID, $cloudId);
		$this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_PATH, $path);
		$output->writeln(
			'- Address <info>' . $loopback . '</info> is now used as <info>loopback</info>'
		);
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $test
	 *
	 * @throws SignatoryException
	 * @throws UnknownInterfaceException
	 * @throws Exception
	 */
	private function checkInternal(InputInterface $input, OutputInterface $output, string $test): void {
		$output->writeln(
			'. The <info>internal</info> setting should only be enabled if you are willing to use Circles in a GlobalScale setup on a local network.'
		);
		$output->writeln(
			'. The address you need to define here is the local address of your Nextcloud, reachable by all other instances of our GlobalScale.'
		);

		$question = new ConfirmationQuestion(
			'- <comment>Do you want to enable this feature ?</comment> (y/N) ', false, '/^(y|Y)/i'
		);

		$helper = $this->getHelper('question');
		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('skipping.');

			return;
		}

		while (true) {
			$output->writeln('');
			$question = new Question(
				'<info>Please write down a new internal address to test</info>: ', ''
			);

			$internal = $helper->ask($input, $output, $question);
			if (is_null($internal) || $internal === '') {
				$output->writeln('skipping.');

				return;
			}

			try {
				[$scheme, $cloudId, $path] = $this->parseAddress($internal);
			} catch (Exception $e) {
				$output->writeln('<error>format must be http[s]://domain.name[:post][/path]</error>');
				continue;
			}

			$internal = rtrim($scheme . '://' . $cloudId, '/');
			$fullInternal = rtrim($scheme . '://' . $cloudId . $path, '/');

			$question = new ConfirmationQuestion(
				'<comment>Do you want to check the validity of this internal address?</comment> (Y/n) ', true,
				'/^(y|Y)/i'
			);

			if ($helper->ask($input, $output, $question)) {
				$testToken = $this->token();
				$this->configService->setAppValue(ConfigService::IFACE_TEST_ID, $cloudId);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_SCHEME, $scheme);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_PATH, $path);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_TOKEN, $testToken);

				$output->writeln('');
				$output->writeln(
					'You will need to run this <info>curl</info> command from a terminal on your local network and paste its result: '
				);
				$output->writeln(
					'     curl -L "' . $internal
					. '/.well-known/webfinger?resource=http://nextcloud.com/&test='
					. $testToken . '"'
				);

				$output->writeln('paste the result here: ');
				$question = new Question('', '');
				$pastedWebfinger = new SimpleDataStore();
				$pastedWebfinger->json(trim($helper->ask($input, $output, $question)));

				if ($pastedWebfinger->g('subject') !== Application::APP_SUBJECT) {
					$output->writeln('<error>Cannot extract SUBJECT from the pasted data</error>');
					continue;
				}

				$pastedHref = '';
				foreach ($pastedWebfinger->gArray('links') as $link) {
					$entry = new SimpleDataStore($link);
					if ($entry->g('rel') === Application::APP_REL) {
						$pastedHref = $entry->g('href');
					}
				}

				if ($pastedHref === '') {
					$output->writeln('<error>Cannot retrieve HREF from the pasted data</error>');
					continue;
				}

				$href = $this->interfaceService->getCloudPath(
					'circles.Remote.appService',
					[],
					InterfaceService::IFACE_TEST
				);

				if ($pastedHref !== $href) {
					$output->writeln(
						'<error>The returned data (' . $pastedHref . ') are not the one expected: </error>'
						. $href
					);
					continue;
				}

				$output->writeln('');
				$output->writeln('<info>First step seems fine.</info>');
				$output->writeln(
					'Next step, please run this <info>curl</info> command from a terminal on your local network and paste its result: '
				);
				$output->writeln(
					'     curl -L "' . $pastedHref . '?test=' . $testToken . '" -H "Accept: application/json"'
				);

				$output->writeln('paste the result here: ');
				$question = new Question('', '');
				$pastedSignatory = new SimpleDataStore();
				$pastedSignatory->json(trim($helper->ask($input, $output, $question)));

				$this->appConfig->clearCache();
				$this->interfaceService->setCurrentInterface(InterfaceService::IFACE_TEST);
				$appSignatory = $this->remoteStreamService->getAppSignatory(false);

				if ($appSignatory->getUid(true) !== $pastedSignatory->g('uid')
					|| $appSignatory->getRoot() !== $pastedSignatory->g('root')) {
					$output->writeln(
						'<error>The returned data ('
						. $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root')
						. ') are not the one expected: </error>'
						. $appSignatory->getUid(true) . '/' . $appSignatory->getRoot()
					);
					continue;
				}

				$output->writeln('* <info>Internal</info> address looks good');
			}

			$this->saveInternal($input, $output, $fullInternal);

			return;
		}
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $internal
	 *
	 * @throws Exception
	 */
	private function saveInternal(InputInterface $input, OutputInterface $output, string $internal): void {
		[$scheme, $cloudId, $path] = $this->parseAddress($internal);

		$output->writeln('');
		$question = new ConfirmationQuestion(
			'- Do you want to save <info>' . $internal
			. '</info> as your <info>internal</info> address ? (y/N) ',
			false, '/^(y|Y)/i'
		);

		$helper = $this->getHelper('question');
		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('skipping.');

			return;
		}

		$this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_SCHEME, $scheme);
		$this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_ID, $cloudId);
		$this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_PATH, $path);

		$output->writeln('- Address <info>' . $internal . '</info> is now used as <info>internal</info>');
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $test
	 */
	private function checkFrontal(InputInterface $input, OutputInterface $output, string $test): void {
		$output->writeln('. The <info>frontal</info> setting is optional.');
		$output->writeln(
			'. The purpose of this address is for your Federated Circle to reach other instances of Nextcloud over the Internet.'
		);
		$output->writeln(
			'. The address you need to define here must be reachable from the Internet.'
		);
		$output->writeln(
			'. By default, this feature is disabled.'
		);

		$question = new ConfirmationQuestion(
			'- <comment>Do you want to enable this feature ?</comment> (y/N) ', false, '/^(y|Y)/i'
		);

		$helper = $this->getHelper('question');
		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('skipping.');

			return;
		}

		while (true) {
			$question = new Question(
				'<info>Please write down a new frontal address to test</info>: ', ''
			);

			$frontal = $helper->ask($input, $output, $question);
			if (is_null($frontal) || $frontal === '') {
				$output->writeln('skipping.');

				return;
			}

			try {
				[$scheme, $cloudId, $path] = $this->parseAddress($frontal);
			} catch (Exception $e) {
				$output->writeln('<error>format must be http[s]://domain.name[:post][/path]</error>');
				continue;
			}

			$frontal = rtrim($scheme . '://' . $cloudId, '/');
			$fullFrontal = rtrim($scheme . '://' . $cloudId . $path, '/');

			$question = new ConfirmationQuestion(
				'<comment>Do you want to check the validity of this frontal address?</comment> (y/N) ', false,
				'/^(y|Y)/i'
			);

			if ($helper->ask($input, $output, $question)) {
				$testToken = $this->token();
				$this->configService->setAppValue(ConfigService::IFACE_TEST_ID, $cloudId);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_SCHEME, $scheme);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_PATH, $path);
				$this->configService->setAppValue(ConfigService::IFACE_TEST_TOKEN, $testToken);

				$output->writeln('');
				$output->writeln(
					'You will need to run this <info>curl</info> command from a remote terminal and paste its result: '
				);
				$output->writeln(
					'     curl -L "' . $frontal
					. '/.well-known/webfinger?resource=http://nextcloud.com/&test='
					. $testToken . '"'
				);

				$output->writeln('paste the result here: ');
				$question = new Question('', '');
				$pastedWebfinger = new SimpleDataStore();
				$pastedWebfinger->json(trim($helper->ask($input, $output, $question)));

				if ($pastedWebfinger->g('subject') !== Application::APP_SUBJECT) {
					$output->writeln('<error>Cannot extract SUBJECT from the pasted data</error>');
					continue;
				}

				$pastedHref = '';
				foreach ($pastedWebfinger->gArray('links') as $link) {
					$entry = new SimpleDataStore($link);
					if ($entry->g('rel') === Application::APP_REL) {
						$pastedHref = $entry->g('href');
					}
				}

				if ($pastedHref === '') {
					$output->writeln('<error>Cannot retrieve HREF from the pasted data</error>');
					continue;
				}

				$href = $this->interfaceService->getCloudPath(
					'circles.Remote.appService',
					[],
					InterfaceService::IFACE_TEST
				);

				if ($pastedHref !== $href) {
					$output->writeln(
						'<error>The returned data (' . $pastedHref . ') are not the one expected: </error>'
						. $href
					);
					continue;
				}

				$output->writeln('');
				$output->writeln('<info>First step seems fine.</info>');
				$output->writeln(
					'Next step, please run this <info>curl</info> command from a remote terminal and paste its result: '
				);
				$output->writeln(
					'     curl -L "' . $pastedHref . '?test=' . $testToken . '" -H "Accept: application/json"'
				);

				$output->writeln('paste the result here: ');
				$question = new Question('', '');
				$pastedSignatory = new SimpleDataStore();
				$pastedSignatory->json(trim($helper->ask($input, $output, $question)));

				$this->appConfig->clearCache();
				$this->interfaceService->setCurrentInterface(InterfaceService::IFACE_TEST);
				$appSignatory = $this->remoteStreamService->getAppSignatory(false);

				if ($appSignatory->getUid(true) !== $pastedSignatory->g('uid')
					|| $appSignatory->getRoot() !== $pastedSignatory->g('root')) {
					$output->writeln(
						'<error>The returned data ('
						. $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root')
						. ') are not the one expected: </error>'
						. $appSignatory->getUid(true) . '/' . $appSignatory->getRoot()
					);
					continue;
				}

				$output->writeln('* <info>Frontal</info> address looks good');
			}

			$this->saveFrontal($input, $output, $fullFrontal);

			return;
		}
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $frontal
	 *
	 * @throws Exception
	 */
	private function saveFrontal(InputInterface $input, OutputInterface $output, string $frontal): void {
		[$scheme, $cloudId, $path] = $this->parseAddress($frontal);

		$output->writeln('');
		$question = new ConfirmationQuestion(
			'- Do you want to save <info>' . $frontal
			. '</info> as your <info>frontal</info> address ? (y/N) ',
			false, '/^(y|Y)/i'
		);

		$helper = $this->getHelper('question');
		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('skipping.');

			return;
		}

		$this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_SCHEME, $scheme);
		$this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_ID, $cloudId);
		$this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_PATH, $path);

		$output->writeln('- Address <info>' . $frontal . '</info> is now used as <info>frontal</info>');
	}

	/**
	 * @param OutputInterface $o
	 * @param string $type
	 * @param string $route
	 * @param array $args
	 *
	 * @return bool
	 */
	private function testRequest(
		OutputInterface $output,
		string $type,
		string $route,
		array $args = []
	): bool {
		$request = new NCRequest('', Request::type($type));
		$this->configService->configureLoopbackRequest($request, $route, $args);
		$request->setFollowLocation(false);

		if ($request->getType() !== Request::TYPE_GET) {
			$request->setDataSerialize(new SimpleDataStore(['empty' => 1]));
		}

		$output->write('- ' . $type . ' request on ' . $request->getCompleteUrl() . ': ');

		try {
			$this->doRequest($request);
			$result = $request->getResult();

			$color = 'error';
			if ($result->getStatusCode() === 200) {
				$color = 'info';
			}

			$output->writeln('<' . $color . '>' . $result->getStatusCode() . '</' . $color . '>');
			if ($result->getStatusCode() === 200) {
				return true;
			}
		} catch (RequestNetworkException $e) {
			$output->writeln('<error>fail</error>');
		}

		return false;
	}

	/**
	 * @param InputInterface $input
	 * @param OutputInterface $output
	 * @param string $address
	 */
	private function saveUrl(InputInterface $input, OutputInterface $output, string $address): void {
		if ($address === '') {
			return;
		}

		$output->writeln('');
		$output->writeln(
			'The address <info>' . $address . '</info> seems to reach your local Nextcloud.'
		);

		$helper = $this->getHelper('question');
		$output->writeln('');
		$question = new ConfirmationQuestion(
			'<info>Do you want to store this address in database ?</info> (y/N) ', false, '/^(y|Y)/i'
		);

		if (!$helper->ask($input, $output, $question)) {
			$output->writeln('Configuration NOT saved');

			return;
		}

		$this->configService->setAppValue(ConfigService::FORCE_NC_BASE, $address);
		$output->writeln(
			'New configuration <info>' . Application::APP_ID . '.' . ConfigService::FORCE_NC_BASE . '=\''
			. $address . '\'</info> stored in database'
		);
	}

	/**
	 * @param string $test
	 *
	 * @return array
	 * @throws Exception
	 */
	private function parseAddress(string $test): array {
		$scheme = parse_url($test, PHP_URL_SCHEME);
		$cloudId = parse_url($test, PHP_URL_HOST);
		$cloudIdPort = parse_url($test, PHP_URL_PORT);
		$path = parse_url($test, PHP_URL_PATH);

		if (is_bool($scheme) || is_bool($cloudId) || is_null($scheme) || is_null($cloudId)) {
			throw new Exception();
		}

		if (is_null($path) || is_bool($path)) {
			$path = '';
		}

		$path = rtrim($path, '/');

		if (!is_null($cloudIdPort)) {
			$cloudId = $cloudId . ':' . $cloudIdPort;
		}

		return [$scheme, $cloudId, $path];
	}
}

Zerion Mini Shell 1.0