JsonManipulator.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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\Json;
  12. /**
  13. * @author Jordi Boggiano <j.boggiano@seld.be>
  14. */
  15. class JsonManipulator
  16. {
  17. private static $RECURSE_BLOCKS;
  18. private static $JSON_VALUE;
  19. private static $JSON_STRING;
  20. private $contents;
  21. private $newline;
  22. private $indent;
  23. public function __construct($contents)
  24. {
  25. if (!self::$RECURSE_BLOCKS) {
  26. self::$RECURSE_BLOCKS = '(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\})*\})*';
  27. self::$JSON_STRING = '"(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x09\x0a-\x1f\\\\"])*"';
  28. self::$JSON_VALUE = '(?:[0-9.]+|null|true|false|'.self::$JSON_STRING.'|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})';
  29. }
  30. $contents = trim($contents);
  31. if (!preg_match('#^\{(.*)\}$#s', $contents)) {
  32. throw new \InvalidArgumentException('The json file must be an object ({})');
  33. }
  34. $this->newline = false !== strpos($contents, "\r\n") ? "\r\n": "\n";
  35. $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents;
  36. $this->detectIndenting();
  37. }
  38. public function getContents()
  39. {
  40. return $this->contents . $this->newline;
  41. }
  42. public function addLink($type, $package, $constraint)
  43. {
  44. $data = @json_decode($this->contents, true);
  45. // abort if the file is not parseable
  46. if (null === $data) {
  47. return false;
  48. }
  49. // no link of that type yet
  50. if (!isset($data[$type])) {
  51. return $this->addMainKey($type, array($package => $constraint));
  52. }
  53. $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'.
  54. '('.preg_quote(JsonFile::encode($type)).'\s*:\s*)('.self::$JSON_VALUE.')(.*)}s';
  55. if (!preg_match($regex, $this->contents, $matches)) {
  56. return false;
  57. }
  58. $links = $matches[3];
  59. if (isset($data[$type][$package])) {
  60. // update existing link
  61. $packageRegex = str_replace('/', '\\\\?/', preg_quote($package));
  62. // addcslashes is used to double up backslashes since preg_replace resolves them as back references otherwise, see #1588
  63. $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)'.self::$JSON_STRING.'}i', addcslashes(JsonFile::encode($package).'${1}"'.$constraint.'"', '\\'), $links);
  64. } else {
  65. if (preg_match('#^\s*\{\s*\S+.*?(\s*\}\s*)$#', $links, $match)) {
  66. // link missing but non empty links
  67. $links = preg_replace(
  68. '{'.preg_quote($match[1]).'$}',
  69. addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\'),
  70. $links
  71. );
  72. } else {
  73. // links empty
  74. $links = '{' . $this->newline .
  75. $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline .
  76. $this->indent . '}';
  77. }
  78. }
  79. $this->contents = $matches[1] . $matches[2] . $links . $matches[4];
  80. return true;
  81. }
  82. public function addRepository($name, $config)
  83. {
  84. return $this->addSubNode('repositories', $name, $config);
  85. }
  86. public function removeRepository($name)
  87. {
  88. return $this->removeSubNode('repositories', $name);
  89. }
  90. public function addConfigSetting($name, $value)
  91. {
  92. return $this->addSubNode('config', $name, $value);
  93. }
  94. public function removeConfigSetting($name)
  95. {
  96. return $this->removeSubNode('config', $name);
  97. }
  98. public function addSubNode($mainNode, $name, $value)
  99. {
  100. // no main node yet
  101. if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
  102. $this->addMainKey(''.$mainNode.'', array($name => $value));
  103. return true;
  104. }
  105. $subName = null;
  106. if (false !== strpos($name, '.')) {
  107. list($name, $subName) = explode('.', $name, 2);
  108. }
  109. // main node content not match-able
  110. $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
  111. if (!preg_match($nodeRegex, $this->contents, $match)) {
  112. return false;
  113. }
  114. $children = $match[2];
  115. // invalid match due to un-regexable content, abort
  116. if (!@json_decode('{'.$children.'}')) {
  117. return false;
  118. }
  119. $that = $this;
  120. // child exists
  121. if (preg_match('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', $children, $matches)) {
  122. $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) {
  123. if ($subName !== null) {
  124. $curVal = json_decode($matches[2], true);
  125. $curVal[$subName] = $value;
  126. $value = $curVal;
  127. }
  128. return $matches[1] . $that->format($value, 1) . $matches[3];
  129. }, $children);
  130. } elseif (preg_match('#[^\s](\s*)$#', $children, $match)) {
  131. if ($subName !== null) {
  132. $value = array($subName => $value);
  133. }
  134. // child missing but non empty children
  135. $children = preg_replace(
  136. '#'.$match[1].'$#',
  137. addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $match[1], '\\'),
  138. $children
  139. );
  140. } else {
  141. if ($subName !== null) {
  142. $value = array($subName => $value);
  143. }
  144. // children present but empty
  145. $children = $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $children;
  146. }
  147. $this->contents = preg_replace($nodeRegex, addcslashes('${1}'.$children.'$3', '\\'), $this->contents);
  148. return true;
  149. }
  150. public function removeSubNode($mainNode, $name)
  151. {
  152. // no node
  153. if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
  154. return true;
  155. }
  156. // empty node
  157. if (preg_match('#"'.$mainNode.'":\s*\{\s*\}#s', $this->contents)) {
  158. return true;
  159. }
  160. // no node content match-able
  161. $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
  162. if (!preg_match($nodeRegex, $this->contents, $match)) {
  163. return false;
  164. }
  165. $children = $match[2];
  166. // invalid match due to un-regexable content, abort
  167. if (!@json_decode('{'.$children.'}')) {
  168. return false;
  169. }
  170. $subName = null;
  171. if (false !== strpos($name, '.')) {
  172. list($name, $subName) = explode('.', $name, 2);
  173. }
  174. // try and find a match for the subkey
  175. if (preg_match('{"'.preg_quote($name).'"\s*:}i', $children)) {
  176. // find best match for the value of "name"
  177. if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:'.self::$JSON_VALUE.')}', $children, $matches)) {
  178. $bestMatch = '';
  179. foreach ($matches[0] as $match) {
  180. if (strlen($bestMatch) < strlen($match)) {
  181. $bestMatch = $match;
  182. }
  183. }
  184. $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count);
  185. if (1 !== $count) {
  186. $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $childrenClean, -1, $count);
  187. if (1 !== $count) {
  188. return false;
  189. }
  190. }
  191. }
  192. }
  193. // no child data left, $name was the only key in
  194. if (!trim($childrenClean)) {
  195. $this->contents = preg_replace($nodeRegex, '$1'.$this->newline.$this->indent.'}', $this->contents);
  196. // we have a subname, so we restore the rest of $name
  197. if ($subName !== null) {
  198. $curVal = json_decode('{'.$children.'}', true);
  199. unset($curVal[$name][$subName]);
  200. $this->addSubNode($mainNode, $name, $curVal[$name]);
  201. }
  202. return true;
  203. }
  204. $that = $this;
  205. $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) {
  206. if ($subName !== null) {
  207. $curVal = json_decode('{'.$matches[2].'}', true);
  208. unset($curVal[$name][$subName]);
  209. $childrenClean = substr($that->format($curVal, 0), 1, -1);
  210. }
  211. return $matches[1] . $childrenClean . $matches[3];
  212. }, $this->contents);
  213. return true;
  214. }
  215. public function addMainKey($key, $content)
  216. {
  217. $content = $this->format($content);
  218. // key exists already
  219. $regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'.
  220. '('.preg_quote(JsonFile::encode($key)).'\s*:\s*'.self::$JSON_VALUE.')(.*)}s';
  221. if (preg_match($regex, $this->contents, $matches)) {
  222. // invalid match due to un-regexable content, abort
  223. if (!@json_decode('{'.$matches[2].'}')) {
  224. return false;
  225. }
  226. $this->contents = $matches[1] . JsonFile::encode($key).': '.$content . $matches[3];
  227. return true;
  228. }
  229. // append at the end of the file and keep whitespace
  230. if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) {
  231. $this->contents = preg_replace(
  232. '#'.$match[1].'\}$#',
  233. addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\'),
  234. $this->contents
  235. );
  236. return true;
  237. }
  238. // append at the end of the file
  239. $this->contents = preg_replace(
  240. '#\}$#',
  241. addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\'),
  242. $this->contents
  243. );
  244. return true;
  245. }
  246. public function format($data, $depth = 0)
  247. {
  248. if (is_array($data)) {
  249. reset($data);
  250. if (is_numeric(key($data))) {
  251. foreach ($data as $key => $val) {
  252. $data[$key] = $this->format($val, $depth + 1);
  253. }
  254. return '['.implode(', ', $data).']';
  255. }
  256. $out = '{' . $this->newline;
  257. $elems = array();
  258. foreach ($data as $key => $val) {
  259. $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1);
  260. }
  261. return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}';
  262. }
  263. return JsonFile::encode($data);
  264. }
  265. protected function detectIndenting()
  266. {
  267. if (preg_match('{^(\s+)"}m', $this->contents, $match)) {
  268. $this->indent = $match[1];
  269. } else {
  270. $this->indent = ' ';
  271. }
  272. }
  273. }