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; 14 15use Composer\Config\ConfigSourceInterface; 16use Composer\Downloader\TransportException; 17use Composer\IO\IOInterface; 18use Composer\Util\Platform; 19 20/** 21 * @author Jordi Boggiano <j.boggiano@seld.be> 22 */ 23class Config 24{ 25 const RELATIVE_PATHS = 1; 26 27 public static $defaultConfig = array( 28 'process-timeout' => 300, 29 'use-include-path' => false, 30 'preferred-install' => 'auto', 31 'notify-on-install' => true, 32 'github-protocols' => array('https', 'ssh', 'git'), 33 'vendor-dir' => 'vendor', 34 'bin-dir' => '{$vendor-dir}/bin', 35 'cache-dir' => '{$home}/cache', 36 'data-dir' => '{$home}', 37 'cache-files-dir' => '{$cache-dir}/files', 38 'cache-repo-dir' => '{$cache-dir}/repo', 39 'cache-vcs-dir' => '{$cache-dir}/vcs', 40 'cache-ttl' => 15552000, // 6 months 41 'cache-files-ttl' => null, // fallback to cache-ttl 42 'cache-files-maxsize' => '300MiB', 43 'bin-compat' => 'auto', 44 'discard-changes' => false, 45 'autoloader-suffix' => null, 46 'sort-packages' => false, 47 'optimize-autoloader' => false, 48 'classmap-authoritative' => false, 49 'prepend-autoloader' => true, 50 'github-domains' => array('github.com'), 51 'bitbucket-expose-hostname' => true, 52 'disable-tls' => false, 53 'secure-http' => true, 54 'cafile' => null, 55 'capath' => null, 56 'github-expose-hostname' => true, 57 'gitlab-domains' => array('gitlab.com'), 58 'store-auths' => 'prompt', 59 'platform' => array(), 60 'archive-format' => 'tar', 61 'archive-dir' => '.', 62 // valid keys without defaults (auth config stuff): 63 // bitbucket-oauth 64 // github-oauth 65 // gitlab-oauth 66 // http-basic 67 ); 68 69 public static $defaultRepositories = array( 70 'packagist.org' => array( 71 'type' => 'composer', 72 'url' => 'https?://packagist.org', 73 'allow_ssl_downgrade' => true, 74 ), 75 ); 76 77 private $config; 78 private $baseDir; 79 private $repositories; 80 private $configSource; 81 private $authConfigSource; 82 private $useEnvironment; 83 private $warnedHosts = array(); 84 85 /** 86 * @param bool $useEnvironment Use COMPOSER_ environment variables to replace config settings 87 * @param string $baseDir Optional base directory of the config 88 */ 89 public function __construct($useEnvironment = true, $baseDir = null) 90 { 91 // load defaults 92 $this->config = static::$defaultConfig; 93 $this->repositories = static::$defaultRepositories; 94 $this->useEnvironment = (bool) $useEnvironment; 95 $this->baseDir = $baseDir; 96 } 97 98 public function setConfigSource(ConfigSourceInterface $source) 99 { 100 $this->configSource = $source; 101 } 102 103 public function getConfigSource() 104 { 105 return $this->configSource; 106 } 107 108 public function setAuthConfigSource(ConfigSourceInterface $source) 109 { 110 $this->authConfigSource = $source; 111 } 112 113 public function getAuthConfigSource() 114 { 115 return $this->authConfigSource; 116 } 117 118 /** 119 * Merges new config values with the existing ones (overriding) 120 * 121 * @param array $config 122 */ 123 public function merge($config) 124 { 125 // override defaults with given config 126 if (!empty($config['config']) && is_array($config['config'])) { 127 foreach ($config['config'] as $key => $val) { 128 if (in_array($key, array('bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'http-basic')) && isset($this->config[$key])) { 129 $this->config[$key] = array_merge($this->config[$key], $val); 130 } elseif ('preferred-install' === $key && isset($this->config[$key])) { 131 if (is_array($val) || is_array($this->config[$key])) { 132 if (is_string($val)) { 133 $val = array('*' => $val); 134 } 135 if (is_string($this->config[$key])) { 136 $this->config[$key] = array('*' => $this->config[$key]); 137 } 138 $this->config[$key] = array_merge($this->config[$key], $val); 139 // the full match pattern needs to be last 140 if (isset($this->config[$key]['*'])) { 141 $wildcard = $this->config[$key]['*']; 142 unset($this->config[$key]['*']); 143 $this->config[$key]['*'] = $wildcard; 144 } 145 } else { 146 $this->config[$key] = $val; 147 } 148 } else { 149 $this->config[$key] = $val; 150 } 151 } 152 } 153 154 if (!empty($config['repositories']) && is_array($config['repositories'])) { 155 $this->repositories = array_reverse($this->repositories, true); 156 $newRepos = array_reverse($config['repositories'], true); 157 foreach ($newRepos as $name => $repository) { 158 // disable a repository by name 159 if (false === $repository) { 160 $this->disableRepoByName($name); 161 continue; 162 } 163 164 // disable a repository with an anonymous {"name": false} repo 165 if (is_array($repository) && 1 === count($repository) && false === current($repository)) { 166 $this->disableRepoByName(key($repository)); 167 continue; 168 } 169 170 // store repo 171 if (is_int($name)) { 172 $this->repositories[] = $repository; 173 } else { 174 if ($name === 'packagist') { // BC support for default "packagist" named repo 175 $this->repositories[$name . '.org'] = $repository; 176 } else { 177 $this->repositories[$name] = $repository; 178 } 179 } 180 } 181 $this->repositories = array_reverse($this->repositories, true); 182 } 183 } 184 185 /** 186 * @return array 187 */ 188 public function getRepositories() 189 { 190 return $this->repositories; 191 } 192 193 /** 194 * Returns a setting 195 * 196 * @param string $key 197 * @param int $flags Options (see class constants) 198 * @throws \RuntimeException 199 * @return mixed 200 */ 201 public function get($key, $flags = 0) 202 { 203 switch ($key) { 204 case 'vendor-dir': 205 case 'bin-dir': 206 case 'process-timeout': 207 case 'data-dir': 208 case 'cache-dir': 209 case 'cache-files-dir': 210 case 'cache-repo-dir': 211 case 'cache-vcs-dir': 212 case 'cafile': 213 case 'capath': 214 // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config 215 $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); 216 217 $val = $this->getComposerEnv($env); 218 $val = rtrim($this->process(false !== $val ? $val : $this->config[$key], $flags), '/\\'); 219 $val = Platform::expandPath($val); 220 221 if (substr($key, -4) !== '-dir') { 222 return $val; 223 } 224 225 return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val); 226 227 case 'cache-ttl': 228 return (int) $this->config[$key]; 229 230 case 'cache-files-maxsize': 231 if (!preg_match('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $this->config[$key], $matches)) { 232 throw new \RuntimeException( 233 "Could not parse the value of 'cache-files-maxsize': {$this->config[$key]}" 234 ); 235 } 236 $size = $matches[1]; 237 if (isset($matches[2])) { 238 switch (strtolower($matches[2])) { 239 case 'g': 240 $size *= 1024; 241 // intentional fallthrough 242 case 'm': 243 $size *= 1024; 244 // intentional fallthrough 245 case 'k': 246 $size *= 1024; 247 break; 248 } 249 } 250 251 return $size; 252 253 case 'cache-files-ttl': 254 if (isset($this->config[$key])) { 255 return (int) $this->config[$key]; 256 } 257 258 return (int) $this->config['cache-ttl']; 259 260 case 'home': 261 $val = preg_replace('#^(\$HOME|~)(/|$)#', rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '/\\') . '/', $this->config[$key]); 262 263 return rtrim($this->process($val, $flags), '/\\'); 264 265 case 'bin-compat': 266 $value = $this->getComposerEnv('COMPOSER_BIN_COMPAT') ?: $this->config[$key]; 267 268 if (!in_array($value, array('auto', 'full'))) { 269 throw new \RuntimeException( 270 "Invalid value for 'bin-compat': {$value}. Expected auto, full" 271 ); 272 } 273 274 return $value; 275 276 case 'discard-changes': 277 if ($env = $this->getComposerEnv('COMPOSER_DISCARD_CHANGES')) { 278 if (!in_array($env, array('stash', 'true', 'false', '1', '0'), true)) { 279 throw new \RuntimeException( 280 "Invalid value for COMPOSER_DISCARD_CHANGES: {$env}. Expected 1, 0, true, false or stash" 281 ); 282 } 283 if ('stash' === $env) { 284 return 'stash'; 285 } 286 287 // convert string value to bool 288 return $env !== 'false' && (bool) $env; 289 } 290 291 if (!in_array($this->config[$key], array(true, false, 'stash'), true)) { 292 throw new \RuntimeException( 293 "Invalid value for 'discard-changes': {$this->config[$key]}. Expected true, false or stash" 294 ); 295 } 296 297 return $this->config[$key]; 298 299 case 'github-protocols': 300 $protos = $this->config['github-protocols']; 301 if ($this->config['secure-http'] && false !== ($index = array_search('git', $protos))) { 302 unset($protos[$index]); 303 } 304 if (reset($protos) === 'http') { 305 throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https", "git" or "ssh"'); 306 } 307 308 return $protos; 309 310 case 'disable-tls': 311 return $this->config[$key] !== 'false' && (bool) $this->config[$key]; 312 313 case 'secure-http': 314 return $this->config[$key] !== 'false' && (bool) $this->config[$key]; 315 316 default: 317 if (!isset($this->config[$key])) { 318 return null; 319 } 320 321 return $this->process($this->config[$key], $flags); 322 } 323 } 324 325 public function all($flags = 0) 326 { 327 $all = array( 328 'repositories' => $this->getRepositories(), 329 ); 330 foreach (array_keys($this->config) as $key) { 331 $all['config'][$key] = $this->get($key, $flags); 332 } 333 334 return $all; 335 } 336 337 public function raw() 338 { 339 return array( 340 'repositories' => $this->getRepositories(), 341 'config' => $this->config, 342 ); 343 } 344 345 /** 346 * Checks whether a setting exists 347 * 348 * @param string $key 349 * @return bool 350 */ 351 public function has($key) 352 { 353 return array_key_exists($key, $this->config); 354 } 355 356 /** 357 * Replaces {$refs} inside a config string 358 * 359 * @param string $value a config string that can contain {$refs-to-other-config} 360 * @param int $flags Options (see class constants) 361 * @return string 362 */ 363 private function process($value, $flags) 364 { 365 $config = $this; 366 367 if (!is_string($value)) { 368 return $value; 369 } 370 371 return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config, $flags) { 372 return $config->get($match[1], $flags); 373 }, $value); 374 } 375 376 /** 377 * Turns relative paths in absolute paths without realpath() 378 * 379 * Since the dirs might not exist yet we can not call realpath or it will fail. 380 * 381 * @param string $path 382 * @return string 383 */ 384 private function realpath($path) 385 { 386 if (preg_match('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) { 387 return $path; 388 } 389 390 return $this->baseDir . '/' . $path; 391 } 392 393 /** 394 * Reads the value of a Composer environment variable 395 * 396 * This should be used to read COMPOSER_ environment variables 397 * that overload config values. 398 * 399 * @param string $var 400 * @return string|bool 401 */ 402 private function getComposerEnv($var) 403 { 404 if ($this->useEnvironment) { 405 return getenv($var); 406 } 407 408 return false; 409 } 410 411 private function disableRepoByName($name) 412 { 413 if (isset($this->repositories[$name])) { 414 unset($this->repositories[$name]); 415 } else if ($name === 'packagist') { // BC support for default "packagist" named repo 416 unset($this->repositories['packagist.org']); 417 } 418 } 419 420 /** 421 * Validates that the passed URL is allowed to be used by current config, or throws an exception. 422 * 423 * @param string $url 424 * @param IOInterface $io 425 */ 426 public function prohibitUrlByConfig($url, IOInterface $io = null) 427 { 428 // Return right away if the URL is malformed or custom (see issue #5173) 429 if (false === filter_var($url, FILTER_VALIDATE_URL)) { 430 return; 431 } 432 433 // Extract scheme and throw exception on known insecure protocols 434 $scheme = parse_url($url, PHP_URL_SCHEME); 435 if (in_array($scheme, array('http', 'git', 'ftp', 'svn'))) { 436 if ($this->get('secure-http')) { 437 throw new TransportException("Your configuration does not allow connections to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details."); 438 } elseif ($io) { 439 $host = parse_url($url, PHP_URL_HOST); 440 if (!isset($this->warnedHosts[$host])) { 441 $io->writeError("<warning>Warning: Accessing $host over $scheme which is an insecure protocol.</warning>"); 442 } 443 $this->warnedHosts[$host] = true; 444 } 445 } 446 } 447} 448