PageRenderTime 58ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/fJSON.php

https://bitbucket.org/wbond/flourish/
PHP | 739 lines | 416 code | 97 blank | 226 comment | 121 complexity | 7174094a4cdae8e266c4b4ba22f5d83f MD5 | raw file
  1. <?php
  2. /**
  3. * Provides encoding and decoding for JSON
  4. *
  5. * This class is a compatibility class for the
  6. * [http://php.net/json json extension] on servers with PHP 5.0 or 5.1, or
  7. * servers with the json extension compiled out.
  8. *
  9. * This class will handle JSON values that are not contained in an array or
  10. * object - such values are not valid according to the JSON spec, but the
  11. * functionality is included for compatiblity with the json extension.
  12. *
  13. * @copyright Copyright (c) 2008-2010 Will Bond
  14. * @author Will Bond [wb] <will@flourishlib.com>
  15. * @license http://flourishlib.com/license
  16. *
  17. * @package Flourish
  18. * @link http://flourishlib.com/fJSON
  19. *
  20. * @version 1.0.0b6
  21. * @changes 1.0.0b6 Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
  22. * @changes 1.0.0b5 Added the ::output() method [wb, 2010-03-15]
  23. * @changes 1.0.0b4 Fixed a bug with ::decode() where JSON objects could lose all but the first key: value pair [wb, 2009-05-06]
  24. * @changes 1.0.0b3 Updated the class to be consistent with PHP 5.2.9+ for encoding and decoding invalid data [wb, 2009-05-04]
  25. * @changes 1.0.0b2 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
  26. * @changes 1.0.0b The initial implementation [wb, 2008-07-12]
  27. */
  28. class fJSON
  29. {
  30. // The following constants allow for nice looking callbacks to static methods
  31. const decode = 'fJSON::decode';
  32. const encode = 'fJSON::encode';
  33. const output = 'fJSON::output';
  34. const sendHeader = 'fJSON::sendHeader';
  35. /**
  36. * An abstract representation of [
  37. *
  38. * @internal
  39. *
  40. * @var integer
  41. */
  42. const J_ARRAY_OPEN = 0;
  43. /**
  44. * An abstract representation of , in a JSON array
  45. *
  46. * @internal
  47. *
  48. * @var integer
  49. */
  50. const J_ARRAY_COMMA = 1;
  51. /**
  52. * An abstract representation of ]
  53. *
  54. * @internal
  55. *
  56. * @var integer
  57. */
  58. const J_ARRAY_CLOSE = 2;
  59. /**
  60. * An abstract representation of {
  61. *
  62. * @internal
  63. *
  64. * @var integer
  65. */
  66. const J_OBJ_OPEN = 3;
  67. /**
  68. * An abstract representation of a JSON object key
  69. *
  70. * @internal
  71. *
  72. * @var integer
  73. */
  74. const J_KEY = 4;
  75. /**
  76. * An abstract representation of :
  77. *
  78. * @internal
  79. *
  80. * @var integer
  81. */
  82. const J_COLON = 5;
  83. /**
  84. * An abstract representation of , in a JSON object
  85. *
  86. * @internal
  87. *
  88. * @var integer
  89. */
  90. const J_OBJ_COMMA = 6;
  91. /**
  92. * An abstract representation of }
  93. *
  94. * @internal
  95. *
  96. * @var integer
  97. */
  98. const J_OBJ_CLOSE = 7;
  99. /**
  100. * An abstract representation of an integer
  101. *
  102. * @internal
  103. *
  104. * @var integer
  105. */
  106. const J_INTEGER = 8;
  107. /**
  108. * An abstract representation of a floating value
  109. *
  110. * @internal
  111. *
  112. * @var integer
  113. */
  114. const J_FLOAT = 9;
  115. /**
  116. * An abstract representation of a boolean true
  117. *
  118. * @internal
  119. *
  120. * @var integer
  121. */
  122. const J_TRUE = 10;
  123. /**
  124. * An abstract representation of a boolean false
  125. *
  126. * @internal
  127. *
  128. * @var integer
  129. */
  130. const J_FALSE = 11;
  131. /**
  132. * An abstract representation of null
  133. *
  134. * @internal
  135. *
  136. * @var integer
  137. */
  138. const J_NULL = 12;
  139. /**
  140. * An abstract representation of a string
  141. *
  142. * @internal
  143. *
  144. * @var integer
  145. */
  146. const J_STRING = 13;
  147. /**
  148. * An array of special characters in JSON strings
  149. *
  150. * @var array
  151. */
  152. static private $control_character_map = array(
  153. '"' => '\"', '\\' => '\\\\', '/' => '\/', "\x8" => '\b',
  154. "\xC" => '\f', "\n" => '\n', "\r" => '\r', "\t" => '\t'
  155. );
  156. /**
  157. * An array of what values are allowed after other values
  158. *
  159. * @internal
  160. *
  161. * @var array
  162. */
  163. static private $next_values = array(
  164. self::J_ARRAY_OPEN => array(
  165. self::J_ARRAY_OPEN => TRUE,
  166. self::J_ARRAY_CLOSE => TRUE,
  167. self::J_OBJ_OPEN => TRUE,
  168. self::J_INTEGER => TRUE,
  169. self::J_FLOAT => TRUE,
  170. self::J_TRUE => TRUE,
  171. self::J_FALSE => TRUE,
  172. self::J_NULL => TRUE,
  173. self::J_STRING => TRUE
  174. ),
  175. self::J_ARRAY_COMMA => array(
  176. self::J_ARRAY_OPEN => TRUE,
  177. self::J_OBJ_OPEN => TRUE,
  178. self::J_INTEGER => TRUE,
  179. self::J_FLOAT => TRUE,
  180. self::J_TRUE => TRUE,
  181. self::J_FALSE => TRUE,
  182. self::J_NULL => TRUE,
  183. self::J_STRING => TRUE
  184. ),
  185. self::J_ARRAY_CLOSE => array(
  186. self::J_ARRAY_CLOSE => TRUE,
  187. self::J_OBJ_CLOSE => TRUE,
  188. self::J_ARRAY_COMMA => TRUE,
  189. self::J_OBJ_COMMA => TRUE
  190. ),
  191. self::J_OBJ_OPEN => array(
  192. self::J_OBJ_CLOSE => TRUE,
  193. self::J_KEY => TRUE
  194. ),
  195. self::J_KEY => array(
  196. self::J_COLON => TRUE
  197. ),
  198. self::J_OBJ_COMMA => array(
  199. self::J_KEY => TRUE
  200. ),
  201. self::J_COLON => array(
  202. self::J_ARRAY_OPEN => TRUE,
  203. self::J_OBJ_OPEN => TRUE,
  204. self::J_INTEGER => TRUE,
  205. self::J_FLOAT => TRUE,
  206. self::J_TRUE => TRUE,
  207. self::J_FALSE => TRUE,
  208. self::J_NULL => TRUE,
  209. self::J_STRING => TRUE
  210. ),
  211. self::J_OBJ_CLOSE => array(
  212. self::J_ARRAY_CLOSE => TRUE,
  213. self::J_OBJ_CLOSE => TRUE,
  214. self::J_ARRAY_COMMA => TRUE,
  215. self::J_OBJ_COMMA => TRUE
  216. ),
  217. self::J_INTEGER => array(
  218. self::J_ARRAY_CLOSE => TRUE,
  219. self::J_OBJ_CLOSE => TRUE,
  220. self::J_ARRAY_COMMA => TRUE,
  221. self::J_OBJ_COMMA => TRUE
  222. ),
  223. self::J_FLOAT => array(
  224. self::J_ARRAY_CLOSE => TRUE,
  225. self::J_OBJ_CLOSE => TRUE,
  226. self::J_ARRAY_COMMA => TRUE,
  227. self::J_OBJ_COMMA => TRUE
  228. ),
  229. self::J_TRUE => array(
  230. self::J_ARRAY_CLOSE => TRUE,
  231. self::J_OBJ_CLOSE => TRUE,
  232. self::J_ARRAY_COMMA => TRUE,
  233. self::J_OBJ_COMMA => TRUE
  234. ),
  235. self::J_FALSE => array(
  236. self::J_ARRAY_CLOSE => TRUE,
  237. self::J_OBJ_CLOSE => TRUE,
  238. self::J_ARRAY_COMMA => TRUE,
  239. self::J_OBJ_COMMA => TRUE
  240. ),
  241. self::J_NULL => array(
  242. self::J_ARRAY_CLOSE => TRUE,
  243. self::J_OBJ_CLOSE => TRUE,
  244. self::J_ARRAY_COMMA => TRUE,
  245. self::J_OBJ_COMMA => TRUE
  246. ),
  247. self::J_STRING => array(
  248. self::J_ARRAY_CLOSE => TRUE,
  249. self::J_OBJ_CLOSE => TRUE,
  250. self::J_ARRAY_COMMA => TRUE,
  251. self::J_OBJ_COMMA => TRUE
  252. )
  253. );
  254. /**
  255. * Decodes a JSON string into native PHP data types
  256. *
  257. * This function is very strict about the format of JSON. If the string is
  258. * not a valid JSON string, `NULL` will be returned.
  259. *
  260. * @param string $json This should be the name of a related class
  261. * @param boolean $assoc If this is TRUE, JSON objects will be represented as an assocative array instead of a `stdClass` object
  262. * @return array|stdClass A PHP equivalent of the JSON string
  263. */
  264. static public function decode($json, $assoc=FALSE)
  265. {
  266. if (!is_string($json) && !is_numeric($json)) {
  267. return NULL;
  268. }
  269. $json = trim($json);
  270. if ($json === '') {
  271. return NULL;
  272. }
  273. // If the json is an array or object, we can rely on the php function
  274. if (function_exists('json_decode') && ($json[0] == '[' || $json[0] == '{' || version_compare(PHP_VERSION, '5.2.9', '>='))) {
  275. return json_decode($json, $assoc);
  276. }
  277. preg_match_all('~\[| # Array begin
  278. \]| # Array end
  279. {| # Object begin
  280. }| # Object end
  281. -?(?:0|[1-9]\d*) # Float
  282. (?:\.\d*(?:[eE][+\-]?\d+)?|
  283. (?:[eE][+\-]?\d+))|
  284. -?(?:0|[1-9]\d*)| # Integer
  285. true| # True
  286. false| # False
  287. null| # Null
  288. ,| # Member separator for arrays and objects
  289. :| # Value separator for objects
  290. "(?:(?:(?!\\\\u)[^\\\\"\n\b\f\r\t]+)| # String
  291. \\\\\\\\|
  292. \\\\/|
  293. \\\\"|
  294. \\\\b|
  295. \\\\f|
  296. \\\\n|
  297. \\\\r|
  298. \\\\t|
  299. \\\\u[0-9a-fA-F]{4})*"|
  300. \s+ # Whitespace
  301. ~x', $json, $matches);
  302. $matched_length = 0;
  303. $stack = array();
  304. $last = NULL;
  305. $last_key = NULL;
  306. $output = NULL;
  307. $container = NULL;
  308. if (sizeof($matches) == 1 && strlen($matches[0][0]) == strlen($json)) {
  309. $match = $matches[0][0];
  310. $stack = array();
  311. $type = self::getElementType($stack, self::J_ARRAY_OPEN, $match);
  312. $element = self::scalarize($type, $match);
  313. if ($match !== $element) {
  314. return $element;
  315. }
  316. }
  317. if ($json[0] != '[' && $json[0] != '{') {
  318. return NULL;
  319. }
  320. foreach ($matches[0] as $match) {
  321. if ($matched_length == 0) {
  322. if ($match == '[') {
  323. $output = array();
  324. $last = self::J_ARRAY_OPEN;
  325. } else {
  326. $output = ($assoc) ? array() : new stdClass();
  327. $last = self::J_OBJ_OPEN;
  328. }
  329. $stack[] = array($last, &$output);
  330. $container =& $output;
  331. $matched_length = 1;
  332. continue;
  333. }
  334. $matched_length += strlen($match);
  335. // Whitespace can be skipped over
  336. if (ctype_space($match)) {
  337. continue;
  338. }
  339. $type = self::getElementType($stack, $last, $match);
  340. // An invalid sequence will cause parsing to stop
  341. if (!isset(self::$next_values[$last][$type])) {
  342. break;
  343. }
  344. // Decode the data values
  345. $match = self::scalarize($type, $match);
  346. // If the element is not a value, record some info and continue
  347. if ($type == self::J_COLON ||
  348. $type == self::J_OBJ_COMMA ||
  349. $type == self::J_ARRAY_COMMA ||
  350. $type == self::J_KEY) {
  351. $last = $type;
  352. if ($type == self::J_KEY) {
  353. $last_key = $match;
  354. }
  355. continue;
  356. }
  357. // This flag is used to indicate if an array or object is being added and thus
  358. // if the container reference needs to be changed to the current match
  359. $ref_match = FALSE;
  360. // Closing an object or array
  361. if ($type == self::J_OBJ_CLOSE || $type == self::J_ARRAY_CLOSE) {
  362. array_pop($stack);
  363. if (sizeof($stack) == 0) {
  364. break;
  365. }
  366. $new_container = end($stack);
  367. $container =& $new_container[1];
  368. $last = $type;
  369. continue;
  370. }
  371. // Opening a new object or array requires some references to keep
  372. // track of what the current container is
  373. if ($type == self::J_OBJ_OPEN) {
  374. $match = ($assoc) ? array() : new stdClass();
  375. $ref_match = TRUE;
  376. }
  377. if ($type == self::J_ARRAY_OPEN) {
  378. $match = array();
  379. $ref_match = TRUE;
  380. }
  381. if ($ref_match) {
  382. $stack[] = array($type, &$match);
  383. $stack_end = end($stack);
  384. }
  385. // Here we assign the value. This code is kind of crazy because
  386. // we have to keep track of the current container by references
  387. // so we can traverse back down the stack as we move out of
  388. // nested arrays and objects
  389. if ($last == self::J_COLON && !$assoc) {
  390. if ($last_key == '') {
  391. $last_key = '_empty_';
  392. }
  393. if ($ref_match) {
  394. $container->$last_key =& $stack_end[1];
  395. $container =& $stack_end[1];
  396. } else {
  397. $container->$last_key = $match;
  398. }
  399. } elseif ($last == self::J_COLON) {
  400. if ($ref_match) {
  401. $container[$last_key] =& $stack_end[1];
  402. $container =& $stack_end[1];
  403. } else {
  404. $container[$last_key] = $match;
  405. }
  406. } else {
  407. if ($ref_match) {
  408. $container[] =& $stack_end[1];
  409. $container =& $stack_end[1];
  410. } else {
  411. $container[] = $match;
  412. }
  413. }
  414. if ($last == self::J_COLON) {
  415. $last_key = NULL;
  416. }
  417. $last = $type;
  418. unset($match);
  419. }
  420. if ($matched_length != strlen($json) || sizeof($stack) > 0) {
  421. return NULL;
  422. }
  423. return $output;
  424. }
  425. /**
  426. * Encodes a PHP value into a JSON string
  427. *
  428. * @param mixed $value The PHP value to encode
  429. * @return string The JSON string that is equivalent to the PHP value
  430. */
  431. static public function encode($value)
  432. {
  433. if (is_resource($value)) {
  434. return 'null';
  435. }
  436. if (function_exists('json_encode')) {
  437. return json_encode($value);
  438. }
  439. if (is_int($value)) {
  440. return (string) $value;
  441. }
  442. if (is_float($value)) {
  443. return (string) $value;
  444. }
  445. if (is_bool($value)) {
  446. return ($value) ? 'true' : 'false';
  447. }
  448. if (is_null($value)) {
  449. return 'null';
  450. }
  451. if (is_string($value)) {
  452. if (!preg_match('#^.*$#usD', $value)) {
  453. return 'null';
  454. }
  455. $char_array = fUTF8::explode($value);
  456. $output = '"';
  457. foreach ($char_array as $char) {
  458. if (isset(self::$control_character_map[$char])) {
  459. $output .= self::$control_character_map[$char];
  460. } elseif (strlen($char) < 2) {
  461. $output .= $char;
  462. } else {
  463. $output .= '\u' . substr(strtolower(fUTF8::ord($char)), 2);
  464. }
  465. }
  466. $output .= '"';
  467. return $output;
  468. }
  469. // Detect if an array is associative, which would mean it needs to be encoded as an object
  470. $is_assoc_array = FALSE;
  471. if (is_array($value) && $value) {
  472. $looking_for = 0;
  473. foreach ($value as $key => $val) {
  474. if (!is_numeric($key) || $key != $looking_for) {
  475. $is_assoc_array = TRUE;
  476. break;
  477. }
  478. $looking_for++;
  479. }
  480. }
  481. if (is_object($value) || $is_assoc_array) {
  482. $output = '{';
  483. $members = array();
  484. foreach ($value as $key => $val) {
  485. $members[] = self::encode((string) $key) . ':' . self::encode($val);
  486. }
  487. $output .= join(',', $members);
  488. $output .= '}';
  489. return $output;
  490. }
  491. if (is_array($value)) {
  492. $output = '[';
  493. $members = array();
  494. foreach ($value as $key => $val) {
  495. $members[] = self::encode($val);
  496. }
  497. $output .= join(',', $members);
  498. $output .= ']';
  499. return $output;
  500. }
  501. }
  502. /**
  503. * Determines the type of a parser JSON element
  504. *
  505. * @param array &$stack The stack of arrays/objects being parsed
  506. * @param integer $last The type of the last element parsed
  507. * @param string $element The element being detected
  508. * @return integer The element type
  509. */
  510. static private function getElementType(&$stack, $last, $element)
  511. {
  512. if ($element == '[') {
  513. return self::J_ARRAY_OPEN;
  514. }
  515. if ($element == ']') {
  516. return self::J_ARRAY_CLOSE;
  517. }
  518. if ($element == '{') {
  519. return self::J_OBJ_OPEN;
  520. }
  521. if ($element == '}') {
  522. return self::J_OBJ_CLOSE;
  523. }
  524. if (ctype_digit($element)) {
  525. return self::J_INTEGER;
  526. }
  527. if (is_numeric($element)) {
  528. return self::J_FLOAT;
  529. }
  530. if ($element == 'true') {
  531. return self::J_TRUE;
  532. }
  533. if ($element == 'false') {
  534. return self::J_FALSE;
  535. }
  536. if ($element == 'null') {
  537. return self::J_NULL;
  538. }
  539. $last_stack = end($stack);
  540. if ($element == ',' && $last_stack[0] == self::J_ARRAY_OPEN) {
  541. return self::J_ARRAY_COMMA;
  542. }
  543. if ($element == ',') {
  544. return self::J_OBJ_COMMA;
  545. }
  546. if ($element == ':') {
  547. return self::J_COLON;
  548. }
  549. if ($last == self::J_OBJ_OPEN || $last == self::J_OBJ_COMMA) {
  550. return self::J_KEY;
  551. }
  552. return self::J_STRING;
  553. }
  554. /**
  555. * Created a unicode code point from a JS escaped unicode character
  556. *
  557. * @param array $match A regex match containing the 4 digit code referenced by the key `1`
  558. * @return string The U+{digits} unicode code point
  559. */
  560. static private function makeUnicodeCodePoint($match)
  561. {
  562. return fUTF8::chr("U+" . $match[1]);
  563. }
  564. /**
  565. * Sets the proper `Content-Type` header and outputs the value, encoded as JSON
  566. *
  567. * @param mixed $value The PHP value to output as JSON
  568. * @return void
  569. */
  570. static public function output($value)
  571. {
  572. self::sendHeader();
  573. echo self::encode($value);
  574. }
  575. /**
  576. * Decodes a scalar value
  577. *
  578. * @param integer $type The type of the element
  579. * @param string $element The element to be converted to a scalar
  580. * @return mixed The scalar value or the original string of the element
  581. */
  582. static private function scalarize($type, $element)
  583. {
  584. if ($type == self::J_INTEGER) {
  585. $element = (integer) $element;
  586. }
  587. if ($type == self::J_FLOAT) {
  588. $element = (float) $element;
  589. }
  590. if ($type == self::J_FALSE) {
  591. $element = FALSE;
  592. }
  593. if ($type == self::J_TRUE) {
  594. $element = TRUE;
  595. }
  596. if ($type == self::J_NULL) {
  597. $element = NULL;
  598. }
  599. if ($type == self::J_STRING || $type == self::J_KEY) {
  600. $element = substr($element, 1, -1);
  601. $element = strtr($element, array_flip(self::$control_character_map));
  602. $element = preg_replace_callback('#\\\\u([0-9a-fA-F]{4})#', array('self', 'makeUnicodeCodePoint'), $element);
  603. }
  604. return $element;
  605. }
  606. /**
  607. * Sets the proper `Content-Type` header for UTF-8 encoded JSON
  608. *
  609. * @return void
  610. */
  611. static public function sendHeader()
  612. {
  613. header('Content-Type: application/json; charset=utf-8');
  614. }
  615. /**
  616. * Forces use as a static class
  617. *
  618. * @return fJSON
  619. */
  620. private function __construct() { }
  621. }
  622. /**
  623. * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
  624. *
  625. * Permission is hereby granted, free of charge, to any person obtaining a copy
  626. * of this software and associated documentation files (the "Software"), to deal
  627. * in the Software without restriction, including without limitation the rights
  628. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  629. * copies of the Software, and to permit persons to whom the Software is
  630. * furnished to do so, subject to the following conditions:
  631. *
  632. * The above copyright notice and this permission notice shall be included in
  633. * all copies or substantial portions of the Software.
  634. *
  635. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  636. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  637. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  638. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  639. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  640. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  641. * THE SOFTWARE.
  642. */