1<?php 2 3/* 4 * This file is part of Composer. 5 * 6 * (c) Nils Adermann <naderman@naderman.de> 7 * Jordi Boggiano <j.boggiano@seld.be> 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Composer\Console; 14 15use Composer\Util\Platform; 16use Composer\Util\Silencer; 17use Symfony\Component\Console\Application as BaseApplication; 18use Symfony\Component\Console\Input\InputInterface; 19use Symfony\Component\Console\Input\InputOption; 20use Symfony\Component\Console\Output\OutputInterface; 21use Symfony\Component\Console\Output\ConsoleOutput; 22use Symfony\Component\Console\Formatter\OutputFormatter; 23use Composer\Command; 24use Composer\Composer; 25use Composer\Factory; 26use Composer\IO\IOInterface; 27use Composer\IO\ConsoleIO; 28use Composer\Json\JsonValidationException; 29use Composer\Util\ErrorHandler; 30use Composer\EventDispatcher\ScriptExecutionException; 31use Composer\Exception\NoSslException; 32 33/** 34 * The console application that handles the commands 35 * 36 * @author Ryan Weaver <ryan@knplabs.com> 37 * @author Jordi Boggiano <j.boggiano@seld.be> 38 * @author François Pluchino <francois.pluchino@opendisplay.com> 39 */ 40class Application extends BaseApplication 41{ 42 /** 43 * @var Composer 44 */ 45 protected $composer; 46 47 /** 48 * @var IOInterface 49 */ 50 protected $io; 51 52 private static $logo = ' ______ 53 / ____/___ ____ ___ ____ ____ ________ _____ 54 / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ 55/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / 56\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ 57 /_/ 58'; 59 60 private $hasPluginCommands = false; 61 private $disablePluginsByDefault = false; 62 63 public function __construct() 64 { 65 static $shutdownRegistered = false; 66 67 if (function_exists('ini_set') && extension_loaded('xdebug')) { 68 ini_set('xdebug.show_exception_trace', false); 69 ini_set('xdebug.scream', false); 70 } 71 72 if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { 73 date_default_timezone_set(Silencer::call('date_default_timezone_get')); 74 } 75 76 if (!$shutdownRegistered) { 77 $shutdownRegistered = true; 78 79 register_shutdown_function(function () { 80 $lastError = error_get_last(); 81 82 if ($lastError && $lastError['message'] && 83 (strpos($lastError['message'], 'Allowed memory') !== false /*Zend PHP out of memory error*/ || 84 strpos($lastError['message'], 'exceeded memory') !== false /*HHVM out of memory errors*/)) { 85 echo "\n". 'Check https://getcomposer.org/doc/articles/troubleshooting.md#memory-limit-errors for more info on how to handle out of memory errors.'; 86 } 87 }); 88 } 89 90 parent::__construct('Composer', Composer::VERSION); 91 } 92 93 /** 94 * {@inheritDoc} 95 */ 96 public function run(InputInterface $input = null, OutputInterface $output = null) 97 { 98 if (null === $output) { 99 $styles = Factory::createAdditionalStyles(); 100 $formatter = new OutputFormatter(null, $styles); 101 $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, null, $formatter); 102 } 103 104 return parent::run($input, $output); 105 } 106 107 /** 108 * {@inheritDoc} 109 */ 110 public function doRun(InputInterface $input, OutputInterface $output) 111 { 112 $this->disablePluginsByDefault = $input->hasParameterOption('--no-plugins'); 113 114 $io = $this->io = new ConsoleIO($input, $output, $this->getHelperSet()); 115 ErrorHandler::register($io); 116 117 // switch working dir 118 if ($newWorkDir = $this->getNewWorkingDir($input)) { 119 $oldWorkingDir = getcwd(); 120 chdir($newWorkDir); 121 $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG); 122 } 123 124 // determine command name to be executed without including plugin commands 125 $commandName = ''; 126 if ($name = $this->getCommandName($input)) { 127 try { 128 $commandName = $this->find($name)->getName(); 129 } catch (\InvalidArgumentException $e) { 130 } 131 } 132 133 if (!$this->disablePluginsByDefault && !$this->hasPluginCommands && 'global' !== $commandName) { 134 try { 135 foreach ($this->getPluginCommands() as $command) { 136 if ($this->has($command->getName())) { 137 $io->writeError('<warning>Plugin command '.$command->getName().' ('.get_class($command).') would override a Composer command and has been skipped</warning>'); 138 } else { 139 $this->add($command); 140 } 141 } 142 } catch (NoSslException $e) { 143 // suppress these as they are not relevant at this point 144 } 145 146 $this->hasPluginCommands = true; 147 } 148 149 // determine command name to be executed incl plugin commands, and check if it's a proxy command 150 $isProxyCommand = false; 151 if ($name = $this->getCommandName($input)) { 152 try { 153 $command = $this->find($name); 154 $commandName = $command->getName(); 155 $isProxyCommand = ($command instanceof Command\BaseCommand && $command->isProxyCommand()); 156 } catch (\InvalidArgumentException $e) { 157 } 158 } 159 160 if (!$isProxyCommand) { 161 $io->writeError(sprintf( 162 'Running %s (%s) with %s on %s', 163 Composer::VERSION, 164 Composer::RELEASE_DATE, 165 defined('HHVM_VERSION') ? 'HHVM '.HHVM_VERSION : 'PHP '.PHP_VERSION, 166 php_uname('s') . ' / ' . php_uname('r') 167 ), true, IOInterface::DEBUG); 168 169 if (PHP_VERSION_ID < 50302) { 170 $io->writeError('<warning>Composer only officially supports PHP 5.3.2 and above, you will most likely encounter problems with your PHP '.PHP_VERSION.', upgrading is strongly recommended.</warning>'); 171 } 172 173 if (extension_loaded('xdebug') && !getenv('COMPOSER_DISABLE_XDEBUG_WARN')) { 174 $io->writeError('<warning>You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug</warning>'); 175 } 176 177 if (defined('COMPOSER_DEV_WARNING_TIME') && $commandName !== 'self-update' && $commandName !== 'selfupdate' && time() > COMPOSER_DEV_WARNING_TIME) { 178 $io->writeError(sprintf('<warning>Warning: This development build of composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.</warning>', $_SERVER['PHP_SELF'])); 179 } 180 181 if (getenv('COMPOSER_NO_INTERACTION')) { 182 $input->setInteractive(false); 183 } 184 185 if (!Platform::isWindows() && function_exists('exec') && !getenv('COMPOSER_ALLOW_SUPERUSER')) { 186 if (function_exists('posix_getuid') && posix_getuid() === 0) { 187 if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { 188 $io->writeError('<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>'); 189 } 190 if ($uid = (int) getenv('SUDO_UID')) { 191 // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on 192 // ref. https://github.com/composer/composer/issues/5119 193 Silencer::call('exec', "sudo -u \\#{$uid} sudo -K > /dev/null 2>&1"); 194 } 195 } 196 // Silently clobber any remaining sudo leases on the current user as well to avoid privilege escalations 197 Silencer::call('exec', 'sudo -K > /dev/null 2>&1'); 198 } 199 200 // Check system temp folder for usability as it can cause weird runtime issues otherwise 201 Silencer::call(function () use ($io) { 202 $tempfile = sys_get_temp_dir() . '/temp-' . md5(microtime()); 203 if (!(file_put_contents($tempfile, __FILE__) && (file_get_contents($tempfile) == __FILE__) && unlink($tempfile) && !file_exists($tempfile))) { 204 $io->writeError(sprintf('<error>PHP temp directory (%s) does not exist or is not writable to Composer. Set sys_temp_dir in your php.ini</error>', sys_get_temp_dir())); 205 } 206 }); 207 208 // add non-standard scripts as own commands 209 $file = Factory::getComposerFile(); 210 if (is_file($file) && is_readable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { 211 if (isset($composer['scripts']) && is_array($composer['scripts'])) { 212 foreach ($composer['scripts'] as $script => $dummy) { 213 if (!defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { 214 if ($this->has($script)) { 215 $io->writeError('<warning>A script named '.$script.' would override a Composer command and has been skipped</warning>'); 216 } else { 217 $this->add(new Command\ScriptAliasCommand($script)); 218 } 219 } 220 } 221 } 222 } 223 } 224 225 try { 226 if ($input->hasParameterOption('--profile')) { 227 $startTime = microtime(true); 228 $this->io->enableDebugging($startTime); 229 } 230 231 $result = parent::doRun($input, $output); 232 233 if (isset($oldWorkingDir)) { 234 chdir($oldWorkingDir); 235 } 236 237 if (isset($startTime)) { 238 $io->writeError('<info>Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MB), time: '.round(microtime(true) - $startTime, 2).'s'); 239 } 240 241 restore_error_handler(); 242 243 return $result; 244 } catch (ScriptExecutionException $e) { 245 return $e->getCode(); 246 } catch (\Exception $e) { 247 $this->hintCommonErrors($e); 248 restore_error_handler(); 249 throw $e; 250 } 251 } 252 253 /** 254 * @param InputInterface $input 255 * @throws \RuntimeException 256 * @return string 257 */ 258 private function getNewWorkingDir(InputInterface $input) 259 { 260 $workingDir = $input->getParameterOption(array('--working-dir', '-d')); 261 if (false !== $workingDir && !is_dir($workingDir)) { 262 throw new \RuntimeException('Invalid working directory specified, '.$workingDir.' does not exist.'); 263 } 264 265 return $workingDir; 266 } 267 268 /** 269 * {@inheritDoc} 270 */ 271 private function hintCommonErrors($exception) 272 { 273 $io = $this->getIO(); 274 275 Silencer::suppress(); 276 try { 277 $composer = $this->getComposer(false, true); 278 if ($composer) { 279 $config = $composer->getConfig(); 280 281 $minSpaceFree = 1024 * 1024; 282 if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) 283 || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) 284 || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) 285 ) { 286 $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>', true, IOInterface::QUIET); 287 } 288 } 289 } catch (\Exception $e) { 290 } 291 Silencer::restore(); 292 293 if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { 294 $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>', true, IOInterface::QUIET); 295 $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details</error>', true, IOInterface::QUIET); 296 } 297 298 if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) { 299 $io->writeError('<error>The following exception is caused by a lack of memory or swap, or not having swap configured</error>', true, IOInterface::QUIET); 300 $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>', true, IOInterface::QUIET); 301 } 302 } 303 304 /** 305 * @param bool $required 306 * @param bool|null $disablePlugins 307 * @throws JsonValidationException 308 * @return \Composer\Composer 309 */ 310 public function getComposer($required = true, $disablePlugins = null) 311 { 312 if (null === $disablePlugins) { 313 $disablePlugins = $this->disablePluginsByDefault; 314 } 315 316 if (null === $this->composer) { 317 try { 318 $this->composer = Factory::create($this->io, null, $disablePlugins); 319 } catch (\InvalidArgumentException $e) { 320 if ($required) { 321 $this->io->writeError($e->getMessage()); 322 exit(1); 323 } 324 } catch (JsonValidationException $e) { 325 $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); 326 $message = $e->getMessage() . ':' . PHP_EOL . $errors; 327 throw new JsonValidationException($message); 328 } 329 } 330 331 return $this->composer; 332 } 333 334 /** 335 * Removes the cached composer instance 336 */ 337 public function resetComposer() 338 { 339 $this->composer = null; 340 } 341 342 /** 343 * @return IOInterface 344 */ 345 public function getIO() 346 { 347 return $this->io; 348 } 349 350 public function getHelp() 351 { 352 return self::$logo . parent::getHelp(); 353 } 354 355 /** 356 * Initializes all the composer commands. 357 */ 358 protected function getDefaultCommands() 359 { 360 $commands = array_merge(parent::getDefaultCommands(), array( 361 new Command\AboutCommand(), 362 new Command\ConfigCommand(), 363 new Command\DependsCommand(), 364 new Command\ProhibitsCommand(), 365 new Command\InitCommand(), 366 new Command\InstallCommand(), 367 new Command\CreateProjectCommand(), 368 new Command\UpdateCommand(), 369 new Command\SearchCommand(), 370 new Command\ValidateCommand(), 371 new Command\ShowCommand(), 372 new Command\SuggestsCommand(), 373 new Command\RequireCommand(), 374 new Command\DumpAutoloadCommand(), 375 new Command\StatusCommand(), 376 new Command\ArchiveCommand(), 377 new Command\DiagnoseCommand(), 378 new Command\RunScriptCommand(), 379 new Command\LicensesCommand(), 380 new Command\GlobalCommand(), 381 new Command\ClearCacheCommand(), 382 new Command\RemoveCommand(), 383 new Command\HomeCommand(), 384 new Command\ExecCommand(), 385 new Command\OutdatedCommand(), 386 )); 387 388 if ('phar:' === substr(__FILE__, 0, 5)) { 389 $commands[] = new Command\SelfUpdateCommand(); 390 } 391 392 return $commands; 393 } 394 395 /** 396 * {@inheritDoc} 397 */ 398 public function getLongVersion() 399 { 400 if (Composer::BRANCH_ALIAS_VERSION) { 401 return sprintf( 402 '<info>%s</info> version <comment>%s (%s)</comment> %s', 403 $this->getName(), 404 Composer::BRANCH_ALIAS_VERSION, 405 $this->getVersion(), 406 Composer::RELEASE_DATE 407 ); 408 } 409 410 return parent::getLongVersion() . ' ' . Composer::RELEASE_DATE; 411 } 412 413 /** 414 * {@inheritDoc} 415 */ 416 protected function getDefaultInputDefinition() 417 { 418 $definition = parent::getDefaultInputDefinition(); 419 $definition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Display timing and memory usage information')); 420 $definition->addOption(new InputOption('--no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.')); 421 $definition->addOption(new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.')); 422 423 return $definition; 424 } 425 426 private function getPluginCommands() 427 { 428 $commands = array(); 429 430 $composer = $this->getComposer(false, false); 431 if (null === $composer) { 432 $composer = Factory::createGlobal($this->io, false); 433 } 434 435 if (null !== $composer) { 436 $pm = $composer->getPluginManager(); 437 foreach ($pm->getPluginCapabilities('Composer\Plugin\Capability\CommandProvider', array('composer' => $composer, 'io' => $this->io)) as $capability) { 438 $newCommands = $capability->getCommands(); 439 if (!is_array($newCommands)) { 440 throw new \UnexpectedValueException('Plugin capability '.get_class($capability).' failed to return an array from getCommands'); 441 } 442 foreach ($newCommands as $command) { 443 if (!$command instanceof Command\BaseCommand) { 444 throw new \UnexpectedValueException('Plugin capability '.get_class($capability).' returned an invalid value, we expected an array of Composer\Command\BaseCommand objects'); 445 } 446 } 447 $commands = array_merge($commands, $newCommands); 448 } 449 } 450 451 return $commands; 452 } 453} 454