123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- <?php
- /*
- * This file is part of Composer.
- *
- * (c) Nils Adermann <naderman@naderman.de>
- * Jordi Boggiano <j.boggiano@seld.be>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Composer\Json;
- /**
- * @author Jordi Boggiano <j.boggiano@seld.be>
- */
- class JsonManipulator
- {
- private static $RECURSE_BLOCKS;
- private static $JSON_VALUE;
- private static $JSON_STRING;
- private $contents;
- private $newline;
- private $indent;
- public function __construct($contents)
- {
- if (!self::$RECURSE_BLOCKS) {
- self::$RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*';
- self::$JSON_STRING = '"(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x09\x0a-\x1f\\\\"])*"';
- self::$JSON_VALUE = '(?:[0-9.]+|null|true|false|'.self::$JSON_STRING.'|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})';
- }
- $contents = trim($contents);
- if (!preg_match('#^\{(.*)\}$#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()
- {
- return $this->contents . $this->newline;
- }
- public function addLink($type, $package, $constraint)
- {
- // no link of that type yet
- if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) {
- $this->addMainKey($type, array($package => $constraint));
- return true;
- }
- $linksRegex = '#("'.$type.'":\s*\{)([^}]+)(\})#s';
- if (!preg_match($linksRegex, $this->contents, $match)) {
- return false;
- }
- $links = $match[2];
- $packageRegex = str_replace('/', '\\\\?/', preg_quote($package));
- // link exists already
- if (preg_match('{"'.$packageRegex.'"\s*:}i', $links)) {
- $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)"[^"]+"}i', addcslashes(JsonFile::encode($package).'${1}"'.$constraint.'"', '\\'), $links);
- } elseif (preg_match('#[^\s](\s*)$#', $links, $match)) {
- // link missing but non empty links
- $links = preg_replace(
- '#'.$match[1].'$#',
- 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) . $links;
- }
- $this->contents = preg_replace($linksRegex, addcslashes('${1}'.$links.'$3', '\\'), $this->contents);
- return true;
- }
- public function addRepository($name, $config)
- {
- return $this->addSubNode('repositories', $name, $config);
- }
- public function removeRepository($name)
- {
- return $this->removeSubNode('repositories', $name);
- }
- public function addConfigSetting($name, $value)
- {
- return $this->addSubNode('config', $name, $value);
- }
- public function removeConfigSetting($name)
- {
- return $this->removeSubNode('config', $name);
- }
- public function addSubNode($mainNode, $name, $value)
- {
- // no main node yet
- if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
- $this->addMainKey(''.$mainNode.'', array($name => $value));
- return true;
- }
- $subName = null;
- if (false !== strpos($name, '.')) {
- list($name, $subName) = explode('.', $name, 2);
- }
- // main node content not match-able
- $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
- if (!preg_match($nodeRegex, $this->contents, $match)) {
- return false;
- }
- $children = $match[2];
- // invalid match due to un-regexable content, abort
- if (!json_decode('{'.$children.'}')) {
- return false;
- }
- $that = $this;
- // child exists
- if (preg_match('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', $children, $matches)) {
- $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) {
- if ($subName !== null) {
- $curVal = json_decode($matches[2], true);
- $curVal[$subName] = $value;
- $value = $curVal;
- }
- return $matches[1] . $that->format($value, 1) . $matches[3];
- }, $children);
- } elseif (preg_match('#[^\s](\s*)$#', $children, $match)) {
- if ($subName !== null) {
- $value = array($subName => $value);
- }
- // child missing but non empty children
- $children = preg_replace(
- '#'.$match[1].'$#',
- addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $match[1], '\\'),
- $children
- );
- } else {
- if ($subName !== null) {
- $value = array($subName => $value);
- }
- // children present but empty
- $children = $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $children;
- }
- $this->contents = preg_replace($nodeRegex, addcslashes('${1}'.$children.'$3', '\\'), $this->contents);
- return true;
- }
- public function removeSubNode($mainNode, $name)
- {
- // no node
- if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
- return true;
- }
- // empty node
- if (preg_match('#"'.$mainNode.'":\s*\{\s*\}#s', $this->contents)) {
- return true;
- }
- // no node content match-able
- $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
- if (!preg_match($nodeRegex, $this->contents, $match)) {
- return false;
- }
- $children = $match[2];
- // invalid match due to un-regexable content, abort
- if (!json_decode('{'.$children.'}')) {
- return false;
- }
- $subName = null;
- if (false !== strpos($name, '.')) {
- list($name, $subName) = explode('.', $name, 2);
- }
- // try and find a match for the subkey
- if (preg_match('{"'.preg_quote($name).'"\s*:}i', $children)) {
- // find best match for the value of "name"
- if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:'.self::$JSON_VALUE.')}', $children, $matches)) {
- $bestMatch = '';
- foreach ($matches[0] as $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;
- }
- }
- }
- }
- // no child data left, $name was the only key in
- if (!trim($childrenClean)) {
- $this->contents = preg_replace($nodeRegex, '$1'.$this->newline.$this->indent.'}', $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;
- }
- $that = $this;
- $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) {
- if ($subName !== null) {
- $curVal = json_decode('{'.$matches[2].'}', true);
- unset($curVal[$name][$subName]);
- $childrenClean = substr($that->format($curVal, 0), 1, -1);
- }
- return $matches[1] . $childrenClean . $matches[3];
- }, $this->contents);
- return true;
- }
- public function addMainKey($key, $content)
- {
- $content = $this->format($content);
- // key exists already
- $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'.
- '('.preg_quote(JsonFile::encode($key)).'\s*:\s*'.self::$JSON_VALUE.')(.*)}s';
- if (preg_match($regex, $this->contents, $matches)) {
- // invalid match due to un-regexable content, abort
- if (!json_decode('{'.$matches[2].'}')) {
- return false;
- }
- $this->contents = $matches[1] . JsonFile::encode($key).': '.$content . $matches[3];
- return true;
- }
- // append at the end of the file and keep whitespace
- if (preg_match('#[^{\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 format($data, $depth = 0)
- {
- 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 = array();
- 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()
- {
- if (preg_match('{^(\s+)"}m', $this->contents, $match)) {
- $this->indent = $match[1];
- } else {
- $this->indent = ' ';
- }
- }
- }
|