JsonFile.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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. use Composer\Composer;
  13. use JsonSchema\Validator;
  14. use Seld\JsonLint\JsonParser;
  15. use Seld\JsonLint\ParsingException;
  16. use Composer\Util\RemoteFilesystem;
  17. use Composer\Downloader\TransportException;
  18. /**
  19. * Reads/writes json files.
  20. *
  21. * @author Konstantin Kudryashiv <ever.zet@gmail.com>
  22. * @author Jordi Boggiano <j.boggiano@seld.be>
  23. */
  24. class JsonFile
  25. {
  26. const LAX_SCHEMA = 1;
  27. const STRICT_SCHEMA = 2;
  28. const JSON_UNESCAPED_SLASHES = 64;
  29. const JSON_PRETTY_PRINT = 128;
  30. const JSON_UNESCAPED_UNICODE = 256;
  31. private $path;
  32. private $rfs;
  33. /**
  34. * Initializes json file reader/parser.
  35. *
  36. * @param string $path path to a lockfile
  37. * @param RemoteFilesystem $rfs required for loading http/https json files
  38. */
  39. public function __construct($path, RemoteFilesystem $rfs = null)
  40. {
  41. $this->path = $path;
  42. if (null === $rfs && preg_match('{^https?://}i', $path)) {
  43. throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
  44. }
  45. $this->rfs = $rfs;
  46. }
  47. /**
  48. * @return string
  49. */
  50. public function getPath()
  51. {
  52. return $this->path;
  53. }
  54. /**
  55. * Checks whether json file exists.
  56. *
  57. * @return bool
  58. */
  59. public function exists()
  60. {
  61. return is_file($this->path);
  62. }
  63. /**
  64. * Reads json file.
  65. *
  66. * @return array
  67. */
  68. public function read()
  69. {
  70. try {
  71. if ($this->rfs) {
  72. $json = $this->rfs->getContents($this->path, $this->path, false);
  73. } else {
  74. $json = file_get_contents($this->path);
  75. }
  76. } catch (TransportException $e) {
  77. throw new \RuntimeException($e->getMessage());
  78. } catch (\Exception $e) {
  79. throw new \RuntimeException('Could not read '.$this->path."\n\n".$e->getMessage());
  80. }
  81. return static::parseJson($json, $this->path);
  82. }
  83. /**
  84. * Writes json file.
  85. *
  86. * @param array $hash writes hash into json file
  87. * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
  88. */
  89. public function write(array $hash, $options = 448)
  90. {
  91. $dir = dirname($this->path);
  92. if (!is_dir($dir)) {
  93. if (file_exists($dir)) {
  94. throw new \UnexpectedValueException(
  95. $dir.' exists and is not a directory.'
  96. );
  97. }
  98. if (!mkdir($dir, 0777, true)) {
  99. throw new \UnexpectedValueException(
  100. $dir.' does not exist and could not be created.'
  101. );
  102. }
  103. }
  104. file_put_contents($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : ''));
  105. }
  106. /**
  107. * Validates the schema of the current json file according to composer-schema.json rules
  108. *
  109. * @param int $schema a JsonFile::*_SCHEMA constant
  110. * @return bool true on success
  111. * @throws \UnexpectedValueException
  112. */
  113. public function validateSchema($schema = self::STRICT_SCHEMA)
  114. {
  115. $content = file_get_contents($this->path);
  116. $data = json_decode($content);
  117. if (null === $data && 'null' !== $content) {
  118. self::validateSyntax($content, $this->path);
  119. }
  120. $schemaFile = __DIR__ . '/../../../res/composer-schema.json';
  121. $schemaData = json_decode(file_get_contents($schemaFile));
  122. if ($schema === self::LAX_SCHEMA) {
  123. $schemaData->additionalProperties = true;
  124. $schemaData->properties->name->required = false;
  125. $schemaData->properties->description->required = false;
  126. }
  127. $validator = new Validator();
  128. $validator->check($data, $schemaData);
  129. // TODO add more validation like check version constraints and such, perhaps build that into the arrayloader?
  130. if (!$validator->isValid()) {
  131. $errors = array();
  132. foreach ((array) $validator->getErrors() as $error) {
  133. $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message'];
  134. }
  135. throw new JsonValidationException('"'.$this->path.'" does not match the expected JSON schema', $errors);
  136. }
  137. return true;
  138. }
  139. /**
  140. * Encodes an array into (optionally pretty-printed) JSON
  141. *
  142. * This code is based on the function found at:
  143. * http://recursive-design.com/blog/2008/03/11/format-json-with-php/
  144. *
  145. * Originally licensed under MIT by Dave Perrett <mail@recursive-design.com>
  146. *
  147. * @param mixed $data Data to encode into a formatted JSON string
  148. * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
  149. * @return string Encoded json
  150. */
  151. public static function encode($data, $options = 448)
  152. {
  153. if (version_compare(PHP_VERSION, '5.4', '>=')) {
  154. return json_encode($data, $options);
  155. }
  156. $json = json_encode($data);
  157. $prettyPrint = (bool) ($options & self::JSON_PRETTY_PRINT);
  158. $unescapeUnicode = (bool) ($options & self::JSON_UNESCAPED_UNICODE);
  159. $unescapeSlashes = (bool) ($options & self::JSON_UNESCAPED_SLASHES);
  160. if (!$prettyPrint && !$unescapeUnicode && !$unescapeSlashes) {
  161. return $json;
  162. }
  163. $result = '';
  164. $pos = 0;
  165. $strLen = strlen($json);
  166. $indentStr = ' ';
  167. $newLine = "\n";
  168. $outOfQuotes = true;
  169. $buffer = '';
  170. $noescape = true;
  171. for ($i = 0; $i <= $strLen; $i++) {
  172. // Grab the next character in the string
  173. $char = substr($json, $i, 1);
  174. // Are we inside a quoted string?
  175. if ('"' === $char && $noescape) {
  176. $outOfQuotes = !$outOfQuotes;
  177. }
  178. if (!$outOfQuotes) {
  179. $buffer .= $char;
  180. $noescape = '\\' === $char ? !$noescape : true;
  181. continue;
  182. } elseif ('' !== $buffer) {
  183. if ($unescapeSlashes) {
  184. $buffer = str_replace('\\/', '/', $buffer);
  185. }
  186. if ($unescapeUnicode && function_exists('mb_convert_encoding')) {
  187. // http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha
  188. $buffer = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', function($match) {
  189. return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
  190. }, $buffer);
  191. }
  192. $result .= $buffer.$char;
  193. $buffer = '';
  194. continue;
  195. }
  196. if (':' === $char) {
  197. // Add a space after the : character
  198. $char .= ' ';
  199. } elseif (('}' === $char || ']' === $char)) {
  200. $pos--;
  201. $prevChar = substr($json, $i - 1, 1);
  202. if ('{' !== $prevChar && '[' !== $prevChar) {
  203. // If this character is the end of an element,
  204. // output a new line and indent the next line
  205. $result .= $newLine;
  206. for ($j = 0; $j < $pos; $j++) {
  207. $result .= $indentStr;
  208. }
  209. } else {
  210. // Collapse empty {} and []
  211. $result = rtrim($result)."\n\n".$indentStr;
  212. }
  213. }
  214. $result .= $char;
  215. // If the last character was the beginning of an element,
  216. // output a new line and indent the next line
  217. if (',' === $char || '{' === $char || '[' === $char) {
  218. $result .= $newLine;
  219. if ('{' === $char || '[' === $char) {
  220. $pos++;
  221. }
  222. for ($j = 0; $j < $pos; $j++) {
  223. $result .= $indentStr;
  224. }
  225. }
  226. }
  227. return $result;
  228. }
  229. /**
  230. * Parses json string and returns hash.
  231. *
  232. * @param string $json json string
  233. * @param string $file the json file
  234. *
  235. * @return mixed
  236. */
  237. public static function parseJson($json, $file = null)
  238. {
  239. $data = json_decode($json, true);
  240. if (null === $data && JSON_ERROR_NONE !== json_last_error()) {
  241. self::validateSyntax($json, $file);
  242. }
  243. return $data;
  244. }
  245. /**
  246. * Validates the syntax of a JSON string
  247. *
  248. * @param string $json
  249. * @param string $file
  250. * @return bool true on success
  251. * @throws \UnexpectedValueException
  252. * @throws JsonValidationException
  253. */
  254. protected static function validateSyntax($json, $file = null)
  255. {
  256. $parser = new JsonParser();
  257. $result = $parser->lint($json);
  258. if (null === $result) {
  259. if (defined('JSON_ERROR_UTF8') && JSON_ERROR_UTF8 === json_last_error()) {
  260. throw new \UnexpectedValueException('"'.$file.'" is not UTF-8, could not parse as JSON');
  261. }
  262. return true;
  263. }
  264. throw new ParsingException('"'.$file.'" does not contain valid JSON'."\n".$result->getMessage(), $result->getDetails());
  265. }
  266. }