PageRenderTime 70ms CodeModel.GetById 40ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Json/JsonManipulator.php

https://github.com/pborreli/composer
PHP | 291 lines | 222 code | 41 blank | 28 comment | 25 complexity | e89ce482f1b2a7d02e9800e36b1e06e2 MD5 | raw file
  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 $contents;
  19. private $newline;
  20. private $indent;
  21. public function __construct($contents)
  22. {
  23. $contents = trim($contents);
  24. if (!preg_match('#^\{(.*)\}$#s', $contents)) {
  25. throw new \InvalidArgumentException('The json file must be an object ({})');
  26. }
  27. $this->newline = false !== strpos("\r\n", $contents) ? "\r\n": "\n";
  28. $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents;
  29. $this->detectIndenting();
  30. }
  31. public function getContents()
  32. {
  33. return $this->contents . $this->newline;
  34. }
  35. public function addLink($type, $package, $constraint)
  36. {
  37. // no link of that type yet
  38. if (!preg_match('#"'.$type.'":\s*\{#', $this->contents)) {
  39. $this->addMainKey($type, $this->format(array($package => $constraint)));
  40. return true;
  41. }
  42. $linksRegex = '#("'.$type.'":\s*\{)([^}]+)(\})#s';
  43. if (!preg_match($linksRegex, $this->contents, $match)) {
  44. return false;
  45. }
  46. $links = $match[2];
  47. $packageRegex = str_replace('/', '\\\\?/', preg_quote($package));
  48. // link exists already
  49. if (preg_match('{"'.$packageRegex.'"\s*:}i', $links)) {
  50. $links = preg_replace('{"'.$packageRegex.'"(\s*:\s*)"[^"]+"}i', addcslashes(JsonFile::encode($package).'${1}"'.$constraint.'"', '\\'), $links);
  51. } elseif (preg_match('#[^\s](\s*)$#', $links, $match)) {
  52. // link missing but non empty links
  53. $links = preg_replace(
  54. '#'.$match[1].'$#',
  55. addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\'),
  56. $links
  57. );
  58. } else {
  59. // links empty
  60. $links = $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $links;
  61. }
  62. $this->contents = preg_replace($linksRegex, addcslashes('${1}'.$links.'$3', '\\'), $this->contents);
  63. return true;
  64. }
  65. public function addRepository($name, $config)
  66. {
  67. return $this->addSubNode('repositories', $name, $config);
  68. }
  69. public function removeRepository($name)
  70. {
  71. return $this->removeSubNode('repositories', $name);
  72. }
  73. public function addConfigSetting($name, $value)
  74. {
  75. return $this->addSubNode('config', $name, $value);
  76. }
  77. public function removeConfigSetting($name)
  78. {
  79. return $this->removeSubNode('config', $name);
  80. }
  81. public function addSubNode($mainNode, $name, $value)
  82. {
  83. // no main node yet
  84. if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
  85. $this->addMainKey(''.$mainNode.'', $this->format(array($name => $value)));
  86. return true;
  87. }
  88. $subName = null;
  89. if (false !== strpos($name, '.')) {
  90. list($name, $subName) = explode('.', $name, 2);
  91. }
  92. // main node content not match-able
  93. $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
  94. if (!preg_match($nodeRegex, $this->contents, $match)) {
  95. return false;
  96. }
  97. $children = $match[2];
  98. // invalid match due to un-regexable content, abort
  99. if (!json_decode('{'.$children.'}')) {
  100. return false;
  101. }
  102. $that = $this;
  103. // child exists
  104. if (preg_match('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', $children, $matches)) {
  105. $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)([0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})(,?)}', function ($matches) use ($name, $subName, $value, $that) {
  106. if ($subName !== null) {
  107. $curVal = json_decode($matches[2], true);
  108. $curVal[$subName] = $value;
  109. $value = $curVal;
  110. }
  111. return $matches[1] . $that->format($value, 1) . $matches[3];
  112. }, $children);
  113. } elseif (preg_match('#[^\s](\s*)$#', $children, $match)) {
  114. if ($subName !== null) {
  115. $value = array($subName => $value);
  116. }
  117. // child missing but non empty children
  118. $children = preg_replace(
  119. '#'.$match[1].'$#',
  120. addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $match[1], '\\'),
  121. $children
  122. );
  123. } else {
  124. if ($subName !== null) {
  125. $value = array($subName => $value);
  126. }
  127. // children present but empty
  128. $children = $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $children;
  129. }
  130. $this->contents = preg_replace($nodeRegex, addcslashes('${1}'.$children.'$3', '\\'), $this->contents);
  131. return true;
  132. }
  133. public function removeSubNode($mainNode, $name)
  134. {
  135. // no node
  136. if (!preg_match('#"'.$mainNode.'":\s*\{#', $this->contents)) {
  137. return true;
  138. }
  139. // empty node
  140. if (preg_match('#"'.$mainNode.'":\s*\{\s*\}#s', $this->contents)) {
  141. return true;
  142. }
  143. // no node content match-able
  144. $nodeRegex = '#("'.$mainNode.'":\s*\{)('.self::$RECURSE_BLOCKS.')(\})#s';
  145. if (!preg_match($nodeRegex, $this->contents, $match)) {
  146. return false;
  147. }
  148. $children = $match[2];
  149. // invalid match due to un-regexable content, abort
  150. if (!json_decode('{'.$children.'}')) {
  151. return false;
  152. }
  153. $subName = null;
  154. if (false !== strpos($name, '.')) {
  155. list($name, $subName) = explode('.', $name, 2);
  156. }
  157. // try and find a match for the subkey
  158. if (preg_match('{"'.preg_quote($name).'"\s*:}i', $children)) {
  159. // find best match for the value of "name"
  160. if (preg_match_all('{"'.preg_quote($name).'"\s*:\s*(?:[0-9.]+|null|true|false|"[^"]+"|\[[^\]]*\]|\{'.self::$RECURSE_BLOCKS.'\})}', $children, $matches)) {
  161. $bestMatch = '';
  162. foreach ($matches[0] as $match) {
  163. if (strlen($bestMatch) < strlen($match)) {
  164. $bestMatch = $match;
  165. }
  166. }
  167. $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count);
  168. if (1 !== $count) {
  169. $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $childrenClean, -1, $count);
  170. if (1 !== $count) {
  171. return false;
  172. }
  173. }
  174. }
  175. }
  176. // no child data left, $name was the only key in
  177. if (!trim($childrenClean)) {
  178. $this->contents = preg_replace($nodeRegex, '$1'.$this->newline.$this->indent.'}', $this->contents);
  179. // we have a subname, so we restore the rest of $name
  180. if ($subName !== null) {
  181. $curVal = json_decode('{'.$children.'}', true);
  182. unset($curVal[$name][$subName]);
  183. $this->addSubNode($mainNode, $name, $curVal[$name]);
  184. }
  185. return true;
  186. }
  187. $that = $this;
  188. $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) {
  189. if ($subName !== null) {
  190. $curVal = json_decode('{'.$matches[2].'}', true);
  191. unset($curVal[$name][$subName]);
  192. $childrenClean = substr($that->format($curVal, 0), 1, -1);
  193. }
  194. return $matches[1] . $childrenClean . $matches[3];
  195. }, $this->contents);
  196. return true;
  197. }
  198. public function addMainKey($key, $content)
  199. {
  200. if (preg_match('#[^{\s](\s*)\}$#', $this->contents, $match)) {
  201. $this->contents = preg_replace(
  202. '#'.$match[1].'\}$#',
  203. addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\'),
  204. $this->contents
  205. );
  206. } else {
  207. $this->contents = preg_replace(
  208. '#\}$#',
  209. addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\'),
  210. $this->contents
  211. );
  212. }
  213. }
  214. public function format($data, $depth = 0)
  215. {
  216. if (is_array($data)) {
  217. reset($data);
  218. if (is_numeric(key($data))) {
  219. foreach ($data as $key => $val) {
  220. $data[$key] = $this->format($val, $depth + 1);
  221. }
  222. return '['.implode(', ', $data).']';
  223. }
  224. $out = '{' . $this->newline;
  225. foreach ($data as $key => $val) {
  226. $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1);
  227. }
  228. return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}';
  229. }
  230. return JsonFile::encode($data);
  231. }
  232. protected function detectIndenting()
  233. {
  234. if (preg_match('{^(\s+)"}', $this->contents, $match)) {
  235. $this->indent = $match[1];
  236. } else {
  237. $this->indent = ' ';
  238. }
  239. }
  240. }