579 lines
18 KiB
PHP
579 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Zend Framework
|
|
*
|
|
* LICENSE
|
|
*
|
|
* This source file is subject to the new BSD license that is bundled
|
|
* with this package in the file LICENSE.txt.
|
|
* It is also available through the world-wide-web at this URL:
|
|
* http://framework.zend.com/license/new-bsd
|
|
* If you did not receive a copy of the license and are unable to
|
|
* obtain it through the world-wide-web, please send an email
|
|
* to license@zend.com so we can send you a copy immediately.
|
|
*
|
|
* @category Zend
|
|
* @package Zend_Json
|
|
* @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
|
|
* @license http://framework.zend.com/license/new-bsd New BSD License
|
|
* @version $Id: Decoder.php 16541 2009-07-07 06:59:03Z bkarwin $
|
|
*/
|
|
|
|
/**
|
|
* @see Zend_Json
|
|
*/
|
|
require_once 'Zend/Json.php';
|
|
|
|
/**
|
|
* Decode JSON encoded string to PHP variable constructs
|
|
*
|
|
* @category Zend
|
|
* @package Zend_Json
|
|
* @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
|
|
* @license http://framework.zend.com/license/new-bsd New BSD License
|
|
*/
|
|
class Zend_Json_Decoder
|
|
{
|
|
/**
|
|
* Parse tokens used to decode the JSON object. These are not
|
|
* for public consumption, they are just used internally to the
|
|
* class.
|
|
*/
|
|
const EOF = 0;
|
|
const DATUM = 1;
|
|
const LBRACE = 2;
|
|
const LBRACKET = 3;
|
|
const RBRACE = 4;
|
|
const RBRACKET = 5;
|
|
const COMMA = 6;
|
|
const COLON = 7;
|
|
|
|
/**
|
|
* Use to maintain a "pointer" to the source being decoded
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_source;
|
|
|
|
/**
|
|
* Caches the source length
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_sourceLength;
|
|
|
|
/**
|
|
* The offset within the souce being decoded
|
|
*
|
|
* @var int
|
|
*
|
|
*/
|
|
protected $_offset;
|
|
|
|
/**
|
|
* The current token being considered in the parser cycle
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_token;
|
|
|
|
/**
|
|
* Flag indicating how objects should be decoded
|
|
*
|
|
* @var int
|
|
* @access protected
|
|
*/
|
|
protected $_decodeType;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $source String source to decode
|
|
* @param int $decodeType How objects should be decoded -- see
|
|
* {@link Zend_Json::TYPE_ARRAY} and {@link Zend_Json::TYPE_OBJECT} for
|
|
* valid values
|
|
* @return void
|
|
*/
|
|
protected function __construct($source, $decodeType)
|
|
{
|
|
// Set defaults
|
|
$this->_source = self::decodeUnicodeString($source);
|
|
$this->_sourceLength = strlen($this->_source);
|
|
$this->_token = self::EOF;
|
|
$this->_offset = 0;
|
|
|
|
// Normalize and set $decodeType
|
|
if (!in_array($decodeType, array(Zend_Json::TYPE_ARRAY, Zend_Json::TYPE_OBJECT)))
|
|
{
|
|
$decodeType = Zend_Json::TYPE_ARRAY;
|
|
}
|
|
$this->_decodeType = $decodeType;
|
|
|
|
// Set pointer at first token
|
|
$this->_getNextToken();
|
|
}
|
|
|
|
/**
|
|
* Decode a JSON source string
|
|
*
|
|
* Decodes a JSON encoded string. The value returned will be one of the
|
|
* following:
|
|
* - integer
|
|
* - float
|
|
* - boolean
|
|
* - null
|
|
* - StdClass
|
|
* - array
|
|
* - array of one or more of the above types
|
|
*
|
|
* By default, decoded objects will be returned as associative arrays; to
|
|
* return a StdClass object instead, pass {@link Zend_Json::TYPE_OBJECT} to
|
|
* the $objectDecodeType parameter.
|
|
*
|
|
* Throws a Zend_Json_Exception if the source string is null.
|
|
*
|
|
* @static
|
|
* @access public
|
|
* @param string $source String to be decoded
|
|
* @param int $objectDecodeType How objects should be decoded; should be
|
|
* either or {@link Zend_Json::TYPE_ARRAY} or
|
|
* {@link Zend_Json::TYPE_OBJECT}; defaults to TYPE_ARRAY
|
|
* @return mixed
|
|
* @throws Zend_Json_Exception
|
|
*/
|
|
public static function decode($source = null, $objectDecodeType = Zend_Json::TYPE_ARRAY)
|
|
{
|
|
if (null === $source) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Must specify JSON encoded source for decoding');
|
|
} elseif (!is_string($source)) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Can only decode JSON encoded strings');
|
|
}
|
|
|
|
$decoder = new self($source, $objectDecodeType);
|
|
|
|
return $decoder->_decodeValue();
|
|
}
|
|
|
|
|
|
/**
|
|
* Recursive driving rountine for supported toplevel tops
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function _decodeValue()
|
|
{
|
|
switch ($this->_token) {
|
|
case self::DATUM:
|
|
$result = $this->_tokenValue;
|
|
$this->_getNextToken();
|
|
return($result);
|
|
break;
|
|
case self::LBRACE:
|
|
return($this->_decodeObject());
|
|
break;
|
|
case self::LBRACKET:
|
|
return($this->_decodeArray());
|
|
break;
|
|
default:
|
|
return null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decodes an object of the form:
|
|
* { "attribute: value, "attribute2" : value,...}
|
|
*
|
|
* If Zend_Json_Encoder was used to encode the original object then
|
|
* a special attribute called __className which specifies a class
|
|
* name that should wrap the data contained within the encoded source.
|
|
*
|
|
* Decodes to either an array or StdClass object, based on the value of
|
|
* {@link $_decodeType}. If invalid $_decodeType present, returns as an
|
|
* array.
|
|
*
|
|
* @return array|StdClass
|
|
*/
|
|
protected function _decodeObject()
|
|
{
|
|
$members = array();
|
|
$tok = $this->_getNextToken();
|
|
|
|
while ($tok && $tok != self::RBRACE) {
|
|
if ($tok != self::DATUM || ! is_string($this->_tokenValue)) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Missing key in object encoding: ' . $this->_source);
|
|
}
|
|
|
|
$key = $this->_tokenValue;
|
|
$tok = $this->_getNextToken();
|
|
|
|
if ($tok != self::COLON) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Missing ":" in object encoding: ' . $this->_source);
|
|
}
|
|
|
|
$tok = $this->_getNextToken();
|
|
$members[$key] = $this->_decodeValue();
|
|
$tok = $this->_token;
|
|
|
|
if ($tok == self::RBRACE) {
|
|
break;
|
|
}
|
|
|
|
if ($tok != self::COMMA) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Missing "," in object encoding: ' . $this->_source);
|
|
}
|
|
|
|
$tok = $this->_getNextToken();
|
|
}
|
|
|
|
switch ($this->_decodeType) {
|
|
case Zend_Json::TYPE_OBJECT:
|
|
// Create new StdClass and populate with $members
|
|
$result = new StdClass();
|
|
foreach ($members as $key => $value) {
|
|
$result->$key = $value;
|
|
}
|
|
break;
|
|
case Zend_Json::TYPE_ARRAY:
|
|
default:
|
|
$result = $members;
|
|
break;
|
|
}
|
|
|
|
$this->_getNextToken();
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Decodes a JSON array format:
|
|
* [element, element2,...,elementN]
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function _decodeArray()
|
|
{
|
|
$result = array();
|
|
$starttok = $tok = $this->_getNextToken(); // Move past the '['
|
|
$index = 0;
|
|
|
|
while ($tok && $tok != self::RBRACKET) {
|
|
$result[$index++] = $this->_decodeValue();
|
|
|
|
$tok = $this->_token;
|
|
|
|
if ($tok == self::RBRACKET || !$tok) {
|
|
break;
|
|
}
|
|
|
|
if ($tok != self::COMMA) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Missing "," in array encoding: ' . $this->_source);
|
|
}
|
|
|
|
$tok = $this->_getNextToken();
|
|
}
|
|
|
|
$this->_getNextToken();
|
|
return($result);
|
|
}
|
|
|
|
|
|
/**
|
|
* Removes whitepsace characters from the source input
|
|
*/
|
|
protected function _eatWhitespace()
|
|
{
|
|
if (preg_match(
|
|
'/([\t\b\f\n\r ])*/s',
|
|
$this->_source,
|
|
$matches,
|
|
PREG_OFFSET_CAPTURE,
|
|
$this->_offset)
|
|
&& $matches[0][1] == $this->_offset)
|
|
{
|
|
$this->_offset += strlen($matches[0][0]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves the next token from the source stream
|
|
*
|
|
* @return int Token constant value specified in class definition
|
|
*/
|
|
protected function _getNextToken()
|
|
{
|
|
$this->_token = self::EOF;
|
|
$this->_tokenValue = null;
|
|
$this->_eatWhitespace();
|
|
|
|
if ($this->_offset >= $this->_sourceLength) {
|
|
return(self::EOF);
|
|
}
|
|
|
|
$str = $this->_source;
|
|
$str_length = $this->_sourceLength;
|
|
$i = $this->_offset;
|
|
$start = $i;
|
|
|
|
switch ($str{$i}) {
|
|
case '{':
|
|
$this->_token = self::LBRACE;
|
|
break;
|
|
case '}':
|
|
$this->_token = self::RBRACE;
|
|
break;
|
|
case '[':
|
|
$this->_token = self::LBRACKET;
|
|
break;
|
|
case ']':
|
|
$this->_token = self::RBRACKET;
|
|
break;
|
|
case ',':
|
|
$this->_token = self::COMMA;
|
|
break;
|
|
case ':':
|
|
$this->_token = self::COLON;
|
|
break;
|
|
case '"':
|
|
$result = '';
|
|
do {
|
|
$i++;
|
|
if ($i >= $str_length) {
|
|
break;
|
|
}
|
|
|
|
$chr = $str{$i};
|
|
|
|
if ($chr == '\\') {
|
|
$i++;
|
|
if ($i >= $str_length) {
|
|
break;
|
|
}
|
|
$chr = $str{$i};
|
|
switch ($chr) {
|
|
case '"' :
|
|
$result .= '"';
|
|
break;
|
|
case '\\':
|
|
$result .= '\\';
|
|
break;
|
|
case '/' :
|
|
$result .= '/';
|
|
break;
|
|
case 'b' :
|
|
$result .= chr(8);
|
|
break;
|
|
case 'f' :
|
|
$result .= chr(12);
|
|
break;
|
|
case 'n' :
|
|
$result .= chr(10);
|
|
break;
|
|
case 'r' :
|
|
$result .= chr(13);
|
|
break;
|
|
case 't' :
|
|
$result .= chr(9);
|
|
break;
|
|
case '\'' :
|
|
$result .= '\'';
|
|
break;
|
|
default:
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception("Illegal escape "
|
|
. "sequence '" . $chr . "'");
|
|
}
|
|
} elseif($chr == '"') {
|
|
break;
|
|
} else {
|
|
$result .= $chr;
|
|
}
|
|
} while ($i < $str_length);
|
|
|
|
$this->_token = self::DATUM;
|
|
//$this->_tokenValue = substr($str, $start + 1, $i - $start - 1);
|
|
$this->_tokenValue = $result;
|
|
break;
|
|
case 't':
|
|
if (($i+ 3) < $str_length && substr($str, $start, 4) == "true") {
|
|
$this->_token = self::DATUM;
|
|
}
|
|
$this->_tokenValue = true;
|
|
$i += 3;
|
|
break;
|
|
case 'f':
|
|
if (($i+ 4) < $str_length && substr($str, $start, 5) == "false") {
|
|
$this->_token = self::DATUM;
|
|
}
|
|
$this->_tokenValue = false;
|
|
$i += 4;
|
|
break;
|
|
case 'n':
|
|
if (($i+ 3) < $str_length && substr($str, $start, 4) == "null") {
|
|
$this->_token = self::DATUM;
|
|
}
|
|
$this->_tokenValue = NULL;
|
|
$i += 3;
|
|
break;
|
|
}
|
|
|
|
if ($this->_token != self::EOF) {
|
|
$this->_offset = $i + 1; // Consume the last token character
|
|
return($this->_token);
|
|
}
|
|
|
|
$chr = $str{$i};
|
|
if ($chr == '-' || $chr == '.' || ($chr >= '0' && $chr <= '9')) {
|
|
if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s',
|
|
$str, $matches, PREG_OFFSET_CAPTURE, $start) && $matches[0][1] == $start) {
|
|
|
|
$datum = $matches[0][0];
|
|
|
|
if (is_numeric($datum)) {
|
|
if (preg_match('/^0\d+$/', $datum)) {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception("Octal notation not supported by JSON (value: $datum)");
|
|
} else {
|
|
$val = intval($datum);
|
|
$fVal = floatval($datum);
|
|
$this->_tokenValue = ($val == $fVal ? $val : $fVal);
|
|
}
|
|
} else {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception("Illegal number format: $datum");
|
|
}
|
|
|
|
$this->_token = self::DATUM;
|
|
$this->_offset = $start + strlen($datum);
|
|
}
|
|
} else {
|
|
require_once 'Zend/Json/Exception.php';
|
|
throw new Zend_Json_Exception('Illegal Token');
|
|
}
|
|
|
|
return($this->_token);
|
|
}
|
|
|
|
/**
|
|
* Decode Unicode Characters from \u0000 ASCII syntax.
|
|
*
|
|
* This algorithm was originally developed for the
|
|
* Solar Framework by Paul M. Jones
|
|
*
|
|
* @link http://solarphp.com/
|
|
* @link http://svn.solarphp.com/core/trunk/Solar/Json.php
|
|
* @param string $value
|
|
* @return string
|
|
*/
|
|
public static function decodeUnicodeString($chrs)
|
|
{
|
|
$delim = substr($chrs, 0, 1);
|
|
$utf8 = '';
|
|
$strlen_chrs = strlen($chrs);
|
|
|
|
for($i = 0; $i < $strlen_chrs; $i++) {
|
|
|
|
$substr_chrs_c_2 = substr($chrs, $i, 2);
|
|
$ord_chrs_c = ord($chrs[$i]);
|
|
|
|
switch (true) {
|
|
case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $i, 6)):
|
|
// single, escaped unicode character
|
|
$utf16 = chr(hexdec(substr($chrs, ($i + 2), 2)))
|
|
. chr(hexdec(substr($chrs, ($i + 4), 2)));
|
|
$utf8 .= self::_utf162utf8($utf16);
|
|
$i += 5;
|
|
break;
|
|
case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
|
|
$utf8 .= $chrs{$i};
|
|
break;
|
|
case ($ord_chrs_c & 0xE0) == 0xC0:
|
|
// characters U-00000080 - U-000007FF, mask 110XXXXX
|
|
//see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
|
|
$utf8 .= substr($chrs, $i, 2);
|
|
++$i;
|
|
break;
|
|
case ($ord_chrs_c & 0xF0) == 0xE0:
|
|
// characters U-00000800 - U-0000FFFF, mask 1110XXXX
|
|
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
|
|
$utf8 .= substr($chrs, $i, 3);
|
|
$i += 2;
|
|
break;
|
|
case ($ord_chrs_c & 0xF8) == 0xF0:
|
|
// characters U-00010000 - U-001FFFFF, mask 11110XXX
|
|
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
|
|
$utf8 .= substr($chrs, $i, 4);
|
|
$i += 3;
|
|
break;
|
|
case ($ord_chrs_c & 0xFC) == 0xF8:
|
|
// characters U-00200000 - U-03FFFFFF, mask 111110XX
|
|
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
|
|
$utf8 .= substr($chrs, $i, 5);
|
|
$i += 4;
|
|
break;
|
|
case ($ord_chrs_c & 0xFE) == 0xFC:
|
|
// characters U-04000000 - U-7FFFFFFF, mask 1111110X
|
|
// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
|
|
$utf8 .= substr($chrs, $i, 6);
|
|
$i += 5;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $utf8;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* This method is from the Solar Framework by Paul M. Jones
|
|
*
|
|
* @link http://solarphp.com
|
|
* @param string $utf16 UTF-16 character
|
|
* @return string UTF-8 character
|
|
*/
|
|
protected static function _utf162utf8($utf16)
|
|
{
|
|
// Check for mb extension otherwise do by hand.
|
|
if( function_exists('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 '';
|
|
}
|
|
}
|
|
|