From fb0ad7c900500f6e3d5451f79baad8c86ee725d4 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 15 Jul 2020 16:18:21 +0100 Subject: [PATCH 1/5] GitLab: clarify interactive auth prompt --- src/Composer/Util/GitLab.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index 7a69ad251..b3cb421ca 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -104,7 +104,7 @@ class GitLab } $this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName())); - $this->io->writeError('To revoke access to this token you can visit '.$originUrl.'/profile/applications'); + $this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/profile/applications'); $attemptCounter = 0; @@ -116,12 +116,17 @@ class GitLab // 403 is max login attempts exceeded if (in_array($e->getCode(), array(403, 401))) { if (401 === $e->getCode()) { - $this->io->writeError('Bad credentials.'); + $response = json_decode($e->getResponse(), true); + if (isset($response['error']) && $response['error'] === 'invalid_grant') { + $this->io->writeError('Bad credentials. If you have two factor authentication enabled you will have to manually create a personal access token'); + } else { + $this->io->writeError('Bad credentials.'); + } } else { $this->io->writeError('Maximum number of login attempts exceeded. Please try again later.'); } - $this->io->writeError('You can also manually create a personal token at '.$scheme.'://'.$originUrl.'/profile/personal_access_tokens'); + $this->io->writeError('You can also manually create a personal access token enabling the "read_api" scope at '.$scheme.'://'.$originUrl.'/profile/personal_access_tokens'); $this->io->writeError('Add it using "composer config --global --auth gitlab-token.'.$originUrl.' "'); continue; From cae913c434818e1318a336e129bfe4e58f5b8e0f Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Thu, 7 May 2020 19:10:36 +0100 Subject: [PATCH 2/5] Add Windows UAC elevation to self-update command If self-update fails on Windows due to file permission issues, a .vbs script is used to elevate a call to the cmd.exe `move` command. Unfortunately it is not possible to know if the user cancelled the UAC prompt using this method - it is possible using a Powershell script, but flashing hidden windows make this a less desirable option. The only downside is that a UAC invoked process is asynchronous, so a 300 millisecond timeout is used to allow cmd.exe to do its stuff. Therefore if the OS is busy the script may finish first and incorrectly report that the file has not been written. --- src/Composer/Command/SelfUpdateCommand.php | 195 ++++++++++++++++++--- 1 file changed, 167 insertions(+), 28 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 925270ade..4ac144ea8 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -16,6 +16,7 @@ use Composer\Composer; use Composer\Factory; use Composer\Config; use Composer\Util\Filesystem; +use Composer\Util\Platform; use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Versions; use Composer\IO\IOInterface; @@ -106,6 +107,11 @@ EOT return $this->fetchKeys($io, $config); } + // ensure composer.phar location is accessible + if (!file_exists($localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$localFilename.'" is not accessible'); + } + // check if current dir is writable and if not try the cache dir from settings $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir; @@ -248,10 +254,8 @@ TAGSPUBKEY $this->cleanBackups($rollbackDir); } - if ($err = $this->setLocalPhar($localFilename, $tempFilename, $backupFile)) { + if (!$this->setLocalPhar($localFilename, $tempFilename, $backupFile)) { @unlink($tempFilename); - $io->writeError('The file is corrupted ('.$err->getMessage().').'); - $io->writeError('Please re-run the self-update command to try again.'); return 1; } @@ -331,9 +335,7 @@ TAGSPUBKEY $io = $this->getIO(); $io->writeError(sprintf("Rolling back to version %s.", $rollbackVersion)); - if ($err = $this->setLocalPhar($localFilename, $oldFile)) { - $io->writeError('The backup file was corrupted ('.$err->getMessage().').'); - + if (!$this->setLocalPhar($localFilename, $oldFile)) { return 1; } @@ -341,37 +343,49 @@ TAGSPUBKEY } /** - * @param string $localFilename - * @param string $newFilename - * @param string $backupTarget - * @throws \Exception - * @return \UnexpectedValueException|\PharException|null + * Checks if the downloaded/rollback phar is valid then moves it + * + * @param string $localFilename The composer.phar location + * @param string $newFilename The downloaded or backup phar + * @param string $backupTarget The filename to use for the backup + * @throws \FilesystemException If the file cannot be moved + * @return bool Whether the phar is valid and has been moved */ protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null) { + $io = $this->getIO(); + @chmod($newFilename, fileperms($localFilename)); + + // check phar validity + if (!$this->validatePhar($newFilename, $error)) { + $io->writeError('The '.($backupTarget ? 'update' : 'backup').' file is corrupted ('.$error.')'); + + if ($backupTarget) { + $io->writeError('Please re-run the self-update command to try again.'); + } + + return false; + } + + // copy current file into backups dir + if ($backupTarget) { + @copy($localFilename, $backupTarget); + } + try { - @chmod($newFilename, fileperms($localFilename)); - if (!ini_get('phar.readonly')) { - // test the phar validity - $phar = new \Phar($newFilename); - // free the variable to unlock the file - unset($phar); - } - - // copy current file into installations dir - if ($backupTarget && file_exists($localFilename)) { - @copy($localFilename, $backupTarget); - } - rename($newFilename, $localFilename); - return null; + return true; } catch (\Exception $e) { - if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { - throw $e; + // see if we can run this operation as an Admin on Windows + if (!is_writable(dirname($localFilename)) + && $io->isInteractive() + && $this->isWindowsNonAdminUser($isCygwin)) { + return $this->tryAsWindowsAdmin($localFilename, $newFilename, $isCygwin); } - return $e; + $action = 'Composer '.($backupTarget ? 'update' : 'rollback'); + throw new FilesystemException($action.' failed: "'.$localFilename.'" could not be written.'.PHP_EOL.$e->getMessage()); } } @@ -414,4 +428,129 @@ TAGSPUBKEY return $finder; } + + /** + * Validates the downloaded/backup phar file + * + * @param string $pharFile The downloaded or backup phar + * @param null|string $error Set by method on failure + * + * Code taken from getcomposer.org/installer. Any changes should be made + * there and replicated here + * + * @return bool If the operation succeeded + * @throws \Exception + */ + protected function validatePhar($pharFile, &$error) + { + if (ini_get('phar.readonly')) { + return true; + } + + try { + // Test the phar validity + $phar = new \Phar($pharFile); + // Free the variable to unlock the file + unset($phar); + $result = true; + } catch (\Exception $e) { + if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { + throw $e; + } + $error = $e->getMessage(); + $result = false; + } + + return $result; + } + + /** + * Returns true if this is a non-admin Windows user account + * + * @param null|string $isCygwin Set by method + * @return bool + */ + protected function isWindowsNonAdminUser(&$isCygwin) + { + $isCygwin = preg_match('/cygwin/i', php_uname()); + + if (!$isCygwin && !Platform::isWindows()) { + return false; + } + + // fltmc.exe manages filter drivers and errors without admin privileges + $command = sprintf('%sfltmc.exe filters', $isCygwin ? 'cmd.exe /c ' : ''); + exec($command, $output, $exitCode); + + return $exitCode !== 0; + } + + /** + * Invokes a UAC prompt to update composer.phar as an admin + * + * Uses a .vbs script to elevate and run the cmd.exe move command. + * + * @param string $localFilename The composer.phar location + * @param string $newFilename The downloaded or backup phar + * @param bool $isCygwin Whether we are running on Cygwin + * @return bool Whether composer.phar has been updated + */ + protected function tryAsWindowsAdmin($localFilename, $newFilename, $isCygwin) + { + $io = $this->getIO(); + + $io->writeError('Unable to write "'.$localFilename.'". Access is denied.'); + $helpMessage = 'Please run the self-update command as an Administrator.'; + $question = 'Complete this operation with Administrator priviledges [Y,n]? '; + + if (!$io->askConfirmation($question, false)) { + $io->writeError('Operation cancelled. '.$helpMessage.''); + + return false; + } + + $tmpFile = tempnam(sys_get_temp_dir(), ''); + $script = $tmpFile.'.vbs'; + rename($tmpFile, $script); + + $checksum = hash_file('sha256', $newFilename); + + // format the file names for cmd.exe + if ($isCygwin) { + $source = exec(sprintf("cygpath -w '%s'", $newFilename)); + $destination = exec(sprintf("cygpath -w '%s'", $localFilename)); + } else { + // cmd's internal move is fussy about backslashes + $source = str_replace('/', '\\', $newFilename); + $destination = str_replace('/', '\\', $localFilename); + } + + $vbs = <<writeError('Operation succeeded.'); + } else { + $io->writeError('Operation failed (file not written). '.$helpMessage.''); + }; + + return $result; + } } From 272654d6e22fe31af4ed035c235f89aa895f7ad8 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Thu, 9 Jul 2020 22:03:49 +0100 Subject: [PATCH 3/5] Fixed spelling mistake --- src/Composer/Command/SelfUpdateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 4ac144ea8..9200734a6 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -501,7 +501,7 @@ TAGSPUBKEY $io->writeError('Unable to write "'.$localFilename.'". Access is denied.'); $helpMessage = 'Please run the self-update command as an Administrator.'; - $question = 'Complete this operation with Administrator priviledges [Y,n]? '; + $question = 'Complete this operation with Administrator privileges [Y,n]? '; if (!$io->askConfirmation($question, false)) { $io->writeError('Operation cancelled. '.$helpMessage.''); From 57f91d01c7d6ae9a3ce6ffa577d7fb16e94d4a8f Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 10 Jul 2020 11:31:18 +0100 Subject: [PATCH 4/5] Fix doc comment --- src/Composer/Command/SelfUpdateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 9200734a6..031e4d253 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -467,7 +467,7 @@ TAGSPUBKEY /** * Returns true if this is a non-admin Windows user account * - * @param null|string $isCygwin Set by method + * @param null|bool $isCygwin Set by method * @return bool */ protected function isWindowsNonAdminUser(&$isCygwin) From 6cb4dc41b81e62d46bd13476cc05390596f2d122 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 16 Jul 2020 12:26:48 +0200 Subject: [PATCH 5/5] Fix bitbucket detection of redirects to login page, fixes #9041 --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f9113d03f..cc4cb61f4 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -404,7 +404,7 @@ class RemoteFilesystem if ($originUrl === 'bitbucket.org' && !$this->isPublicBitBucketDownload($fileUrl) && substr($fileUrl, -4) === '.zip' - && (!$locationHeader || substr($locationHeader, -4) !== '.zip') + && (!$locationHeader || substr(parse_url($locationHeader, PHP_URL_PATH), -4) !== '.zip') && $contentType && preg_match('{^text/html\b}i', $contentType) ) { $result = false;