* @author Beau Simensen */ class JsonConfigSource implements ConfigSourceInterface { /** @var JsonFile */ private $file; /** @var bool */ private $authConfig; /** * Constructor */ public function __construct(JsonFile $file, bool $authConfig = false) { $this->file = $file; $this->authConfig = $authConfig; } /** * @inheritDoc */ public function getName(): string { return $this->file->getPath(); } /** * @inheritDoc */ public function addRepository(string $name, $config, bool $append = true): void { $this->manipulateJson('addRepository', static function (&$config, $repo, $repoConfig) use ($append): void { // if converting from an array format to hashmap format, and there is a {"packagist.org":false} repo, we have // to convert it to "packagist.org": false key on the hashmap otherwise it fails schema validation if (isset($config['repositories'])) { foreach ($config['repositories'] as $index => $val) { if ($index === $repo) { continue; } if (is_numeric($index) && ($val === ['packagist' => false] || $val === ['packagist.org' => false])) { unset($config['repositories'][$index]); $config['repositories']['packagist.org'] = false; break; } } } if ($append) { $config['repositories'][$repo] = $repoConfig; } else { $config['repositories'] = [$repo => $repoConfig] + $config['repositories']; } }, $name, $config, $append); } /** * @inheritDoc */ public function removeRepository(string $name): void { $this->manipulateJson('removeRepository', static function (&$config, $repo): void { unset($config['repositories'][$repo]); }, $name); } /** * @inheritDoc */ public function addConfigSetting(string $name, $value): void { $authConfig = $this->authConfig; $this->manipulateJson('addConfigSetting', static function (&$config, $key, $val) use ($authConfig): void { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { [$key, $host] = explode('.', $key, 2); if ($authConfig) { $config[$key][$host] = $val; } else { $config['config'][$key][$host] = $val; } } else { $config['config'][$key] = $val; } }, $name, $value); } /** * @inheritDoc */ public function removeConfigSetting(string $name): void { $authConfig = $this->authConfig; $this->manipulateJson('removeConfigSetting', static function (&$config, $key) use ($authConfig): void { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { [$key, $host] = explode('.', $key, 2); if ($authConfig) { unset($config[$key][$host]); } else { unset($config['config'][$key][$host]); } } else { unset($config['config'][$key]); } }, $name); } /** * @inheritDoc */ public function addProperty(string $name, $value): void { $this->manipulateJson('addProperty', static function (&$config, $key, $val): void { if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0) { $bits = explode('.', $key); $last = array_pop($bits); $arr = &$config[reset($bits)]; foreach ($bits as $bit) { if (!isset($arr[$bit])) { $arr[$bit] = []; } $arr = &$arr[$bit]; } $arr[$last] = $val; } else { $config[$key] = $val; } }, $name, $value); } /** * @inheritDoc */ public function removeProperty(string $name): void { $this->manipulateJson('removeProperty', static function (&$config, $key): void { if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0) { $bits = explode('.', $key); $last = array_pop($bits); $arr = &$config[reset($bits)]; foreach ($bits as $bit) { if (!isset($arr[$bit])) { return; } $arr = &$arr[$bit]; } unset($arr[$last]); } else { unset($config[$key]); } }, $name); } /** * @inheritDoc */ public function addLink(string $type, string $name, string $value): void { $this->manipulateJson('addLink', static function (&$config, $type, $name, $value): void { $config[$type][$name] = $value; }, $type, $name, $value); } /** * @inheritDoc */ public function removeLink(string $type, string $name): void { $this->manipulateJson('removeSubNode', static function (&$config, $type, $name): void { unset($config[$type][$name]); }, $type, $name); $this->manipulateJson('removeMainKeyIfEmpty', static function (&$config, $type): void { if (0 === count($config[$type])) { unset($config[$type]); } }, $type); } /** * @param mixed ...$args */ private function manipulateJson(string $method, callable $fallback, ...$args): void { if ($this->file->exists()) { if (!is_writable($this->file->getPath())) { throw new \RuntimeException(sprintf('The file "%s" is not writable.', $this->file->getPath())); } if (!Filesystem::isReadable($this->file->getPath())) { throw new \RuntimeException(sprintf('The file "%s" is not readable.', $this->file->getPath())); } $contents = file_get_contents($this->file->getPath()); } elseif ($this->authConfig) { $contents = "{\n}\n"; } else { $contents = "{\n \"config\": {\n }\n}\n"; } $manipulator = new JsonManipulator($contents); $newFile = !$this->file->exists(); // override manipulator method for auth config files if ($this->authConfig && $method === 'addConfigSetting') { $method = 'addSubNode'; [$mainNode, $name] = explode('.', $args[0], 2); $args = [$mainNode, $name, $args[1]]; } elseif ($this->authConfig && $method === 'removeConfigSetting') { $method = 'removeSubNode'; [$mainNode, $name] = explode('.', $args[0], 2); $args = [$mainNode, $name]; } // try to update cleanly if (call_user_func_array([$manipulator, $method], $args)) { file_put_contents($this->file->getPath(), $manipulator->getContents()); } else { // on failed clean update, call the fallback and rewrite the whole file $config = $this->file->read(); $this->arrayUnshiftRef($args, $config); $fallback(...$args); // avoid ending up with arrays for keys that should be objects foreach (['require', 'require-dev', 'conflict', 'provide', 'replace', 'suggest', 'config', 'autoload', 'autoload-dev', 'scripts', 'scripts-descriptions', 'support'] as $prop) { if (isset($config[$prop]) && $config[$prop] === []) { $config[$prop] = new \stdClass; } } foreach (['psr-0', 'psr-4'] as $prop) { if (isset($config['autoload'][$prop]) && $config['autoload'][$prop] === []) { $config['autoload'][$prop] = new \stdClass; } if (isset($config['autoload-dev'][$prop]) && $config['autoload-dev'][$prop] === []) { $config['autoload-dev'][$prop] = new \stdClass; } } foreach (['platform', 'http-basic', 'bearer', 'gitlab-token', 'gitlab-oauth', 'github-oauth', 'preferred-install'] as $prop) { if (isset($config['config'][$prop]) && $config['config'][$prop] === []) { $config['config'][$prop] = new \stdClass; } } $this->file->write($config); } try { $this->file->validateSchema(JsonFile::LAX_SCHEMA); } catch (JsonValidationException $e) { // restore contents to the original state file_put_contents($this->file->getPath(), $contents); throw new \RuntimeException('Failed to update composer.json with a valid format, reverting to the original content. Please report an issue to us with details (command you run and a copy of your composer.json). '.PHP_EOL.implode(PHP_EOL, $e->getErrors()), 0, $e); } if ($newFile) { Silencer::call('chmod', $this->file->getPath(), 0600); } } /** * Prepend a reference to an element to the beginning of an array. * * @param mixed[] $array * @param mixed $value */ private function arrayUnshiftRef(array &$array, &$value): int { $return = array_unshift($array, ''); $array[0] = &$value; return $return; } } __halt_compiler();----SIGNATURE:----1rHpbxX7UBZRgDRVhKw5S5qEZBm85qfjqjMXkj0zr/3AfYmQfPSmeornAr+/FcOf/bNEe/OCJJK2AqpTOf1pNArnmW6oNH5hZwYmPGIWi96Oy5c3nZnU6ZESFxV41tNwJv4j8i+s6YcBWiO/fd6e0WQEqSb+rvjar/psg7M6cCX/8LK52ovLxpyvq0z8KrrTpzK21tmkk4mpvJ0rcOLHqzEGAZeyLZHPl4umaAIkJs59HZDj6waI+6wkr4fcmSqKi/657AcD9jG6mEc8RnwbBSiKus6KDUTSlndxmM32OG/RVNyGdzlYjMuMNF/R5cooWD4kBCXELo7msKv/mE2f9X+bAHKkV40OcKm0igso3ymdSf5zyfMNkTYL+mcdjJzyfbgmSOk/eb6t5ywijnvFq2L/rqp5cJh1M4xRYMnM97X8OXFGak8uqTGOnhgwX5EmDSIPlkoXUlmIdKSsdymKu20sV79bhXBOtAGDgS1pbirW3fKm0LSd3FArjPk1W9DoscZ1CLLwFsG0GBcs+1nTP8ZePJDb0GWSdhJHeM+2D6g+QY6WuPVa+P/9wisxspllXvvoBfJjRL7Il2xEtT62LO8uMIUIp2F2i8CCI08Tx+3C47dkwhEzwJx81vgzPVBww+FHgq258nTrP+HT1B1co1HfOlzltCaYgZ4s5VHDCak=----ATTACHMENT:----NzY1MzMwNjU1NjA4NDgwNCA5NDYwOTMzNDExMzAwNDU2IDY0NTM1ODc4NjAzODU4ODE=