diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a427067 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/examples export-ignore +/vendor export-ignore +/docs export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/*.md export-ignore +/phpunit.xml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c9d5ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea +composer.lock +vendor/ +tests/resources/compiled/ +*.cache diff --git a/README.md b/README.md index 67043b6..fad914b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,96 @@ # Services_JSON PHP implementaion of json_encode/decode + +[](https://packagist.org/packages/eftec/bladeone) +[](https://packagist.org/packages/eftec/bladeone) +[]() +[]() +[]() +[]() +[]() + +JSON (JavaScript Object Notation, http://json.org) is a lightweight data-interchange format. +It is easy for humans to read and write. It is easy for machines to parse and generate. +It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999. +This feature can also be found in Python. JSON is a text format that is completely language independent +but uses conventions that are familiar to programmers of the C-family of languages, including +C, C++, C#, Java, JavaScript, Perl, TCL, and many others. These properties make JSON an ideal +data-interchange language. + +This package provides a simple encoder and decoder for JSON notation. It is intended for use +with client-side Javascript applications that make use of HTTPRequest to perform server +communication functions - data can be encoded into JSON notation for use in a client-side +javascript, or decoded from incoming Javascript requests. JSON format is native to Javascript, +and can be directly eval() with no further parsing overhead. + +## So, what is the goal with this version? + +While this version doesn't have a better performance than json_encode() and json_decode() available as extension, +it has the next features: + +[x] it doesn't require an extension. If you can't install an extension, then you can use this version. +[x] **it works with json with unquoted keys** (for example javascript notation) +[x] It is a simple file with no dependency. + +## Usage + +### Getting started + +* Install the library using composer (or you could download Services_JSON.php manually) +```shell +composer require eftec/services_json +``` +* Include the dependency +```php +use eftec\ServicesJson\Services_JSON; +include '../vendor/autoload.php'; +$s=new Services_JSON(); +``` + +* And creates a service class +```php +$s=new Services_JSON(); +``` +See the folder examples for further examples + +### Decode + +Decode transform (decodified) a json string into a stdclass or an associative array + +```php +$s=new Services_JSON(); +$json='{"hello":{"a":2,"b":3},"world":[1,2,3,"aaa"]}'; +var_dump($s->decode($json)); // as stdclass +var_dump($s->decode($json,true)); // as array +``` +It also works with unquoted keys + +```php +$s=new Services_JSON(); +$json='{hello:{a:2,b:3},world:[1,2,3,"aaa","bbbb"]}'; // the keys are unquoted. +var_dump($s->decode($json)); // as stdclass +var_dump($s->decode($json,true)); // as array +``` + +### Encode + +Encode transform a value (array, object, primitive value, etc.) into a json expression (a string) + +```php +$array=["hello"=>['a'=>2,'b'=>3],'world'=>[1,2,3,"aaa","bbb"]]; +$obj=(object)$array; +var_dump($s->encode($array)); // encode an associative array +var_dump($s->encode($obj)); // encode an object +``` + + +## Changelog + +* 1.1 + * It works with PHP 7.2 and higher. + * It doesn't require PECL to work. + * The code was cleaned + * web header is removed. + * Method decode() could return an associative array. +* 1.0.3-1.0.0 PECL version. It only works with PHP 4.x and PHP 5.x + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..078468d --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "eftec/services_json", + "description": "PHP implementaion of json_encode/decode", + "type": "library", + "license": "BSD", + "autoload": { + "psr-4": { + "eftec\\ServicesJson\\": "src/" + } + }, + "authors": [ + { + "name": "Michal Migurski", + "email": "mike-json@teczno.com" + }, + { + "name": "Matt Knapp" + }, + { + "name": "Brett Stimmerman" + }, + { + "name": "jorgecc", + "email": "jcastro@eftec.cl" + } + ], + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "ext-mbstring": "*" + } +} diff --git a/examples/example_decode.php b/examples/example_decode.php new file mode 100644 index 0000000..5d1b884 --- /dev/null +++ b/examples/example_decode.php @@ -0,0 +1,15 @@ +"; +echo "
"; +var_dump($s->decode($json)); +echo ""; +echo "Decode as array:
"; +var_dump($s->decode($json,true)); +echo ""; diff --git a/examples/example_decode_unquoted.php b/examples/example_decode_unquoted.php new file mode 100644 index 0000000..b48a62b --- /dev/null +++ b/examples/example_decode_unquoted.php @@ -0,0 +1,17 @@ +"; +echo "
"; +var_dump($s->decode($json)); +echo ""; +echo "Decode as array:
"; +var_dump($s->decode($json,true)); +echo ""; diff --git a/examples/example_encode.php b/examples/example_encode.php new file mode 100644 index 0000000..73c04bb --- /dev/null +++ b/examples/example_encode.php @@ -0,0 +1,20 @@ +['a'=>2,'b'=>3],'world'=>[1,2,3,"aaa","bbb"]]; + +$obj=(object)$array; + +echo "Encode array to json
"; +var_dump($s->encode($array)); +echo ""; +echo "Encode object to json
"; +var_dump($s->encode($obj)); +echo ""; diff --git a/src/Services_JSON.php b/src/Services_JSON.php new file mode 100644 index 0000000..1311d6e --- /dev/null +++ b/src/Services_JSON.php @@ -0,0 +1,838 @@ + + * @author Matt Knapp
+ * // create a new instance of Services_JSON
+ * $json = new Services_JSON();
+ *
+ * // convert a complexe value to JSON notation, and send it to the browser
+ * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
+ * $output = $json->encode($value);
+ *
+ * print($output);
+ * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
+ *
+ * // accept incoming POST data, assumed to be in JSON notation
+ * $input = file_get_contents('php://input', 1000000);
+ * $value = $json->decode($input);
+ *
+ */
+class Services_JSON
+{
+ /**
+ * constructs a new JSON instance
+ *
+ * @param int $use object behavior flags; combine with boolean-OR
+ *
+ * possible values:
+ * - SERVICES_JSON_LOOSE_TYPE: loose typing.
+ * "{...}" syntax creates associative arrays
+ * instead of objects in decode().
+ * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
+ * Values which can't be encoded (e.g. resources)
+ * appear as NULL instead of throwing errors.
+ * By default, a deeply-nested resource will
+ * bubble up with an error, so all return values
+ * from encode() should be checked with isError()
+ * - SERVICES_JSON_USE_TO_JSON: call toJSON when serializing objects
+ * It serializes the return value from the toJSON call rather
+ * than the object it'self, toJSON can return associative arrays,
+ * strings or numbers, if you return an object, make sure it does
+ * not have a toJSON method, otherwise an error will occur.
+ */
+ public function __construct($use = 0)
+ {
+ $this->use = $use;
+ $this->_mb_strlen = function_exists('mb_strlen');
+ $this->_mb_convert_encoding = function_exists('mb_convert_encoding');
+ $this->_mb_substr = function_exists('mb_substr');
+ }
+
+ // private - cache the mbstring lookup results..
+ public $_mb_strlen = false;
+ public $_mb_substr = false;
+ public $_mb_convert_encoding = false;
+
+ /**
+ * convert a string from one UTF-16 char to one UTF-8 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf16 UTF-16 character
+ * @return string UTF-8 character
+ * @access private
+ */
+ public function utf162utf8($utf16): string
+ {
+ if ($this->_mb_convert_encoding) {
+ return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
+ }
+ $bytes = (ord($utf16[0]) << 8) | ord($utf16[1]);
+ switch (true) {
+ case ((0x7F & $bytes) == $bytes):
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x7F & $bytes);
+ case (0x07FF & $bytes) == $bytes:
+ // return a 2-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xC0 | (($bytes >> 6) & 0x1F))
+ . chr(0x80 | ($bytes & 0x3F));
+ case (0xFFFF & $bytes) == $bytes:
+ // return a 3-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xE0 | (($bytes >> 12) & 0x0F))
+ . chr(0x80 | (($bytes >> 6) & 0x3F))
+ . chr(0x80 | ($bytes & 0x3F));
+ }
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * convert a string from one UTF-8 char to one UTF-16 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf8 UTF-8 character
+ * @return string UTF-16 character
+ * @access private
+ */
+ public function utf82utf16($utf8): string
+ {
+ // oh please oh please oh please oh please oh please
+ if ($this->_mb_convert_encoding) {
+ return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+ }
+ switch ($this->strlen8($utf8)) {
+ case 1:
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return $utf8;
+ case 2:
+ // return a UTF-16 character from a 2-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x07 & (ord($utf8[0]) >> 2))
+ . chr((0xC0 & (ord($utf8[0]) << 6))
+ | (0x3F & ord($utf8[1])));
+ case 3:
+ // return a UTF-16 character from a 3-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr((0xF0 & (ord($utf8[0]) << 4))
+ | (0x0F & (ord($utf8[1]) >> 2)))
+ . chr((0xC0 & (ord($utf8[1]) << 6))
+ | (0x7F & ord($utf8[2])));
+ }
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * encodes an arbitrary variable into JSON format (and sends JSON Header)
+ *
+ * @param mixed $var any number, boolean, string, array, or object to be encoded.
+ * see argument 1 to Services_JSON() above for array-parsing behavior.
+ * if var is a strng, note that encode() always expects it
+ * to be in ASCII or UTF-8 format!
+ *
+ * @return mixed JSON string representation of input var or an error if a problem occurs
+ * @access public
+ */
+ public function encode($var)
+ {
+ //header('Content-type: application/json');
+ return $this->encodeUnsafe($var);
+ }
+
+ /**
+ * encodes an arbitrary variable into JSON format without JSON Header - warning - may allow XSS!!!!)
+ *
+ * @param mixed $var any number, boolean, string, array, or object to be encoded.
+ * see argument 1 to Services_JSON() above for array-parsing behavior.
+ * if var is a strng, note that encode() always expects it
+ * to be in ASCII or UTF-8 format!
+ *
+ * @return mixed JSON string representation of input var or an error if a problem occurs
+ * @access public
+ */
+ public function encodeUnsafe($var)
+ {
+ // see bug #16908 - regarding numeric locale printing
+ $lc = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, 'C');
+ $ret = $this->_encode($var);
+ setlocale(LC_NUMERIC, $lc);
+ return $ret;
+ }
+
+ /**
+ * PRIVATE CODE that does the work of encodes an arbitrary variable into JSON format
+ *
+ * @param mixed $var any number, boolean, string, array, or object to be encoded.
+ * see argument 1 to Services_JSON() above for array-parsing behavior.
+ * if var is a strng, note that encode() always expects it
+ * to be in ASCII or UTF-8 format!
+ *
+ * @return mixed JSON string representation of input var or an error if a problem occurs
+ * @access public
+ */
+ public function _encode($var)
+ {
+ switch (gettype($var)) {
+ case 'boolean':
+ return $var ? 'true' : 'false';
+ case 'NULL':
+ return 'null';
+ case 'integer':
+ return (int)$var;
+ case 'double':
+ case 'float':
+ return (float)$var;
+ case 'string':
+ // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+ $ascii = '';
+ $strlen_var = $this->strlen8($var);
+ /*
+ * Iterate over every character in the string,
+ * escaping with a slash or encoding to UTF-8 where necessary
+ */
+ for ($c = 0; $c < $strlen_var; ++$c) {
+ $ord_var_c = ord($var[$c]);
+ switch (true) {
+ case $ord_var_c == 0x08:
+ $ascii .= '\b';
+ break;
+ case $ord_var_c == 0x09:
+ $ascii .= '\t';
+ break;
+ case $ord_var_c == 0x0A:
+ $ascii .= '\n';
+ break;
+ case $ord_var_c == 0x0C:
+ $ascii .= '\f';
+ break;
+ case $ord_var_c == 0x0D:
+ $ascii .= '\r';
+ break;
+ case $ord_var_c == 0x22:
+ case $ord_var_c == 0x2F:
+ case $ord_var_c == 0x5C:
+ // double quote, slash, slosh
+ $ascii .= '\\' . $var[$c];
+ break;
+ case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+ // characters U-00000000 - U-0000007F (same as ASCII)
+ $ascii .= $var[$c];
+ break;
+ case (($ord_var_c & 0xE0) == 0xC0):
+ // characters U-00000080 - U-000007FF, mask 110XXXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ if ($c + 1 >= $strlen_var) {
+ $c += 1;
+ $ascii .= '?';
+ break;
+ }
+ $char = pack('C*', $ord_var_c, ord($var[$c + 1]));
+ $c += 1;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ case (($ord_var_c & 0xF0) == 0xE0):
+ if ($c + 2 >= $strlen_var) {
+ $c += 2;
+ $ascii .= '?';
+ break;
+ }
+ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ @ord($var[$c + 1]),
+ @ord($var[$c + 2]));
+ $c += 2;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ case (($ord_var_c & 0xF8) == 0xF0):
+ if ($c + 3 >= $strlen_var) {
+ $c += 3;
+ $ascii .= '?';
+ break;
+ }
+ // characters U-00010000 - U-001FFFFF, mask 11110XXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var[$c + 1]),
+ ord($var[$c + 2]),
+ ord($var[$c + 3]));
+ $c += 3;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ case (($ord_var_c & 0xFC) == 0xF8):
+ // characters U-00200000 - U-03FFFFFF, mask 111110XX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ if ($c + 4 >= $strlen_var) {
+ $c += 4;
+ $ascii .= '?';
+ break;
+ }
+ $char = pack('C*', $ord_var_c,
+ ord($var[$c + 1]),
+ ord($var[$c + 2]),
+ ord($var[$c + 3]),
+ ord($var[$c + 4]));
+ $c += 4;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ case (($ord_var_c & 0xFE) == 0xFC):
+ if ($c + 5 >= $strlen_var) {
+ $c += 5;
+ $ascii .= '?';
+ break;
+ }
+ // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var[$c + 1]),
+ ord($var[$c + 2]),
+ ord($var[$c + 3]),
+ ord($var[$c + 4]),
+ ord($var[$c + 5]));
+ $c += 5;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ }
+ }
+ return '"' . $ascii . '"';
+ case 'array':
+ /*
+ * As per JSON spec if any array key is not an integer
+ * we must treat the the whole array as an object. We
+ * also try to catch a sparsely populated associative
+ * array with numeric keys here because some JS engines
+ * will create an array with empty indexes up to
+ * max_index which can cause memory issues and because
+ * the keys, which may be relevant, will be remapped
+ * otherwise.
+ *
+ * As per the ECMA and JSON specification an object may
+ * have any string as a property. Unfortunately due to
+ * a hole in the ECMA specification if the key is a
+ * ECMA reserved word or starts with a digit the
+ * parameter is only accessible using ECMAScript's
+ * bracket notation.
+ */
+ // treat as a JSON object
+ if (is_array($var) && count($var) && (array_keys($var) !== range(0, count($var) - 1))) {
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($var),
+ array_values($var));
+ foreach ($properties as $property) {
+ if ($this->isError($property)) {
+ return $property;
+ }
+ }
+ return '{' . implode(',', $properties) . '}';
+ }
+ // treat it like a regular array
+ $elements = array_map(array($this, '_encode'), $var);
+ foreach ($elements as $element) {
+ if ($this->isError($element)) {
+ return $element;
+ }
+ }
+ return '[' . implode(',', $elements) . ']';
+ case 'object':
+ // support toJSON methods.
+ if (($this->use & SERVICES_JSON_USE_TO_JSON) && method_exists($var, 'toJSON')) {
+ // this may end up allowing unlimited recursion
+ // so we check the return value to make sure it's not got the same method.
+ $recode = $var->toJSON();
+ if (method_exists($recode, 'toJSON')) {
+ if (($this->use & SERVICES_JSON_SUPPRESS_ERRORS)) {
+ return 'null';
+ }
+ $this->Services_JSON_Error(get_class($var) .
+ " toJSON returned an object with a toJSON method.");
+ }
+ return $this->_encode($recode);
+ }
+ $vars = get_object_vars($var);
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($vars),
+ array_values($vars));
+ foreach ($properties as $property) {
+ if ($this->isError($property)) {
+ return $property;
+ }
+ }
+ return '{' . implode(',', $properties) . '}';
+ default:
+ if (($this->use & SERVICES_JSON_SUPPRESS_ERRORS)) {
+ return 'null';
+ }
+ $this->Services_JSON_Error(gettype($var) . " can not be encoded as JSON string");
+ }
+ }
+
+ /**
+ * array-walking function for use in generating JSON-formatted name-value pairs
+ *
+ * @param string $name name of key to use
+ * @param mixed $value reference to an array element to be encoded
+ *
+ * @return string JSON-formatted name-value pair, like '"name":value'
+ * @access private
+ */
+ public function name_value($name, $value)
+ {
+ $encoded_value = $this->_encode($value);
+ if ($this->isError($encoded_value)) {
+ return $encoded_value;
+ }
+ return $this->_encode(strval($name)) . ':' . $encoded_value;
+ }
+
+ /**
+ * reduce a string by removing leading and trailing comments and whitespace
+ *
+ * @param $str string string value to strip of comments and whitespace
+ *
+ * @return string string value stripped of comments and whitespace
+ * @access private
+ */
+ public function reduce_string($str): string
+ {
+ $str = preg_replace(array(
+ // eliminate single line comments in '// ...' form
+ '#^\s*//(.+)$#m',
+ // eliminate multi-line comments in '/* ... */' form, at start of string
+ '#^\s*/\*(.+)\*/#Us',
+ // eliminate multi-line comments in '/* ... */' form, at end of string
+ '#/\*(.+)\*/\s*$#Us'
+ ), '', $str);
+ // eliminate extraneous space
+ return trim($str);
+ }
+
+ /**
+ * decodes a JSON string into appropriate variable
+ *
+ * @param string $str JSON-formatted string
+ * @param bool $asAssocArray if true, then it returns as an associative array.