*/ class JsonManipulator { /** @var string */ private const DEFINES = "(?(DEFINE)\n (? -? (?= [1-9]|0(?!\d) ) \d++ (?:\.\d++)? (?:[eE] [+-]?+ \d++)? )\n (? true | false | null )\n (? " (?:[^"\\]*+ | \\ ["\\bfnrt\/] | \\ u [0-9A-Fa-f]{4} )* " )\n (? \[ (?: (?&json) \s*+ (?: , (?&json) \s*+ )*+ )?+ \s*+ \] )\n (? \s*+ (?&string) \s*+ : (?&json) \s*+ )\n (? \{ (?: (?&pair) (?: , (?&pair) )*+ )?+ \s*+ \} )\n (? \s*+ (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) )\n )"; /** @var string */ private $contents; /** @var string */ private $newline; /** @var string */ private $indent; public function __construct(string $contents) { $contents = trim($contents); if ($contents === '') { $contents = '{}'; } if (!Preg::isMatch('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); } $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; $this->detectIndenting(); } public function getContents(): string { return $this->contents . $this->newline; } public function addLink(string $type, string $package, string $constraint, bool $sortPackages = false): bool { $decoded = JsonFile::parseJson($this->contents); // no link of that type yet if (!isset($decoded[$type])) { return $this->addMainKey($type, [$package => $constraint]); } $regex = '{'.self::DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($type)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; if (!Preg::isMatch($regex, $this->contents, $matches)) { return false; } assert(is_string($matches['start'])); assert(is_string($matches['value'])); assert(is_string($matches['end'])); $links = $matches['value']; // try to find existing link $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); $regex = '{'.self::DEFINES.'"(?P'.$packageRegex.')"(\s*:\s*)(?&string)}ix'; if (Preg::isMatch($regex, $links, $packageMatches)) { assert(is_string($packageMatches['package'])); // update existing link $existingPackage = $packageMatches['package']; $packageRegex = str_replace('/', '\\\\?/', preg_quote($existingPackage)); $links = Preg::replaceCallback('{'.self::DEFINES.'"'.$packageRegex.'"(?P\s*:\s*)(?&string)}ix', static function ($m) use ($existingPackage, $constraint): string { return JsonFile::encode(str_replace('\\/', '/', $existingPackage)) . $m['separator'] . '"' . $constraint . '"'; }, $links); } else { if (Preg::isMatchStrictGroups('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { // link missing but non empty links $links = Preg::replace( '{'.preg_quote($match[1]).'$}', // addcslashes is used to double up backslashes/$ since preg_replace resolves them as back references otherwise, see #1588 addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\$'), $links ); } else { // links empty $links = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . $this->indent . '}'; } } if (true === $sortPackages) { $requirements = json_decode($links, true); $this->sortPackages($requirements); $links = $this->format($requirements); } $this->contents = $matches['start'] . $matches['property'] . $links . $matches['end']; return true; } /** * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. * * @link https://getcomposer.org/doc/02-libraries.md#platform-packages * * @param array $packages */ private function sortPackages(array &$packages = []): void { $prefix = static function ($requirement): string { if (PlatformRepository::isPlatformPackage($requirement)) { return Preg::replace( [ '/^php/', '/^hhvm/', '/^ext/', '/^lib/', '/^\D/', ], [ '0-$0', '1-$0', '2-$0', '3-$0', '4-$0', ], $requirement ); } return '5-'.$requirement; }; uksort($packages, static function ($a, $b) use ($prefix): int { return strnatcmp($prefix($a), $prefix($b)); }); } /** * @param array|false $config */ public function addRepository(string $name, $config, bool $append = true): bool { return $this->addSubNode('repositories', $name, $config, $append); } public function removeRepository(string $name): bool { return $this->removeSubNode('repositories', $name); } /** * @param mixed $value */ public function addConfigSetting(string $name, $value): bool { return $this->addSubNode('config', $name, $value); } public function removeConfigSetting(string $name): bool { return $this->removeSubNode('config', $name); } /** * @param mixed $value */ public function addProperty(string $name, $value): bool { if (strpos($name, 'suggest.') === 0) { return $this->addSubNode('suggest', substr($name, 8), $value); } if (strpos($name, 'extra.') === 0) { return $this->addSubNode('extra', substr($name, 6), $value); } if (strpos($name, 'scripts.') === 0) { return $this->addSubNode('scripts', substr($name, 8), $value); } return $this->addMainKey($name, $value); } public function removeProperty(string $name): bool { if (strpos($name, 'suggest.') === 0) { return $this->removeSubNode('suggest', substr($name, 8)); } if (strpos($name, 'extra.') === 0) { return $this->removeSubNode('extra', substr($name, 6)); } if (strpos($name, 'scripts.') === 0) { return $this->removeSubNode('scripts', substr($name, 8)); } return $this->removeMainKey($name); } /** * @param mixed $value */ public function addSubNode(string $mainNode, string $name, $value, bool $append = true): bool { $decoded = JsonFile::parseJson($this->contents); $subName = null; if (in_array($mainNode, ['config', 'extra', 'scripts']) && false !== strpos($name, '.')) { [$name, $subName] = explode('.', $name, 2); } // no main node yet if (!isset($decoded[$mainNode])) { if ($subName !== null) { $this->addMainKey($mainNode, [$name => [$subName => $value]]); } else { $this->addMainKey($mainNode, [$name => $value]); } return true; } // main node content not match-able $nodeRegex = '{'.self::DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; try { if (!Preg::isMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } assert(is_string($match['start'])); assert(is_string($match['content'])); assert(is_string($match['end'])); $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children)) { return false; } // child exists $childRegex = '{'.self::DEFINES.'(?P"'.preg_quote($name).'"\s*:\s*)(?P(?&json))(?P,?)}x'; if (Preg::isMatch($childRegex, $children, $matches)) { $children = Preg::replaceCallback($childRegex, function ($matches) use ($subName, $value): string { if ($subName !== null && is_string($matches['content'])) { $curVal = json_decode($matches['content'], true); if (!is_array($curVal)) { $curVal = []; } $curVal[$subName] = $value; $value = $curVal; } return $matches['start'] . $this->format($value, 1) . $matches['end']; }, $children); } else { Preg::match('#^{ (?P\s*?) (?P\S+.*?)? (?P\s*) }$#sx', $children, $match); $whitespace = ''; if (!empty($match['trailingspace'])) { $whitespace = $match['trailingspace']; } if (!empty($match['content'])) { if ($subName !== null) { $value = [$subName => $value]; } // child missing but non empty children if ($append) { $children = Preg::replace( '#'.$whitespace.'}$#', addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}', '\\$'), $children ); } else { $whitespace = ''; if (!empty($match['leadingspace'])) { $whitespace = $match['leadingspace']; } $children = Preg::replace( '#^{'.$whitespace.'#', addcslashes('{' . $whitespace . JsonFile::encode($name).': '.$this->format($value, 1) . ',' . $this->newline . $this->indent . $this->indent, '\\$'), $children ); } } else { if ($subName !== null) { $value = [$subName => $value]; } // children present but empty $children = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}'; } } $this->contents = Preg::replaceCallback($nodeRegex, static function ($m) use ($children): string { return $m['start'] . $children . $m['end']; }, $this->contents); return true; } public function removeSubNode(string $mainNode, string $name): bool { $decoded = JsonFile::parseJson($this->contents); // no node or empty node if (empty($decoded[$mainNode])) { return true; } // no node content match-able $nodeRegex = '{'.self::DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; try { if (!Preg::isMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } assert(is_string($match['start'])); assert(is_string($match['content'])); assert(is_string($match['end'])); $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children, true)) { return false; } $subName = null; if (in_array($mainNode, ['config', 'extra', 'scripts']) && false !== strpos($name, '.')) { [$name, $subName] = explode('.', $name, 2); } // no node to remove if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { return true; } // try and find a match for the subkey $keyRegex = str_replace('/', '\\\\?/', preg_quote($name)); if (Preg::isMatch('{"'.$keyRegex.'"\s*:}i', $children)) { // find best match for the value of "name" if (Preg::isMatchAll('{'.self::DEFINES.'"'.$keyRegex.'"\s*:\s*(?:(?&json))}x', $children, $matches)) { $bestMatch = ''; foreach ($matches[0] as $match) { assert(is_string($match)); if (strlen($bestMatch) < strlen($match)) { $bestMatch = $match; } } $childrenClean = Preg::replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count); if (1 !== $count) { $childrenClean = Preg::replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $childrenClean, -1, $count); if (1 !== $count) { return false; } } } } else { $childrenClean = $children; } if (!isset($childrenClean)) { throw new \InvalidArgumentException("JsonManipulator: \$childrenClean is not defined. Please report at https://github.com/composer/composer/issues/new."); } // no child data left, $name was the only key in unset($match); Preg::match('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $childrenClean, $match); if (empty($match['content'])) { $newline = $this->newline; $indent = $this->indent; $this->contents = Preg::replaceCallback($nodeRegex, static function ($matches) use ($indent, $newline): string { return $matches['start'] . '{' . $newline . $indent . '}' . $matches['end']; }, $this->contents); // we have a subname, so we restore the rest of $name if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); $this->addSubNode($mainNode, $name, $curVal[$name]); } return true; } $this->contents = Preg::replaceCallback($nodeRegex, function ($matches) use ($name, $subName, $childrenClean): string { assert(is_string($matches['content'])); if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); $childrenClean = $this->format($curVal); } return $matches['start'] . $childrenClean . $matches['end']; }, $this->contents); return true; } /** * @param mixed $content */ public function addMainKey(string $key, $content): bool { $decoded = JsonFile::parseJson($this->contents); $content = $this->format($content); // key exists already $regex = '{'.self::DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))(?P.*)}sx'; if (isset($decoded[$key]) && Preg::isMatch($regex, $this->contents, $matches)) { // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['key'].'}')) { return false; } $this->contents = $matches['start'] . JsonFile::encode($key).': '.$content . $matches['end']; return true; } // append at the end of the file and keep whitespace if (Preg::isMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { $this->contents = Preg::replace( '#'.$match[1].'\}$#', addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\$'), $this->contents ); return true; } // append at the end of the file $this->contents = Preg::replace( '#\}$#', addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\$'), $this->contents ); return true; } public function removeMainKey(string $key): bool { $decoded = JsonFile::parseJson($this->contents); if (!array_key_exists($key, $decoded)) { return true; } // key exists already $regex = '{'.self::DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))\s*,?\s*(?P.*)}sx'; if (Preg::isMatch($regex, $this->contents, $matches)) { assert(is_string($matches['start'])); assert(is_string($matches['removal'])); assert(is_string($matches['end'])); // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['removal'].'}')) { return false; } // check that we are not leaving a dangling comma on the previous line if the last line was removed if (Preg::isMatchStrictGroups('#,\s*$#', $matches['start']) && Preg::isMatch('#^\}$#', $matches['end'])) { $matches['start'] = rtrim(Preg::replace('#,(\s*)$#', '$1', $matches['start']), $this->indent); } $this->contents = $matches['start'] . $matches['end']; if (Preg::isMatch('#^\{\s*\}\s*$#', $this->contents)) { $this->contents = "{\n}"; } return true; } return false; } public function removeMainKeyIfEmpty(string $key): bool { $decoded = JsonFile::parseJson($this->contents); if (!array_key_exists($key, $decoded)) { return true; } if (is_array($decoded[$key]) && count($decoded[$key]) === 0) { return $this->removeMainKey($key); } return true; } /** * @param mixed $data */ public function format($data, int $depth = 0): string { if (is_array($data)) { reset($data); if (is_numeric(key($data))) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } return '['.implode(', ', $data).']'; } $out = '{' . $this->newline; $elems = []; foreach ($data as $key => $val) { $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; } return JsonFile::encode($data); } protected function detectIndenting(): void { if (Preg::isMatchStrictGroups('{^([ \t]+)"}m', $this->contents, $match)) { $this->indent = $match[1]; } else { $this->indent = ' '; } } } __halt_compiler();----SIGNATURE:----hChNngqankYggz+F/LIBCO2g1G7bzhAisHLmloq2Uxosn6jW1j/Wno/EY0GzzmjNHF5Bja8tzAsoJFnR5rHvB9ta1hNCIRZTOCu93Ahk9ZPk3Nj9+dDhtLEnsck+c/aPumk49vbbk1aV1FNW2etymn2tsVsWANCuowQBJQ43bNf3PbBpNEZYR4eINU3A/HpdHdBkMgsmzPRe33HmRgwaH8yrSbj9tZB9hSsY9CDThnTO7Wc7mtKonzGahuckyzaQwuMumJipnk++ThfefF6AkwchbWRqYcIC2OgQevzDYUZTp7VpbVdKa31g1ubYxoVyEHyYkcEaErxqZdvX4ZDmavrF7SZCjucTTk/1HFbaQaajM/SMcbxVByB6tD5GySZ287U/4AwjUFUQXmmZiyDCKjr6rQ9mSO5SfIm1+SyC+MCWI/kVAvIjRikZcXZVSGtduRgaXFJab8IXaQOw4gdOsLQBpmTbX6y7xbkaHStTNX1s0OcYiP/eceKVfQSeyn5RoV3pDHglfzancPCawLtJ6VfUmo1VfxjBGYz3mzHBSDay3HaFTCRAHY8vX8dTcevGdnaagP9nJ9Yf76iN1ylryL3MKAe/othohWsHGv862RvrQM1shTdkRfSOYwFPwdiJu8fJwi/u+KwEzw6rpAm95LhriM1N9Ko2Rgs5JBdu8hM=----ATTACHMENT:----ODQ0NzgyMTQwNzczODM0NSA5NTE0OTIxNTQ3NTYxNjQxIDc3NTU5MTY3NDY2MTkxNA==