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.pull/9056/head
parent
a585c65a12
commit
cae913c434
|
@ -16,6 +16,7 @@ use Composer\Composer;
|
||||||
use Composer\Factory;
|
use Composer\Factory;
|
||||||
use Composer\Config;
|
use Composer\Config;
|
||||||
use Composer\Util\Filesystem;
|
use Composer\Util\Filesystem;
|
||||||
|
use Composer\Util\Platform;
|
||||||
use Composer\SelfUpdate\Keys;
|
use Composer\SelfUpdate\Keys;
|
||||||
use Composer\SelfUpdate\Versions;
|
use Composer\SelfUpdate\Versions;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
|
@ -106,6 +107,11 @@ EOT
|
||||||
return $this->fetchKeys($io, $config);
|
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
|
// check if current dir is writable and if not try the cache dir from settings
|
||||||
$tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir;
|
$tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir;
|
||||||
|
|
||||||
|
@ -248,10 +254,8 @@ TAGSPUBKEY
|
||||||
$this->cleanBackups($rollbackDir);
|
$this->cleanBackups($rollbackDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($err = $this->setLocalPhar($localFilename, $tempFilename, $backupFile)) {
|
if (!$this->setLocalPhar($localFilename, $tempFilename, $backupFile)) {
|
||||||
@unlink($tempFilename);
|
@unlink($tempFilename);
|
||||||
$io->writeError('<error>The file is corrupted ('.$err->getMessage().').</error>');
|
|
||||||
$io->writeError('<error>Please re-run the self-update command to try again.</error>');
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -331,9 +335,7 @@ TAGSPUBKEY
|
||||||
|
|
||||||
$io = $this->getIO();
|
$io = $this->getIO();
|
||||||
$io->writeError(sprintf("Rolling back to version <info>%s</info>.", $rollbackVersion));
|
$io->writeError(sprintf("Rolling back to version <info>%s</info>.", $rollbackVersion));
|
||||||
if ($err = $this->setLocalPhar($localFilename, $oldFile)) {
|
if (!$this->setLocalPhar($localFilename, $oldFile)) {
|
||||||
$io->writeError('<error>The backup file was corrupted ('.$err->getMessage().').</error>');
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,37 +343,49 @@ TAGSPUBKEY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $localFilename
|
* Checks if the downloaded/rollback phar is valid then moves it
|
||||||
* @param string $newFilename
|
*
|
||||||
* @param string $backupTarget
|
* @param string $localFilename The composer.phar location
|
||||||
* @throws \Exception
|
* @param string $newFilename The downloaded or backup phar
|
||||||
* @return \UnexpectedValueException|\PharException|null
|
* @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)
|
protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null)
|
||||||
{
|
{
|
||||||
|
$io = $this->getIO();
|
||||||
|
@chmod($newFilename, fileperms($localFilename));
|
||||||
|
|
||||||
|
// check phar validity
|
||||||
|
if (!$this->validatePhar($newFilename, $error)) {
|
||||||
|
$io->writeError('<error>The '.($backupTarget ? 'update' : 'backup').' file is corrupted ('.$error.')</error>');
|
||||||
|
|
||||||
|
if ($backupTarget) {
|
||||||
|
$io->writeError('<error>Please re-run the self-update command to try again.</error>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy current file into backups dir
|
||||||
|
if ($backupTarget) {
|
||||||
|
@copy($localFilename, $backupTarget);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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);
|
rename($newFilename, $localFilename);
|
||||||
|
|
||||||
return null;
|
return true;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) {
|
// see if we can run this operation as an Admin on Windows
|
||||||
throw $e;
|
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;
|
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('<error>Unable to write "'.$localFilename.'". Access is denied.</error>');
|
||||||
|
$helpMessage = 'Please run the self-update command as an Administrator.';
|
||||||
|
$question = 'Complete this operation with Administrator priviledges [<comment>Y,n</comment>]? ';
|
||||||
|
|
||||||
|
if (!$io->askConfirmation($question, false)) {
|
||||||
|
$io->writeError('<warning>Operation cancelled. '.$helpMessage.'</warning>');
|
||||||
|
|
||||||
|
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 = <<<EOT
|
||||||
|
Set UAC = CreateObject("Shell.Application")
|
||||||
|
UAC.ShellExecute "cmd.exe", "/c move /y ""$source"" ""$destination""", "", "runas", 0
|
||||||
|
Wscript.Sleep(300)
|
||||||
|
EOT;
|
||||||
|
|
||||||
|
file_put_contents($script, $vbs);
|
||||||
|
|
||||||
|
if ($isCygwin) {
|
||||||
|
chmod($script, 0755);
|
||||||
|
$cygscript = sprintf('"%s"', exec(sprintf("cygpath -w '%s'", $script)));
|
||||||
|
$command = sprintf("cmd.exe /c '%s'", $cygscript);
|
||||||
|
} else {
|
||||||
|
$command = sprintf('"%s"', $script);
|
||||||
|
}
|
||||||
|
|
||||||
|
exec($command);
|
||||||
|
@unlink($script);
|
||||||
|
|
||||||
|
// see if the file was moved
|
||||||
|
if ($result = (hash_file('sha256', $localFilename) === $checksum)) {
|
||||||
|
$io->writeError('<info>Operation succeeded.</info>');
|
||||||
|
} else {
|
||||||
|
$io->writeError('<error>Operation failed (file not written). '.$helpMessage.'</error>');
|
||||||
|
};
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue