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 loopback address.');
$this->checkLoopback($input, $output, $test);
$output->writeln('');
$output->writeln('');
}
if ($type === '' || $type === 'internal') {
$output->writeln('### Testing internal address.');
$this->checkInternal($input, $output, $test);
$output->writeln('');
$output->writeln('');
}
if (!$input->getOption('alpha')) {
return 0;
}
if ($type === '' || $type === 'frontal') {
$output->writeln('### Testing frontal 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 loopback setting is mandatory and can be checked locally.');
$output->writeln(
'. The address you need to define here must be a reachable url of your F7cloud 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('* Loopback address looks good');
if ($notDefault) {
$this->saveLoopback($input, $output, $test);
}
return;
} catch (Exception $e) {
}
$output->writeln('');
$output->writeln('- You do not have a valid loopback address setup right now.');
$output->writeln('');
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
while (true) {
$question = new Question('Please write down a new loopback address to test: ', '');
$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('format must be http[s]://domain.name[:post][/path]');
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;
}
$this->appConfig->setValueInt(Application::APP_ID, 'test_dummy_token', time() + 10);
if (!$this->testRequest(
$output, 'POST', 'circles.EventWrapper.asyncBroadcast',
['token' => 'test-dummy-token']
)) {
return false;
}
$timer = round(microtime(true) * 1000.0);
$output->write('- Creating async FederatedEvent ');
$test = new FederatedEvent(LoopbackTest::class);
$this->federatedEventService->newEvent($test);
$output->writeln(
'' . $test->getWrapperToken() . ' ' .
'(took ' . ((string)(round(microtime(true) * 1000.0) - $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('Event created too many Wrappers');
return false;
}
$wrapper = array_shift($wrappers);
$checkVerify = $wrapper->getEvent()->getData()->gInt('verify');
if ($checkVerify === LoopbackTest::VERIFY) {
$output->write('verify=' . ((string)$checkVerify) . ' ');
} else {
$output->writeln('verify=' . ((string)$checkVerify) . '');
return false;
}
$checkManage = $wrapper->getResult()->gInt('manage');
if ($checkManage === LoopbackTest::MANAGE) {
$output->write('manage=' . ((string)$checkManage) . ' ');
} else {
$output->writeln('manage=' . ((string)$checkManage) . '');
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 '
. $loopback
. ' as your loopback address ? (y/N) ',
false, '/^(y|Y)/i'
);
/** @var QuestionHelper $helper */
$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 ' . $loopback . ' is now used as loopback'
);
}
/**
* @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 internal 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 F7cloud, reachable by all other instances of our GlobalScale.'
);
$question = new ConfirmationQuestion(
'- Do you want to enable this feature ? (y/N) ', false, '/^(y|Y)/i'
);
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
if (!$helper->ask($input, $output, $question)) {
$output->writeln('skipping.');
return;
}
while (true) {
$output->writeln('');
$question = new Question(
'Please write down a new internal address to test: ', ''
);
$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('format must be http[s]://domain.name[:post][/path]');
continue;
}
$internal = rtrim($scheme . '://' . $cloudId, '/');
$fullInternal = rtrim($scheme . '://' . $cloudId . $path, '/');
$question = new ConfirmationQuestion(
'Do you want to check the validity of this internal address? (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 curl command from a terminal on your local network and paste its result: '
);
$output->writeln(
' curl -L "' . $internal
. '/.well-known/webfinger?resource=http://f7cloud.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('Cannot extract SUBJECT from the pasted data');
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('Cannot retrieve HREF from the pasted data');
continue;
}
$href = $this->interfaceService->getCloudPath(
'circles.Remote.appService',
[],
InterfaceService::IFACE_TEST
);
if ($pastedHref !== $href) {
$output->writeln(
'The returned data (' . $pastedHref . ') are not the one expected: '
. $href
);
continue;
}
$output->writeln('');
$output->writeln('First step seems fine.');
$output->writeln(
'Next step, please run this curl 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(
'The returned data ('
. $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root')
. ') are not the one expected: '
. $appSignatory->getUid(true) . '/' . $appSignatory->getRoot()
);
continue;
}
$output->writeln('* Internal 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 ' . $internal
. ' as your internal address ? (y/N) ',
false, '/^(y|Y)/i'
);
/** @var QuestionHelper $helper */
$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 ' . $internal . ' is now used as internal');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param string $test
*/
private function checkFrontal(InputInterface $input, OutputInterface $output, string $test): void {
$output->writeln('. The frontal setting is optional.');
$output->writeln(
'. The purpose of this address is for your Federated Circle to reach other instances of F7cloud 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(
'- Do you want to enable this feature ? (y/N) ', false, '/^(y|Y)/i'
);
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
if (!$helper->ask($input, $output, $question)) {
$output->writeln('skipping.');
return;
}
while (true) {
$question = new Question(
'Please write down a new frontal address to test: ', ''
);
$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('format must be http[s]://domain.name[:post][/path]');
continue;
}
$frontal = rtrim($scheme . '://' . $cloudId, '/');
$fullFrontal = rtrim($scheme . '://' . $cloudId . $path, '/');
$question = new ConfirmationQuestion(
'Do you want to check the validity of this frontal address? (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 curl command from a remote terminal and paste its result: '
);
$output->writeln(
' curl -L "' . $frontal
. '/.well-known/webfinger?resource=http://f7cloud.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('Cannot extract SUBJECT from the pasted data');
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('Cannot retrieve HREF from the pasted data');
continue;
}
$href = $this->interfaceService->getCloudPath(
'circles.Remote.appService',
[],
InterfaceService::IFACE_TEST
);
if ($pastedHref !== $href) {
$output->writeln(
'The returned data (' . $pastedHref . ') are not the one expected: '
. $href
);
continue;
}
$output->writeln('');
$output->writeln('First step seems fine.');
$output->writeln(
'Next step, please run this curl 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(
'The returned data ('
. $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root')
. ') are not the one expected: '
. $appSignatory->getUid(true) . '/' . $appSignatory->getRoot()
);
continue;
}
$output->writeln('* Frontal 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 ' . $frontal
. ' as your frontal address ? (y/N) ',
false, '/^(y|Y)/i'
);
/** @var QuestionHelper $helper */
$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 ' . $frontal . ' is now used as frontal');
}
/**
* @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 . '>' . ((string)$result->getStatusCode()) . '' . $color . '>');
if ($result->getStatusCode() === 200) {
return true;
}
} catch (RequestNetworkException $e) {
$output->writeln('fail');
}
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 ' . $address . ' seems to reach your local F7cloud.'
);
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$output->writeln('');
$question = new ConfirmationQuestion(
'Do you want to store this address in database ? (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 ' . Application::APP_ID . '.' . ConfigService::FORCE_NC_BASE . '=\''
. $address . '\' 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 . ':' . ((string)$cloudIdPort);
}
return [$scheme, $cloudId, $path];
}
}