diff --git a/composer.json b/composer.json index 50a3eab6f9..6bbab203aa 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "license": "MIT", "require": { "doctrine/cache": "~1.4.2", - "platformsh/client": "0.1.22", + "platformsh/client": "0.1.25", "symfony/console": ">= 2.5.2 < 2.7.0", "symfony/yaml": "~2.5", "symfony/finder": "~2.5", diff --git a/composer.lock b/composer.lock index 25fa5adc2f..bf78cc2ca8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "a23456cf58f3af5ab6bcc50e628ef6cd", + "hash": "04cbd376b25031efc594be557a34bcb0", "packages": [ { "name": "cocur/slugify", @@ -678,16 +678,16 @@ }, { "name": "platformsh/client", - "version": "v0.1.22", + "version": "v0.1.25", "source": { "type": "git", "url": "https://github.com/platformsh/platformsh-client-php.git", - "reference": "ef62cecf911d58f8aa07dabd3fe91a29dce80d70" + "reference": "59ff57d37fa00cab88fb02ffe23bc4db9bb7251a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/platformsh/platformsh-client-php/zipball/ef62cecf911d58f8aa07dabd3fe91a29dce80d70", - "reference": "ef62cecf911d58f8aa07dabd3fe91a29dce80d70", + "url": "https://api.github.com/repos/platformsh/platformsh-client-php/zipball/59ff57d37fa00cab88fb02ffe23bc4db9bb7251a", + "reference": "59ff57d37fa00cab88fb02ffe23bc4db9bb7251a", "shasum": "" }, "require": { @@ -715,7 +715,7 @@ } ], "description": "Platform.sh API client", - "time": "2015-09-04 11:36:40" + "time": "2015-09-23 11:43:38" }, { "name": "react/promise", diff --git a/src/Command/Activity/ActivityListCommand.php b/src/Command/Activity/ActivityListCommand.php index 11ec95ae49..d1f3f9728c 100644 --- a/src/Command/Activity/ActivityListCommand.php +++ b/src/Command/Activity/ActivityListCommand.php @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ); } - if ($output instanceof StreamOutput && ($input->getOption('pipe') || !$this->isTerminal($output))) { + if ($output instanceof StreamOutput && $input->getOption('pipe')) { $stream = $output->getStream(); array_unshift($rows, $headers); foreach ($rows as $row) { diff --git a/src/Command/Auth/LoginCommand.php b/src/Command/Auth/LoginCommand.php index 0a4bae1a17..e4bb49ef61 100644 --- a/src/Command/Auth/LoginCommand.php +++ b/src/Command/Auth/LoginCommand.php @@ -1,6 +1,7 @@ getHelper('question'); $question = new Question('Your email address: '); @@ -96,10 +98,47 @@ function ($answer) { try { $this->authenticateUser($email, $password); - } catch (\InvalidArgumentException $e) { - $output->writeln("\nLogin failed. Please check your credentials.\n"); - $output->writeln("Forgot your password? Visit: https://accounts.platform.sh/user/password\n"); - $this->configureAccount($input, $output); + } catch (BadResponseException $e) { + // If a two-factor authentication challenge is received, then ask + // the user for their TOTP code, and then retry authenticateUser(). + if ($e->getResponse()->getHeader('X-Drupal-TFA')) { + $question = new Question("Your application verification code: "); + $question->setValidator(function ($answer) use ($email, $password) { + if (trim($answer) == '') { + throw new \RuntimeException("The code cannot be empty."); + } + try { + $this->authenticateUser($email, $password, $answer); + } + catch (BadResponseException $e) { + // If there is a two-factor authentication error, show + // the error description that the server provides. + // + // A RuntimeException here causes the user to be asked + // again for their TOTP code. + if ($e->getResponse()->getHeader('X-Drupal-TFA')) { + $json = $e->getResponse()->json(); + throw new \RuntimeException($json['error_description']); + } + else { + throw $e; + } + } + + return $answer; + }); + $question->setMaxAttempts(5); + $output->writeln("\nTwo-factor authentication is required."); + $helper->ask($input, $output, $question); + } + elseif ($e->getResponse()->getStatusCode() === 401) { + $output->writeln("\nLogin failed. Please check your credentials.\n"); + $output->writeln("Forgot your password? Visit: https://accounts.platform.sh/user/password\n"); + $this->configureAccount($input, $output); + } + else { + throw $e; + } } } diff --git a/src/Command/Environment/EnvironmentBranchCommand.php b/src/Command/Environment/EnvironmentBranchCommand.php index eb1aea595f..df1ab7b185 100644 --- a/src/Command/Environment/EnvironmentBranchCommand.php +++ b/src/Command/Environment/EnvironmentBranchCommand.php @@ -55,6 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output) { $this->envArgName = 'parent'; $this->validateInput($input, true); + $selectedProject = $this->getSelectedProject(); $branchName = $input->getArgument('name'); if (empty($branchName)) { @@ -62,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // List environments. return $this->runOtherCommand( 'environments', - array('--project' => $this->getSelectedProject()->id) + array('--project' => $selectedProject->id) ); } $this->stdErr->writeln("You must specify the name of the new branch."); @@ -79,7 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } - if ($environment = $this->getEnvironment($machineName, $this->getSelectedProject())) { + if ($environment = $this->getEnvironment($machineName, $selectedProject)) { $checkout = $this->getHelper('question') ->confirm( "The environment $machineName already exists. Check out?", @@ -129,6 +130,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $activity = $selectedEnvironment->branch($branchName, $machineName); + // Clear the environments cache, as branching has started. + $this->clearEnvironmentsCache($selectedProject); + if ($projectRoot) { $gitHelper = new GitHelper(new ShellHelper($this->stdErr)); $gitHelper->setDefaultRepositoryDir($projectRoot . '/' . LocalProject::REPOSITORY_DIR); @@ -169,6 +173,9 @@ protected function execute(InputInterface $input, OutputInterface $output) "The environment $branchName has been branched.", 'Branching failed' ); + + // Clear the environments cache again. + $this->clearEnvironmentsCache($selectedProject); } $build = $input->getOption('build'); diff --git a/src/Command/Environment/EnvironmentSshCommand.php b/src/Command/Environment/EnvironmentSshCommand.php index 6c93a4f523..5ca9257e28 100644 --- a/src/Command/Environment/EnvironmentSshCommand.php +++ b/src/Command/Environment/EnvironmentSshCommand.php @@ -37,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $sshUrl = $this->getSelectedEnvironment() ->getSshUrl($input->getOption('app')); - if ($input->getOption('pipe') || !$this->isTerminal($output)) { + if ($input->getOption('pipe')) { $output->write($sshUrl); return 0; diff --git a/src/Command/Local/LocalDrushAliasesCommand.php b/src/Command/Local/LocalDrushAliasesCommand.php index 41c3583a3e..14701df0ef 100644 --- a/src/Command/Local/LocalDrushAliasesCommand.php +++ b/src/Command/Local/LocalDrushAliasesCommand.php @@ -50,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $projectConfig = LocalProject::getProjectConfig($projectRoot); $current_group = isset($projectConfig['alias-group']) ? $projectConfig['alias-group'] : $projectConfig['id']; - if ($input->getOption('pipe') || !$this->isTerminal($output)) { + if ($input->getOption('pipe')) { $output->writeln($current_group); return 0; diff --git a/src/Command/Local/LocalInitCommand.php b/src/Command/Local/LocalInitCommand.php index edecd6cca3..980b3ec63f 100644 --- a/src/Command/Local/LocalInitCommand.php +++ b/src/Command/Local/LocalInitCommand.php @@ -56,6 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } $gitUrl = $project->getGitUrl(); + $projectId = $project->id; } $inside = strpos(getcwd(), $realPath) === 0; diff --git a/src/Command/PlatformCommand.php b/src/Command/PlatformCommand.php index 652a4d6ea5..6e92ed4ccb 100644 --- a/src/Command/PlatformCommand.php +++ b/src/Command/PlatformCommand.php @@ -274,12 +274,13 @@ public function isLocal() * * @param string $email The user's email. * @param string $password The user's password. + * @param string $totp The user's TFA one-time password. */ - protected function authenticateUser($email, $password) + protected function authenticateUser($email, $password, $totp = null) { $this->getClient(false) ->getConnector() - ->logIn($email, $password, true); + ->logIn($email, $password, true, $totp); } /** @@ -450,6 +451,14 @@ public function getProjects($refresh = false) */ protected function getProject($id, $host = null, $refresh = false) { + // Allow the specified project to be a full URL. + if (strpos($id, '//') !== false) { + $url = $id; + $id = basename($url); + $host = parse_url($url, PHP_URL_HOST); + } + + // Find the project in the user's main project list. $projects = $this->getProjects($refresh); if (isset($projects[$id])) { return $projects[$id]; @@ -553,7 +562,7 @@ protected function getEnvironment($id, Project $project = null, $refresh = false * * @param Project $project */ - protected function clearEnvironmentsCache(Project $project = null) + public function clearEnvironmentsCache(Project $project = null) { $project = $project ?: $this->getSelectedProject(); self::$cache->delete('environments:' . $project->id); diff --git a/src/Command/Project/ProjectGetCommand.php b/src/Command/Project/ProjectGetCommand.php index b3a04be5d6..e7c888346b 100644 --- a/src/Command/Project/ProjectGetCommand.php +++ b/src/Command/Project/ProjectGetCommand.php @@ -122,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $local = new LocalProject(); $hostname = parse_url($project->getUri(), PHP_URL_HOST) ?: null; - $local->createProjectFiles($projectRoot, $projectId, $hostname); + $local->createProjectFiles($projectRoot, $project->id, $hostname); $environments = $this->getEnvironments($project, true); diff --git a/src/Command/Project/ProjectListCommand.php b/src/Command/Project/ProjectListCommand.php index 990073f870..ae8e637824 100644 --- a/src/Command/Project/ProjectListCommand.php +++ b/src/Command/Project/ProjectListCommand.php @@ -37,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $projects = $this->getProjects($refresh); - if ($input->getOption('pipe') || !$this->isTerminal($output)) { + if ($input->getOption('pipe')) { $output->writeln(array_keys($projects)); return 0; diff --git a/src/Command/User/UserRoleCommand.php b/src/Command/User/UserRoleCommand.php index f934b996be..1edef4a3e1 100644 --- a/src/Command/User/UserRoleCommand.php +++ b/src/Command/User/UserRoleCommand.php @@ -89,7 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->stdErr->writeln("User $email updated"); } - if ($input->getOption('pipe') || !$this->isTerminal($output)) { + if ($input->getOption('pipe')) { if ($level == 'project') { $output->writeln($selectedUser->role); } elseif ($level == 'environment') { diff --git a/src/Command/Variable/VariableGetCommand.php b/src/Command/Variable/VariableGetCommand.php index 987b84daf1..a9c45f3ee5 100644 --- a/src/Command/Variable/VariableGetCommand.php +++ b/src/Command/Variable/VariableGetCommand.php @@ -76,7 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } } - if ($input->getOption('pipe') || !$this->isTerminal($output)) { + if ($input->getOption('pipe')) { foreach ($results as $variable) { $output->writeln($variable['id'] . "\t" . $variable['value']); } diff --git a/src/Console/EventSubscriber.php b/src/Console/EventSubscriber.php index d945563b31..38f9e8aaae 100644 --- a/src/Console/EventSubscriber.php +++ b/src/Console/EventSubscriber.php @@ -4,9 +4,11 @@ use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ParseException; +use Platformsh\Cli\Command\PlatformCommand; use Platformsh\Cli\Exception\ConnectionFailedException; use Platformsh\Cli\Exception\LoginRequiredException; use Platformsh\Cli\Exception\PermissionDeniedException; +use Platformsh\Client\Exception\EnvironmentStateException; use Symfony\Component\Console\Event\ConsoleExceptionEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -77,5 +79,14 @@ public function onException(ConsoleExceptionEvent $event) $event->stopPropagation(); } } + + // When an environment is found to be in the wrong state, perhaps our + // cache is old - we should invalidate it. + if ($exception instanceof EnvironmentStateException) { + $command = $event->getCommand(); + if ($command instanceof PlatformCommand) { + $command->clearEnvironmentsCache(); + } + } } }