core/model/modx/jsonrpc/jsonrpc.inc
<?php
/**
* @package modx
* @subpackage jsonrpc
*/
/**
* JSON extension to the PHP-XMLRPC lib
*
* For more info see:
* http://www.json.org/
* http://json-rpc.org/
*
* @author Gaetano Giunta
* @version $Id: jsonrpc.inc,v 1.30 2007/02/22 13:50:18 ggiunta Exp $
* @copyright (c) 2005-2006 G. Giunta
*
* @todo the JSON proposed RFC states that when making json calls, we should
* specify an 'accep: application/json' http header. Currently we either
* do not otuput an 'accept' header or specify 'any' (in curl mode)
**/
// requires: xmlrpc.inc 2.0 or later
// Note: the json spec omits \v, but it is present in ECMA-262, so we allow it
$GLOBALS['ecma262_entities'] = array(
'b' => chr(8),
'f' => chr(12),
'n' => chr(10),
'r' => chr(13),
't' => chr(9),
'v' => chr(11)
);
// tables used for transcoding different charsets into us-ascii javascript
$GLOBALS['ecma262_iso88591_Entities']=array();
$GLOBALS['ecma262_iso88591_Entities']['in'] = array();
$GLOBALS['ecma262_iso88591_Entities']['out'] = array();
for ($i = 0; $i < 32; $i++)
{
$GLOBALS['ecma262_iso88591_Entities']['in'][] = chr($i);
$GLOBALS['ecma262_iso88591_Entities']['out'][] = sprintf('\u%\'04x', $i);
}
for ($i = 160; $i < 256; $i++)
{
$GLOBALS['ecma262_iso88591_Entities']['in'][] = chr($i);
$GLOBALS['ecma262_iso88591_Entities']['out'][] = sprintf('\u%\'04x', $i);
}
/**
* Encode php strings to valid JSON unicode representation.
* All chars outside ASCII range are converted to \uXXXX for maximum portability.
* @param string $data (in iso-8859-1 charset by default)
* @param string charset of source string, defaults to $GLOBALS['xmlrpc_internalencoding']
* @param string charset of the encoded string, defaults to ASCII for maximum interoperabilty
* @return string
* @access private
* @todo add support for UTF-16 as destination charset instead of ASCII
* @todo add support for UTF-16 as source charset
*/
function json_encode_entities($data, $src_encoding='', $dest_encoding='')
{
if ($src_encoding == '')
{
// lame, but we know no better...
$src_encoding = $GLOBALS['xmlrpc_internalencoding'];
}
switch(strtoupper($src_encoding.'_'.$dest_encoding))
{
case 'ISO-8859-1_':
case 'ISO-8859-1_US-ASCII':
$escaped_data = str_replace(array('\\', '"', '/', "\t", "\n", "\r", chr(8), chr(11), chr(12)), array('\\\\', '\"', '\/', '\t', '\n', '\r', '\b', '\v', '\f'), $data);
$escaped_data = str_replace($GLOBALS['ecma262_iso88591_Entities']['in'], $GLOBALS['ecma262_iso88591_Entities']['out'], $escaped_data);
break;
case 'ISO-8859-1_UTF-8':
$escaped_data = str_replace(array('\\', '"', '/', "\t", "\n", "\r", chr(8), chr(11), chr(12)), array('\\\\', '\"', '\/', '\t', '\n', '\r', '\b', '\v', '\f'), $data);
$escaped_data = utf8_encode($escaped_data);
break;
case 'ISO-8859-1_ISO-8859-1':
case 'US-ASCII_US-ASCII':
case 'US-ASCII_UTF-8':
case 'US-ASCII_':
case 'US-ASCII_ISO-8859-1':
case 'UTF-8_UTF-8':
$escaped_data = str_replace(array('\\', '"', '/', "\t", "\n", "\r", chr(8), chr(11), chr(12)), array('\\\\', '\"', '\/', '\t', '\n', '\r', '\b', '\v', '\f'), $data);
break;
case 'UTF-8_':
case 'UTF-8_US-ASCII':
case 'UTF-8_ISO-8859-1':
// NB: this will choke on invalid UTF-8, going most likely beyond EOF
$escaped_data = "";
// be kind to users creating string jsonrpcvals out of different php types
$data = (string) $data;
$ns = strlen ($data);
for ($nn = 0; $nn < $ns; $nn++)
{
$ch = $data[$nn];
$ii = ord($ch);
//1 7 0bbbbbbb (127)
if ($ii < 128)
{
/// @todo shall we replace this with a (supposedly) faster str_replace?
switch($ii){
case 8:
$escaped_data .= '\b';
break;
case 9:
$escaped_data .= '\t';
break;
case 10:
$escaped_data .= '\n';
break;
case 11:
$escaped_data .= '\v';
break;
case 12:
$escaped_data .= '\f';
break;
case 13:
$escaped_data .= '\r';
break;
case 34:
$escaped_data .= '\"';
break;
case 47:
$escaped_data .= '\/';
break;
case 92:
$escaped_data .= '\\';
break;
default:
$escaped_data .= $ch;
} // switch
}
//2 11 110bbbbb 10bbbbbb (2047)
else if ($ii>>5 == 6)
{
$b1 = ($ii & 31);
$ii = ord($data[$nn+1]);
$b2 = ($ii & 63);
$ii = ($b1 * 64) + $b2;
$ent = sprintf ('\u%\'04x', $ii);
$escaped_data .= $ent;
$nn += 1;
}
//3 16 1110bbbb 10bbbbbb 10bbbbbb
else if ($ii>>4 == 14)
{
$b1 = ($ii & 31);
$ii = ord($data[$nn+1]);
$b2 = ($ii & 63);
$ii = ord($data[$nn+2]);
$b3 = ($ii & 63);
$ii = ((($b1 * 64) + $b2) * 64) + $b3;
$ent = sprintf ('\u%\'04x', $ii);
$escaped_data .= $ent;
$nn += 2;
}
//4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
else if ($ii>>3 == 30)
{
$b1 = ($ii & 31);
$ii = ord($data[$nn+1]);
$b2 = ($ii & 63);
$ii = ord($data[$nn+2]);
$b3 = ($ii & 63);
$ii = ord($data[$nn+3]);
$b4 = ($ii & 63);
$ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4;
$ent = sprintf ('\u%\'04x', $ii);
$escaped_data .= $ent;
$nn += 3;
}
}
break;
default:
$escaped_data = '';
error_log("Converting from $src_encoding to $dest_encoding: not supported...");
} // switch
return $escaped_data;
/*
$length = strlen($data);
$escapeddata = "";
for($position = 0; $position < $length; $position++)
{
$character = substr($data, $position, 1);
$code = ord($character);
switch($code)
{
case 8:
$character = '\b';
break;
case 9:
$character = '\t';
break;
case 10:
$character = '\n';
break;
case 12:
$character = '\f';
break;
case 13:
$character = '\r';
break;
case 34:
$character = '\"';
break;
case 47:
$character = '\/';
break;
case 92:
$character = '\\\\';
break;
default:
if($code < 32 || $code > 159)
{
$character = "\u".str_pad(dechex($code), 4, '0', STR_PAD_LEFT);
}
break;
}
$escapeddata .= $character;
}
return $escapeddata;
*/
}
/**
* Parse a JSON string.
* NB: try to accept any valid string according to ECMA, even though the JSON
* spec is much more strict.
* Assumes input is UTF-8...
* @param string $data a json string
* @param bool $return_phpvals if true, do not rebuild jsonrpcval objects, but plain php values
* @param string $src_encoding
* @param string $dest_encoding
* @return bool
* @access private
* @todo support for other source encodings than UTF-8
* @todo optimization creep: build elements of arrays/objects asap instead of counting chars many times
* @todo we should move to xmlrpc_defencoding and xmlrpc_internalencoding as predefined values, but it would make this even slower...
* Maybe just move those two parameters outside of here into callers?
*
* @bug parsing of "[1]// comment here" works in ie/ff, but not here
* @bug parsing of "[.1]" works in ie/ff, but not here
* @bug parsing of "[01]" works in ie/ff, but not here
* @bug parsing of "{true:1}" works here, but not in ie/ff
* @bug parsing of "{a b:1}" works here, but not in ie/ff
*/
function json_parse($data, $return_phpvals=false, $src_encoding='UTF-8', $dest_encoding='ISO-8859-1')
{
// optimization creep: this is quite costly. Is there any better way to achieve it?
// also note that json does not really allow comments...
$data = preg_replace(array(
// eliminate single line comments in '// ...' form
// REMOVED BECAUSE OF BUGS: 1-does not match at end of non-empty line, 2-eats inside strings, too
//'#^\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'
), '', $data);
$data = trim($data); // remove excess whitespace
if ($data == '')
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (empty string?)';
return false;
}
//echo "Parsing string (".$data.")\n";
switch($data[0])
{
case '"':
case "'":
$len = strlen($data);
// quoted string: check for closing char first
if ($data[$len-1] == $data[0] && $len > 1)
{
// UTF8-decode (or encode) string
// NB: we MUST do this BEFORE looking for \xNN, \uMMMM or other escape sequences
if ($src_encoding == 'UTF-8' && ($dest_encoding == 'ISO-8859-1' || $dest_encoding == 'US-ASCII'))
{
$data = utf8_decode($data);
$len = strlen($data);
}
else
{
if ($dest_encoding == 'UTF-8' && ($src_encoding == 'ISO-8859-1' || $src_encoding == 'US-ASCII'))
{
$data = utf8_encode($data);
$len = strlen($data);
}
//else
//{
// $GLOBALS['_xh']['value'] = $GLOBALS['_xh']['ac'];
//}
}
$outdata = '';
$delim = $data[0];
for ($i = 1; $i < $len-1; $i++)
{
switch($data[$i])
{
case '\\':
if ($i == $len-2)
{
break;
}
switch($data[$i+1])
{
case 'b':
case 'f':
case 'n':
case 'r':
case 't':
case 'v':
$outdata .= $GLOBALS['ecma262_entities'][$data[$i+1]];
$i++;
break;
case 'u':
// most likely unicode code point
if ($dest_encoding == 'UTF-8')
{
/// @todo see if this is faster / works in all cases
//$outdata .= utf8_encode(chr(hexdec(substr($data, $i+4, 2))));
// encode the UTF code point into utf-8...
$ii = hexdec(substr($data, $i+2, 4));
if ($ii < 0x80)
{
$outdata .= chr($ii);
}
else if ($ii <= 0x800)
{
$outdata .= chr(0xc0 | $ii >> 6) . chr(0x80 | ($ii & 0x3f));
}
else if ($ii <= 0x10000)
{
$outdata .= chr(0xe0 | $ii >> 12) . chr(0x80 | ($ii >> 6 & 0x3f)) . chr(0x80 | ($ii & 0x3f));
}
else
{
$outdata .= chr(0xf0 | $ii >> 20) . chr(0x80 | ($ii >> 12 & 0x3f)) . chr(0x80 | ($ii >> 6 & 0x3f)) . chr(0x80 | ($ii & 0x3f));
}
$i += 5;
}
else
{
// Note: we only decode code points below 256, so we take the last 2 chars of the unicode representation
$outdata .= chr(hexdec(substr($data, $i+4, 2)));
$i += 5;
}
break;
case 'x':
// most likely unicode code point in hexadecimal
// Note: the json spec omits this case, but ECMA-262 does not...
if ($dest_encoding == 'UTF-8')
{
// encode the UTF code point into utf-8...
$ii = hexdec(substr($data, $i+2, 2));
if ($ii < 0x80)
{
$outdata .= chr($ii);
}
else if ($ii <= 0x800)
{
$outdata .= chr(0xc0 | $ii >> 6) . chr(0x80 | ($ii & 0x3f));
}
else if ($ii <= 0x10000)
{
$outdata .= chr(0xe0 | $ii >> 12) . chr(0x80 | ($ii >> 6 & 0x3f)) . chr(0x80 | ($ii & 0x3f));
}
else
{
$outdata .= chr(0xf0 | $ii >> 20) . chr(0x80 | ($ii >> 12 & 0x3f)) . chr(0x80 | ($ii >> 6 & 0x3f)) . chr(0x80 | ($ii & 0x3f));
}
$i += 3;
}
else
{
$outdata .= chr(hexdec(substr($data, $i+2, 2)));
$i += 3;
}
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
// Note: ECMA-262 forbids these escapes, we just skip it...
break;
default:
// Note: Javascript 1.5 on http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Guide
// mentions syntax /XXX with X octal number, but ECMA262
// explicitly forbids it...
$outdata .= $data[$i+1];
$i++;
} // end of switch on slash char found
break;
case $delim:
// found unquoted end of string in middle of string
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (unescaped quote char inside string?)';
return false;
case "\n":
case "\r":
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (line terminator char inside string?)';
return false;
default:
$outdata .= $data[$i];
}
} // end of loop on string chars
//echo "Found a string\n";
$GLOBALS['_xh']['vt'] = 'string';
$GLOBALS['_xh']['value'] = $outdata;
}
else
{
// string without a terminating quote
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (string missing closing quote?)';
return false;
}
break;
case '[':
case '{':
$len = strlen($data);
// object and array notation: use the same parsing code
if ($data[0] == '[')
{
if ($data[$len-1] != ']')
{
// invalid array
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (array missing closing bracket?)';
return false;
}
$GLOBALS['_xh']['vt'] = 'array';
}
else
{
if ($data[$len-1] != '}')
{
// invalid object
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (object missing closing bracket?)';
return false;
}
$GLOBALS['_xh']['vt'] = 'struct';
}
$data = trim(substr($data, 1, -1));
//echo "Parsing array/obj (".$data.")\n";
if ($data == '')
{
// empty array/object
$GLOBALS['_xh']['value'] = array();
}
else
{
$valuestack = array();
$last = array('type' => 'sl', 'start' => 0);
$len = strlen($data);
$value = array();
$keypos = null;
//$ac = '';
$vt = '';
//$start = 0;
for ($i = 0; $i <= $len; $i++)
{
if ($i == $len || ($data[$i] == ',' && $last['type'] == 'sl'))
{
// end of element: push it onto array
$slice = substr($data, $last['start'], ($i - $last['start']));
//$slice = trim($slice); useless here, sincewe trim it on sub-elementparsing
//echo "Found slice (".$slice.")\n";
//$valuestack[] = $last; // necessario ???
//$last = array('type' => 'sl', 'start' => ($i + 1));
if ($GLOBALS['_xh']['vt'] == 'array')
{
if ($slice == '')
{
// 'elided' element: ecma supports it, so do we
// what should happen here in fact is that
// "array index is augmented and element is undefined"
// NOTE: Firefox's js engine does not create
// trailing undefined elements, while IE does...
//if ($i < $len)
//{
if ($return_phpvals)
{
$value[] = null;
}
else
{
$value[] = new jsonrpcval(null, 'null');
}
//}
}
else
{
if (!json_parse($slice, $return_phpvals, $src_encoding, $dest_encoding))
{
return false;
}
else
{
$value[] = $GLOBALS['_xh']['value'];
$GLOBALS['_xh']['vt'] = 'array';
}
}
}
else
{
if (!$keypos)
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (missing object member name?)';
return false;
}
else
{
if (!json_parse(substr($data, $last['start'], $keypos-$last['start']), true, $src_encoding, $dest_encoding) ||
$GLOBALS['_xh']['vt'] != 'string')
{
// object member name received unquoted: what to do???
// be tolerant as much as we can. ecma tolerates numbers as identifiers, too...
$key = trim(substr($data, $last['start'], $keypos-$last['start']));
}
else
{
$key = $GLOBALS['_xh']['value'];
}
//echo "Use extension: $use_extension\n";
if (!json_parse(substr($data, $keypos+1, $i-$keypos-1), $return_phpvals, $src_encoding, $dest_encoding))
{
return false;
}
$value[$key] = $GLOBALS['_xh']['value'];
$GLOBALS['_xh']['vt'] = 'struct';
$keypos = null;
}
}
$last['start'] = $i + 1;
$vt = ''; // reset type of val found
}
else if ($data[$i] == '"' || $data[$i] == "'")
{
// found beginning of string: run till end
$ok = false;
for ($j = $i+1; $j < $len; $j++)
{
if ($data[$j] == $data[$i])
{
$ok = true;
break;
}
else if($data[$j] == '\\')
{
$j++;
}
}
if ($ok)
{
$i = $j; // advance pointer to end of string
$vt = 'st';
}
else
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (string missing closing quote?)';
return false;
}
}
else if ($data[$i] == "[")
{
$valuestack[] = $last;
$last = array('type' => 'ar', 'start' => $i);
}
else if ($data[$i] == '{')
{
$valuestack[] = $last;
$last = array('type' => 'ob', 'start' => $i);
}
else if ($data[$i] == "]")
{
if ($last['type'] == 'ar')
{
$last = array_pop($valuestack);
$vt = 'ar';
}
else
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (unmatched array closing bracket?)';
return false;
}
}
else if ($data[$i] == '}')
{
if ($last['type'] == 'ob')
{
$last = array_pop($valuestack);
$vt = 'ob';
}
else
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (unmatched object closing bracket?)';
return false;
}
}
else if ($data[$i] == ':' && $last['type'] == 'sl' && !$keypos)
{
//echo "Found key stop at pos. $i\n";
$keypos = $i;
}
else if ($data[$i] == '/' && $i < $len-1 && $data[$i+1] == "*")
{
// found beginning of comment: run till end
$ok = false;
for ($j = $i+2; $j < $len-1; $j++)
{
if ($data[$j] == '*' && $data[$j+1] == '/')
{
$ok = true;
break;
}
}
if ($ok)
{
$i = $j+1; // advance pointer to end of string
}
else
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data (comment missing closing tag?)';
return false;
}
}
}
$GLOBALS['_xh']['value'] = $value;
}
//return true;
break;
default:
//echo "Found a scalar val (not string): '$data'\n";
// be tolerant of uppercase chars in numbers/booleans/null
$data = strtolower($data);
if ($data == "true")
{
//echo "Found a true\n";
$GLOBALS['_xh']['value'] = true;
$GLOBALS['_xh']['vt'] = 'boolean';
}
else if ($data == "false")
{
//echo "Found a false\n";
$GLOBALS['_xh']['value'] = false;
$GLOBALS['_xh']['vt'] = 'boolean';
}
else if ($data == "null")
{
//echo "Found a null\n";
$GLOBALS['_xh']['value'] = null;
$GLOBALS['_xh']['vt'] = 'null';
}
// we could use is_numeric here, but rules are slightly different,
// e.g. 012 is NOT valid according to JSON or ECMA, but browsers inetrpret it as octal
/// @todo add support for .5
/// @todo add support for numbers in octal notation, eg. 010
else if (preg_match("#^-?(0|[1-9][0-9]*)(\.[0-9]*)?([e][+-]?[0-9]+)?$#" ,$data))
{
if (preg_match('#[.e]#', $data))
{
//echo "Found a double\n";
// floating point
$GLOBALS['_xh']['value'] = (double)$data;
$GLOBALS['_xh']['vt'] = 'double';
}
else
{
//echo "Found an int\n";
//integer
$GLOBALS['_xh']['value'] = (int)$data;
$GLOBALS['_xh']['vt'] = 'int';
}
//return true;
}
else if (preg_match("#^0x[0-9a-f]+$#", $data))
{
// int in hex notation: not in JSON, but in ECMA...
$GLOBALS['_xh']['vt'] = 'int';
$GLOBALS['_xh']['value'] = hexdec(substr($data, 2));
}
else
{
$GLOBALS['_xh']['isf_reason'] = 'Invalid data';
return false;
}
} // switch $data[0]
if (!$return_phpvals)
{
$GLOBALS['_xh']['value'] = new jsonrpcval($GLOBALS['_xh']['value'], $GLOBALS['_xh']['vt']);
}
return true;
}
/**
* Used in place of json_parse to take advantage of native json decoding when available:
* it parses either a jsonrpc request or a response.
* NB: php native decoding of json balks anyway at anything but array / struct as top level element
* @access private
* @bug unicode chars are handled differently from this and json_parse...
* @todo add support for src and dest encoding!!!
*/
function json_parse_native($data)
{
//echo "Parsing string - internal way (".$data.")\n";
$out = json_decode($data, true);
if (!is_array($out))
{
//$GLOBALS['_xh']['isf'] = 2;
$GLOBALS['_xh']['isf_reason'] = 'JSON parsing failed';
return false;
}
// decoding will be fine for a jsonrpc error response, so we have to
// check for it by hand here...
//else if (array_key_exists('error', $out) && $out['error'] != null)
//{
// $GLOBALS['_xh']['isf'] = 1;
//$GLOBALS['_xh']['value'] = $out['error'];
//}
else
{
$GLOBALS['_xh']['value'] = $out;
return true;
}
}
/**
* Parse a json string, expected to be jsonrpc request format
* @access private
*/
function jsonrpc_parse_req($data, $return_phpvals=false, $use_extension=false, $src_encoding='')
{
$GLOBALS['_xh']['isf']=0;
$GLOBALS['_xh']['isf_reason']='';
if ($return_phpvals && $use_extension)
{
$ok = json_parse_native($data);
}
else
{
$ok = json_parse($data, $return_phpvals, $src_encoding);
}
if ($ok)
{
if (!$return_phpvals)
$GLOBALS['_xh']['value'] = @$GLOBALS['_xh']['value']->me['struct'];
if (!is_array($GLOBALS['_xh']['value']) || !array_key_exists('method', $GLOBALS['_xh']['value'])
|| !array_key_exists('params', $GLOBALS['_xh']['value']) || !array_key_exists('id', $GLOBALS['_xh']['value']))
{
$GLOBALS['_xh']['isf_reason'] = 'JSON parsing did not return correct jsonrpc request object';
return false;
}
else
{
$GLOBALS['_xh']['method'] = $GLOBALS['_xh']['value']['method'];
$GLOBALS['_xh']['params'] = $GLOBALS['_xh']['value']['params'];
$GLOBALS['_xh']['id'] = $GLOBALS['_xh']['value']['id'];
if (!$return_phpvals)
{
/// @todo we should check for appropriate type for method name and params array...
$GLOBALS['_xh']['method'] = $GLOBALS['_xh']['method']->scalarval();
$GLOBALS['_xh']['params'] = $GLOBALS['_xh']['params']->me['array'];
$GLOBALS['_xh']['id'] = php_jsonrpc_decode($GLOBALS['_xh']['id']);
}
return true;
}
}
else
{
return false;
}
}
/**
* Parse a json string, expected to be in json-rpc response format.
* @access private
* @todo checks missing:
* - no extra members in response
* - no extra members in error struct
* - resp. ID validation
*/
function jsonrpc_parse_resp($data, $return_phpvals=false, $use_extension=false, $src_encoding='')
{
$GLOBALS['_xh']['isf']=0;
$GLOBALS['_xh']['isf_reason']='';
if ($return_phpvals && $use_extension)
{
$ok = json_parse_native($data);
}
else
{
$ok = json_parse($data, $return_phpvals, $src_encoding);
}
if ($ok)
{
if (!$return_phpvals)
{
$GLOBALS['_xh']['value'] = @$GLOBALS['_xh']['value']->me['struct'];
}
if (!is_array($GLOBALS['_xh']['value']) || !array_key_exists('result', $GLOBALS['_xh']['value'])
|| !array_key_exists('error', $GLOBALS['_xh']['value']) || !array_key_exists('id', $GLOBALS['_xh']['value']))
{
//$GLOBALS['_xh']['isf'] = 2;
$GLOBALS['_xh']['isf_reason'] = 'JSON parsing did not return correct jsonrpc response object';
return false;
}
if (!$return_phpvals)
{
$d_error = php_jsonrpc_decode($GLOBALS['_xh']['value']['error']);
$GLOBALS['_xh']['value']['id'] = php_jsonrpc_decode($GLOBALS['_xh']['value']['id']);
}
else
{
$d_error = $GLOBALS['_xh']['value']['error'];
}
$GLOBALS['_xh']['id'] = $GLOBALS['_xh']['value']['id'];
if ($d_error != null)
{
$GLOBALS['_xh']['isf'] = 1;
//$GLOBALS['_xh']['value'] = $d_error;
if (is_array($d_error) && array_key_exists('faultCode', $d_error)
&& array_key_exists('faultString', $d_error))
{
if($d_error['faultCode'] == 0)
{
// FAULT returned, errno needs to reflect that
$d_error['faultCode'] = -1;
}
$GLOBALS['_xh']['value'] = $d_error;
}
// NB: what about jsonrpc servers that do NOT respect
// the faultCode/faultString convention???
// we force the error into a string. regardless of type...
else //if (is_string($GLOBALS['_xh']['value']))
{
if ($return_phpvals)
{
$GLOBALS['_xh']['value'] = array('faultCode' => -1, 'faultString' => var_export($GLOBALS['_xh']['value']['error'], true));
}
else
{
$GLOBALS['_xh']['value'] = array('faultCode' => -1, 'faultString' => serialize_jsonrpcval($GLOBALS['_xh']['value']['error']));
}
}
}
else
{
$GLOBALS['_xh']['value'] = $GLOBALS['_xh']['value']['result'];
}
return true;
}
else
{
return false;
}
}
class jsonrpc_client extends xmlrpc_client
{
// by default, no multicall exists for JSON-RPC, so do not try it
var $no_multicall = true;
// default return type of calls to json-rpc servers: jsonrpcvals
var $return_type = 'jsonrpcvals';
/*
function jsonrpc_client($path, $server='', $port='', $method='')
{
$this->xmlrpc_client($path, $server, $port, $method);
// we need to override the list of std supported encodings, since
// according to ECMA-262, the standard charset is UTF-16
$this->accepted_charset_encodings = array('UTF-16', 'UTF-8', 'ISO-8859-1', 'US-ASCII');
}
*/
}
class jsonrpcmsg extends xmlrpcmsg
{
var $id = null; // used to store request ID internally
var $content_type = 'application/json';
/**
* @param string $meth the name of the method to invoke
* @param array $pars array of parameters to be paased to the method (xmlrpcval objects)
* @param mixed $id the id of the jsonrpc request
*/
function jsonrpcmsg($meth, $pars=0, $id=null)
{
// NB: a NULL id is allowed and has a very definite meaning!
$this->id = $id;
$this->xmlrpcmsg($meth, $pars);
}
/**
* @access private
*/
function createPayload($charset_encoding='')
{
if ($charset_encoding != '')
$this->content_type = 'application/json; charset=' . $charset_encoding;
else
$this->content_type = 'application/json';
// @ todo: verify if all chars are allowed for method names or can
// we just skip the js encoding on it?
$this->payload = "{\n\"method\": \"" . json_encode_entities($this->methodname, '', $charset_encoding) . "\",\n\"params\": [ ";
for($i = 0; $i < sizeof($this->params); $i++)
{
$p = $this->params[$i];
// MB: we try to force serialization as json even though the object
// param might be a plain xmlrpcval object.
// This way we do not need to override addParam, aren't we lazy?
$this->payload .= "\n " . serialize_jsonrpcval($p, $charset_encoding) .
",";
}
$this->payload = substr($this->payload, 0, -1) . "\n],\n\"id\": ";
switch (true)
{
case $this->id === null:
$this->payload .= 'null';
break;
case is_string($this->id):
$this->payload .= '"'.json_encode_entities($this->id, '', $charset_encoding).'"';
break;
case is_bool($this->id):
$this->payload .= ($this->id ? 'true' : 'false');
break;
default:
$this->payload .= $this->id;
}
$this->payload .= "\n}\n";
}
/**
* Parse the jsonrpc response contained in the string $data and return a jsonrpcresp object.
* @param string $data the xmlrpc response, eventually including http headers
* @param bool $headers_processed when true prevents parsing HTTP headers for interpretation of content-encoding and conseuqent decoding
* @param string $return_type decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or 'phpvals'
* @return jsonrpcresp
* @access private
*/
function &parseResponse($data='', $headers_processed=false, $return_type='jsonrpcvals')
{
if($this->debug)
{
print "<PRE>---GOT---\n" . htmlentities($data) . "\n---END---\n</PRE>";
}
if($data == '')
{
error_log('XML-RPC: jsonrpcmsg::parseResponse: no response received from server.');
$r = new jsonrpcresp(0, $GLOBALS['xmlrpcerr']['no_data'], $GLOBALS['xmlrpcstr']['no_data']);
return $r;
}
$GLOBALS['_xh']=array();
$raw_data = $data;
// parse the HTTP headers of the response, if present, and separate them from data
if(substr($data, 0, 4) == 'HTTP')
{
$r =& $this->parseResponseHeaders($data, $headers_processed);
if ($r)
{
// parent class implementation of parseResponseHeaders returns in case
// of error an object of the wrong type: recode it into correct object
$rj = new jsonrpcresp(0, $r->faultCode(), $r->faultString());
$rj->raw_data = $data;
return $rj;
}
}
else
{
$GLOBALS['_xh']['headers'] = array();
$GLOBALS['_xh']['cookies'] = array();
}
if($this->debug)
{
$start = strpos($data, '/* SERVER DEBUG INFO (BASE64 ENCODED):');
if ($start !== false)
{
$start += strlen('/* SERVER DEBUG INFO (BASE64 ENCODED):');
$end = strpos($data, '*/', $start);
$comments = substr($data, $start, $end-$start);
print "<PRE>---SERVER DEBUG INFO (DECODED) ---\n\t".htmlentities(str_replace("\n", "\n\t", base64_decode($comments)))."\n---END---\n</PRE>";
}
}
// be tolerant of extra whitespace in response body
$data = trim($data);
// be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
$end = strrpos($data, '}');
if ($end)
{
$data = substr($data, 0, $end+1);
}
// if user wants back raw json, give it to him
if ($return_type == 'json')
{
$r = new jsonrpcresp($data, 0, '', 'json');
$r->hdrs = $GLOBALS['_xh']['headers'];
$r->_cookies = $GLOBALS['_xh']['cookies'];
$r->raw_data = $raw_data;
return $r;
}
// @todo shall we try to check for non-unicode json received ???
if (!jsonrpc_parse_resp($data, $return_type=='phpvals'))
{
if ($this->debug)
{
/// @todo echo something for user?
}
$r = new jsonrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'],
$GLOBALS['xmlrpcstr']['invalid_return'] . ' ' . $GLOBALS['_xh']['isf_reason']);
}
//elseif ($return_type == 'jsonrpcvals' && !is_object($GLOBALS['_xh']['value']))
//{
// then something odd has happened
// and it's time to generate a client side error
// indicating something odd went on
// $r = & new jsonrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'],
// $GLOBALS['xmlrpcstr']['invalid_return']);
//}
else
{
$v = $GLOBALS['_xh']['value'];
if ($this->debug)
{
print "<PRE>---PARSED---\n" ;
var_export($v);
print "\n---END---</PRE>";
}
if($GLOBALS['_xh']['isf'])
{
$r = new jsonrpcresp(0, $v['faultCode'], $v['faultString']);
}
else
{
$r = new jsonrpcresp($v, 0, '', $return_type);
}
$r->id = $GLOBALS['_xh']['id'];
}
$r->hdrs = $GLOBALS['_xh']['headers'];
$r->_cookies = $GLOBALS['_xh']['cookies'];
$r->raw_data = $raw_data;
return $r;
}
}
class jsonrpcresp extends xmlrpcresp
{
var $content_type = 'application/json'; // NB: forces us to send US-ASCII over http
var $id = null;
/// @todo override creator, to set proper valtyp and id!
/**
* Returns json representation of the response.
* @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed
* @return string the json representation of the response
* @access public
*/
function serialize($charset_encoding='')
{
if ($charset_encoding != '')
$this->content_type = 'application/json; charset=' . $charset_encoding;
else
$this->content_type = 'application/json';
$this->payload = serialize_jsonrpcresp($this, $this->id, $charset_encoding);
return $this->payload;
}
}
class jsonrpcval extends xmlrpcval
{
/**
* Returns json representation of the value.
* @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed
* @return string
* @access public
*/
function serialize($charset_encoding='')
{
return serialize_jsonrpcval($this, $charset_encoding);
}
}
/**
* Takes a json value in PHP jsonrpcval object format
* and translates it into native PHP types.
*
* @param jsonrpcval $jsonrpc_val
* @param array $options if 'decode_php_objs' is set in the options array, jsonrpc objects can be decoded into php objects
* @return mixed
* @access public
*/
function php_jsonrpc_decode($jsonrpc_val, $options=array())
{
$kind = $jsonrpc_val->kindOf();
if($kind == 'scalar')
{
return $jsonrpc_val->scalarval();
}
elseif($kind == 'array')
{
$size = $jsonrpc_val->arraysize();
$arr = array();
for($i = 0; $i < $size; $i++)
{
$arr[] = php_jsonrpc_decode($jsonrpc_val->arraymem($i), $options);
}
return $arr;
}
elseif($kind == 'struct')
{
$jsonrpc_val->structreset();
// If user said so, try to rebuild php objects for specific struct vals.
/// @todo should we raise a warning for class not found?
// shall we check for proper subclass of xmlrpcval instead of
// presence of _php_class to detect what we can do?
if (in_array('decode_php_objs', $options))
{
if( $jsonrpc_val->_php_class != ''
&& class_exists($jsonrpc_val->_php_class))
{
$obj = @new $jsonrpc_val->_php_class;
}
else
{
$obj = new stdClass();
}
while(list($key,$value) = $jsonrpc_val->structeach())
{
$obj->$key = php_jsonrpc_decode($value, $options);
}
return $obj;
}
else
{
$arr = array();
while(list($key,$value) = $jsonrpc_val->structeach())
{
$arr[$key] = php_jsonrpc_decode($value, $options);
}
return $arr;
}
}
}
/**
* Takes native php types and encodes them into jsonrpc PHP object format.
* It will not re-encode jsonrpcval objects.
*
* @param mixed $php_val the value to be converted into a jsonrpcval object
* @param array $options can include 'encode_php_objs'
* @return jsonrpcval
* @access public
*/
function &php_jsonrpc_encode($php_val, $options='')
{
$type = gettype($php_val);
switch($type)
{
case 'string':
$jsonrpc_val = new jsonrpcval($php_val, $GLOBALS['xmlrpcString']);
break;
case 'integer':
$jsonrpc_val = new jsonrpcval($php_val, $GLOBALS['xmlrpcInt']);
break;
case 'double':
$jsonrpc_val = new jsonrpcval($php_val, $GLOBALS['xmlrpcDouble']);
break;
case 'boolean':
$jsonrpc_val = new jsonrpcval($php_val, $GLOBALS['xmlrpcBoolean']);
break;
case 'resource': // for compat with php json extension...
case 'NULL':
$jsonrpc_val = new jsonrpcval($php_val, $GLOBALS['xmlrpcNull']);
break;
case 'array':
// PHP arrays can be encoded to either objects or arrays,
// depending on wheter they are hashes or plain 0..n integer indexed
// A shorter one-liner would be
// $tmp = array_diff(array_keys($php_val), range(0, count($php_val)-1));
// but execution time skyrockets!
$j = 0;
$arr = array();
$ko = false;
foreach($php_val as $key => $val)
{
$arr[$key] =& php_jsonrpc_encode($val, $options);
if(!$ko && $key !== $j)
{
$ko = true;
}
$j++;
}
if($ko)
{
$jsonrpc_val = new jsonrpcval($arr, $GLOBALS['xmlrpcStruct']);
}
else
{
$jsonrpc_val = new jsonrpcval($arr, $GLOBALS['xmlrpcArray']);
}
break;
case 'object':
if($php_val instanceof jsonrpcval)
{
$jsonrpc_val = $php_val;
}
else
{
$arr = array();
while(list($k,$v) = each($php_val))
{
$arr[$k] = php_jsonrpc_encode($v, $options);
}
$jsonrpc_val = new jsonrpcval($arr, $GLOBALS['xmlrpcStruct']);
if (in_array('encode_php_objs', $options))
{
// let's save original class name into xmlrpcval:
// might be useful later on...
$jsonrpc_val->_php_class = get_class($php_val);
}
}
break;
// catch "user function", "unknown type"
default:
$jsonrpc_val = new jsonrpcval();
break;
}
return $jsonrpc_val;
}
/**
* Convert the json representation of a jsonrpc method call, jsonrpc method response
* or single json value into the appropriate object (a.k.a. deserialize).
* Please note that there is no way to distinguish the serialized representation
* of a single json val of type object which has the 3 appropriate members from
* the serialization of a method call or method response.
* In such a case, the function will return a jsonrpcresp or jsonrpcmsg
* @param string $json_val
* @param array $options
* @return mixed false on error, or an instance of jsonrpcval, jsonrpcresp or jsonrpcmsg
* @access public
* @todo add options controlling character set encodings
*/
function php_jsonrpc_decode_json($json_val, $options=array())
{
$src_encoding = array_key_exists('src_encoding', $options) ? $options['src_encoding'] : $GLOBALS['xmlrpc_defencoding'];
$dest_encoding = array_key_exists('dest_encoding', $options) ? $options['dest_encoding'] : $GLOBALS['xmlrpc_internalencoding'];
//$GLOBALS['_xh'] = array();
$GLOBALS['_xh']['isf'] = 0;
if (!json_parse($json_val, false, $src_encoding, $dest_encoding))
{
error_log($GLOBALS['_xh']['isf_reason']);
return false;
}
else
{
$val = $GLOBALS['_xh']['value']; // shortcut
if ($GLOBALS['_xh']['value']->kindOf() == 'struct')
{
if ($GLOBALS['_xh']['value']->structSize() == 3)
{
if ($GLOBALS['_xh']['value']->structMemExists('method') &&
$GLOBALS['_xh']['value']->structMemExists('params') &&
$GLOBALS['_xh']['value']->structMemExists('id'))
{
/// @todo we do not check for correct type of 'method', 'params' struct members...
$method = $GLOBALS['_xh']['value']->structMem('method');
$msg = new jsonrpcmsg($method->scalarval(), null, php_jsonrpc_decode($GLOBALS['_xh']['value']->structMem('id')));
$params = $GLOBALS['_xh']['value']->structMem('params');
for($i = 0; $i < $params->arraySize(); ++$i)
{
$msg->addparam($params->arrayMem($i));
}
return $msg;
}
else
if ($GLOBALS['_xh']['value']->structMemExists('result') &&
$GLOBALS['_xh']['value']->structMemExists('error') &&
$GLOBALS['_xh']['value']->structMemExists('id'))
{
$id = php_jsonrpc_decode($GLOBALS['_xh']['value']->structMem('id'));
$err = php_jsonrpc_decode($GLOBALS['_xh']['value']->structMem('error'));
if ($err == null)
{
$resp = new jsonrpcresp($GLOBALS['_xh']['value']->structMem('result'));
}
else
{
if (is_array($err) && array_key_exists('faultCode', $err)
&& array_key_exists('faultString', $err))
{
if($err['faultCode'] == 0)
{
// FAULT returned, errno needs to reflect that
$err['faultCode'] = -1;
}
}
// NB: what about jsonrpc servers that do NOT respect
// the faultCode/faultString convention???
// we force the error into a string. regardless of type...
else //if (is_string($GLOBALS['_xh']['value']))
{
$err = array('faultCode' => -1, 'faultString' => serialize_jsonrpcval($GLOBALS['_xh']['value']->structMem('error')));
}
$resp = new jsonrpcresp(0, $err['faultCode'], $err['faultString']);
}
$resp->id = $id;
return $resp;
}
}
}
// not a request msg nor a response: a plain jsonrpcval obj
return $GLOBALS['_xh']['value'];
}
}
/**
* Serialize a jsonrpcresp (or xmlrpcresp) as json.
* Moved outside of the corresponding class to ease multi-serialization of
* xmlrpcresp objects
* @param xmlrpcresp or jsonrpcresp $resp
* @param mixed $id
* @return string
* @access private
*/
function serialize_jsonrpcresp($resp, $id=null, $charset_encoding='')
{
$result = "{\n\"id\": ";
switch (true)
{
case $id === null:
$result .= 'null';
break;
case is_string($id):
$result .= '"'.json_encode_entities($id, '', $charset_encoding).'"';
break;
case is_bool($id):
$result .= ($id ? 'true' : 'false');
break;
default:
$result .= $id;
}
$result .= ", ";
if($resp->errno)
{
// let non-ASCII response messages be tolerated by clients
// by encoding non ascii chars
$result .= "\"error\": { \"faultCode\": " . $resp->errno . ", \"faultString\": \"" . json_encode_entities($resp->errstr, null, $charset_encoding) . "\" }, \"result\": null";
}
else
{
if(!is_object($resp->val) || !($resp->val instanceof xmlrpcval))
{
if (is_string($resp->val) && $resp->valtyp == 'json')
{
$result .= "\"error\": null, \"result\": " . $resp->val;
}
else
{
/// @todo try to build something serializable?
die('cannot serialize jsonrpcresp objects whose content is native php values');
}
}
else
{
$result .= "\"error\": null, \"result\": " .
serialize_jsonrpcval($resp->val, $charset_encoding);
}
}
$result .= "\n}";
return $result;
}
/**
* Serialize a jsonrpcval (or xmlrpcval) as json.
* Moved outside of the corresponding class to ease multi-serialization of
* xmlrpcval objects
* @param xmlrpcval or jsonrpcval $value
* @string $charset_encoding
* @access private
*/
function serialize_jsonrpcval($value, $charset_encoding='')
{
reset($value->me);
list($typ, $val) = each($value->me);
$rs = '';
switch(@$GLOBALS['xmlrpcTypes'][$typ])
{
case 1:
switch($typ)
{
case $GLOBALS['xmlrpcString']:
$rs .= '"' . json_encode_entities($val, null, $charset_encoding). '"';
break;
case $GLOBALS['xmlrpcI4']:
case $GLOBALS['xmlrpcInt']:
$rs .= (int)$val;
break;
case $GLOBALS['xmlrpcDateTime']:
// quote date as a json string.
// assumes date format is valid and will not break js...
$rs .= '"' . $val . '"';
break;
case $GLOBALS['xmlrpcDouble']:
// add a .0 in case value is integer.
// This helps us carrying around floats in js, and keep them separated from ints
$sval = strval((double)$val); // convert to string
if (strpos($sval, '.') !== false || strpos($sval, 'e') !== false)
{
$rs .= $sval;
}
else
{
$rs .= $val.'.0';
}
break;
case $GLOBALS['xmlrpcBoolean']:
$rs .= ($val ? 'true' : 'false');
break;
case $GLOBALS['xmlrpcBase64']:
// treat base 64 values as strings ???
$rs .= '"' . base64_encode($val) . '"';
break;
default:
$rs .= "null";
}
break;
case 2:
// array
$rs .= "[";
$len = sizeof($val);
if ($len)
{
for($i = 0; $i < $len; $i++)
{
$rs .= serialize_jsonrpcval($val[$i], $charset_encoding);
$rs .= ",";
}
$rs = substr($rs, 0, -1) . "]";
}
else
{
$rs .= "]";
}
break;
case 3:
// struct
//if ($value->_php_class)
//{
/// @todo implement json-rpc extension for object serialization
//$rs.='<struct php_class="' . $this->_php_class . "\">\n";
//}
//else
//{
//}
if (is_array($val)) {
foreach($val as $key2 => $val2)
{
$rs .= ',"'.json_encode_entities($key2, null, $charset_encoding).'":';
$rs .= serialize_jsonrpcval($val2, $charset_encoding);
}
}
$rs = '{' . substr($rs, 1) . '}';
break;
default:
break;
}
return $rs;
}
?>