JsonManipulator.php 13 KB

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