ConfigCommand.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Command;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Input\InputArgument;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use Symfony\Component\Console\Output\OutputInterface;
  16. use JsonSchema\Validator;
  17. use Composer\Config;
  18. use Composer\Factory;
  19. use Composer\Json\JsonFile;
  20. use Composer\Json\JsonValidationException;
  21. /**
  22. * @author Joshua Estes <Joshua.Estes@iostudio.com>
  23. * @author Jordi Boggiano <j.boggiano@seld.be>
  24. */
  25. class ConfigCommand extends Command
  26. {
  27. /**
  28. * @var Composer\Json\JsonFile
  29. */
  30. protected $configFile;
  31. /**
  32. * {@inheritDoc}
  33. */
  34. protected function configure()
  35. {
  36. $this
  37. ->setName('config')
  38. ->setDescription('Set config options')
  39. ->setDefinition(array(
  40. new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'),
  41. new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'),
  42. new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'),
  43. new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'),
  44. new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'),
  45. new InputArgument('setting-key', null, 'Setting key'),
  46. new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'),
  47. ))
  48. ->setHelp(<<<EOT
  49. This command allows you to edit some basic composer settings in either the
  50. local composer.json file or the global config.json file.
  51. To edit the global config.json file:
  52. <comment>php composer.phar --global</comment>
  53. To add a repository:
  54. <comment>php composer.phar repositories.foo vcs http://bar.com</comment>
  55. You can add a repository to the global config.json file by passing in the
  56. <info>--global</info> option.
  57. To edit the file in an external editor:
  58. <comment>php composer.phar --edit</comment>
  59. To choose your editor you can set the "EDITOR" env variable.
  60. To get a list of configuration values in the file:
  61. <comment>php composer.phar --list</comment>
  62. You can always pass more than one option. As an example, if you want to edit the
  63. global config.json file.
  64. <comment>php composer.phar --edit --global</comment>
  65. EOT
  66. )
  67. ;
  68. }
  69. /**
  70. * {@inheritDoc}
  71. */
  72. protected function initialize(InputInterface $input, OutputInterface $output)
  73. {
  74. if ($input->getOption('global') && 'composer.json' !== $input->getOption('file')) {
  75. throw new \RuntimeException('--file and --global can not be combined');
  76. }
  77. // Get the local composer.json, global config.json, or if the user
  78. // passed in a file to use
  79. $this->configFile = $input->getOption('global')
  80. ? (Factory::createConfig()->get('home') . '/config.json')
  81. : $input->getOption('file');
  82. $this->configFile = new JsonFile($this->configFile);
  83. if (!$this->configFile->exists()) {
  84. touch($this->configFile->getPath());
  85. // If you read an empty file, Composer throws an error
  86. // Toss some of the defaults in there
  87. $defaults = Config::$defaultConfig;
  88. $defaults['repositories'] = Config::$defaultRepositories;
  89. $this->configFile->write($defaults);
  90. }
  91. }
  92. /**
  93. * {@inheritDoc}
  94. */
  95. protected function execute(InputInterface $input, OutputInterface $output)
  96. {
  97. // Open file in editor
  98. if ($input->getOption('editor')) {
  99. $editor = getenv('EDITOR');
  100. if (!$editor) {
  101. $editor = defined('PHP_WINDOWS_VERSION_BUILD') ? 'notepad' : 'vi';
  102. }
  103. system($editor . ' ' . $this->configFile->getPath() . (defined('PHP_WINDOWS_VERSION_BUILD') ? '': ' > `tty`'));
  104. return 0;
  105. }
  106. // List the configuration of the file settings
  107. if ($input->getOption('list')) {
  108. $this->displayFileContents($this->configFile->read(), $output);
  109. return 0;
  110. }
  111. if (!$input->getArgument('setting-key')) {
  112. return 0;
  113. }
  114. // If the user enters in a config variable, parse it and save to file
  115. if (array() !== $input->getArgument('setting-value') && $input->getOption('unset')) {
  116. throw new \RuntimeException('You can not combine a setting value with --unset');
  117. }
  118. if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) {
  119. throw new \RuntimeException('You must include a setting value or pass --unset to clear the value');
  120. }
  121. /**
  122. * The user needs the ability to add a repository with one command.
  123. * For example "config -g repository.foo 'vcs http://example.com'
  124. */
  125. $configSettings = $this->configFile->read(); // what is current in the config
  126. $values = $input->getArgument('setting-value'); // what the user is trying to add/change
  127. // handle repositories
  128. if (preg_match('/^repos?(?:itories)?\.(.+)/', $input->getArgument('setting-key'), $matches)) {
  129. if ($input->getOption('unset')) {
  130. unset($configSettings['repositories'][$matches[1]]);
  131. } else {
  132. $settingKey = 'repositories.'.$matches[1];
  133. if (2 !== count($values)) {
  134. throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com');
  135. }
  136. $setting = $this->parseSetting($settingKey, array(
  137. 'type' => $values[0],
  138. 'url' => $values[1],
  139. ));
  140. // Could there be a better way to do this?
  141. $configSettings = array_merge_recursive($configSettings, $setting);
  142. $this->validateSchema($configSettings);
  143. }
  144. } else {
  145. // handle config values
  146. $uniqueConfigValues = array(
  147. 'process-timeout' => array('is_numeric', 'intval'),
  148. 'vendor-dir' => array('is_string', function ($val) { return $val; }),
  149. 'bin-dir' => array('is_string', function ($val) { return $val; }),
  150. 'notify-on-install' => array(
  151. function ($val) { return true; },
  152. function ($val) { return $val !== 'false' && (bool) $val; }
  153. ),
  154. );
  155. $multiConfigValues = array(
  156. 'github-protocols' => array(
  157. function ($vals) {
  158. if (!is_array($vals)) {
  159. return 'array expected';
  160. }
  161. foreach ($vals as $val) {
  162. if (!in_array($val, array('git', 'https', 'http'))) {
  163. return 'valid protocols include: git, https, http';
  164. }
  165. }
  166. return true;
  167. },
  168. function ($vals) {
  169. return $vals;
  170. }
  171. ),
  172. );
  173. $settingKey = $input->getArgument('setting-key');
  174. foreach ($uniqueConfigValues as $name => $callbacks) {
  175. if ($settingKey === $name) {
  176. list($validator, $normalizer) = $callbacks;
  177. if ($input->getOption('unset')) {
  178. unset($configSettings['config'][$settingKey]);
  179. } else {
  180. if (1 !== count($values)) {
  181. throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300');
  182. }
  183. if (true !== $validation = $validator($values[0])) {
  184. throw new \RuntimeException(sprintf(
  185. '"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  186. $values[0]
  187. ));
  188. }
  189. $setting = $this->parseSetting('config.'.$settingKey, $normalizer($values[0]));
  190. $configSettings = array_merge($configSettings, $setting);
  191. $this->validateSchema($configSettings);
  192. }
  193. }
  194. }
  195. foreach ($multiConfigValues as $name => $callbacks) {
  196. if ($settingKey === $name) {
  197. list($validator, $normalizer) = $callbacks;
  198. if ($input->getOption('unset')) {
  199. unset($configSettings['config'][$settingKey]);
  200. } else {
  201. if (true !== $validation = $validator($values)) {
  202. throw new \RuntimeException(sprintf(
  203. '%s is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  204. json_encode($values)
  205. ));
  206. }
  207. $setting = $this->parseSetting('config.'.$settingKey, $normalizer($values));
  208. $configSettings = array_merge($configSettings, $setting);
  209. $this->validateSchema($configSettings);
  210. }
  211. }
  212. }
  213. }
  214. // clean up empty sections
  215. if (empty($configSettings['repositories'])) {
  216. unset($configSettings['repositories']);
  217. }
  218. if (empty($configSettings['config'])) {
  219. unset($configSettings['config']);
  220. }
  221. $this->configFile->write($configSettings);
  222. }
  223. /**
  224. * Display the contents of the file in a pretty formatted way
  225. *
  226. * @param array $contents
  227. * @param OutputInterface $output
  228. * @param string|null $k
  229. */
  230. protected function displayFileContents(array $contents, OutputInterface $output, $k = null)
  231. {
  232. // @todo Look into a way to refactor this code, as it is right now, I
  233. // don't like it, also the name of the function could be better
  234. foreach ($contents as $key => $value) {
  235. if (is_array($value)) {
  236. $k .= $key . '.';
  237. $this->displayFileContents($value, $output, $k);
  238. if (substr_count($k,'.') > 1) {
  239. $k = str_split($k,strrpos($k,'.',-2));
  240. $k = $k[0] . '.';
  241. } else {
  242. $k = null;
  243. }
  244. continue;
  245. }
  246. $output->writeln('[<comment>' . $k . $key . '</comment>] <info>' . $value . '</info>');
  247. }
  248. }
  249. /**
  250. * This function will take a setting key (a.b.c) and return an
  251. * array that matches this
  252. *
  253. * @param string $key
  254. * @param string $value
  255. * @return array
  256. */
  257. protected function parseSetting($key, $value)
  258. {
  259. $parts = array_reverse(explode('.', $key));
  260. $tmp = array();
  261. for ($i = 0; $i < count($parts); $i++) {
  262. $tmp[$parts[$i]] = (0 === $i) ? $value : $tmp;
  263. if (0 < $i) {
  264. unset($tmp[$parts[$i - 1]]);
  265. }
  266. }
  267. return $tmp;
  268. }
  269. /**
  270. * After the command sets a new config value, this will parse it writes
  271. * it to disk to make sure that it is valid according the the composer.json
  272. * schema.
  273. *
  274. * @param array $data
  275. * @throws JsonValidationException
  276. * @return boolean
  277. */
  278. protected function validateSchema(array $data)
  279. {
  280. // TODO Figure out what should be excluded from the validation check
  281. // TODO validation should vary based on if it's global or local
  282. $schemaFile = __DIR__ . '/../../../res/composer-schema.json';
  283. $schemaData = json_decode(file_get_contents($schemaFile));
  284. unset(
  285. $schemaData->properties->name,
  286. $schemaData->properties->description
  287. );
  288. $validator = new Validator();
  289. $validator->check(json_decode(json_encode($data)), $schemaData);
  290. if (!$validator->isValid()) {
  291. $errors = array();
  292. foreach ((array) $validator->getErrors() as $error) {
  293. $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message'];
  294. }
  295. throw new JsonValidationException('"'.$this->configFile->getPath().'" does not match the expected JSON schema'."\n". implode("\n",$errors));
  296. }
  297. return true;
  298. }
  299. }