diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php
index 3bc482e63..2e1d37d43 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|bool $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 privileges [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;
+ }
}
diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php
index ea0c72477..ded2393b6 100644
--- a/src/Composer/Util/GitLab.php
+++ b/src/Composer/Util/GitLab.php
@@ -119,7 +119,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;
@@ -131,12 +131,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;
diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php
index 4bac6a88f..615de0adb 100644
--- a/src/Composer/Util/RemoteFilesystem.php
+++ b/src/Composer/Util/RemoteFilesystem.php
@@ -351,7 +351,7 @@ class RemoteFilesystem
if ($originUrl === 'bitbucket.org'
&& !$this->authHelper->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;