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