// Copyright (c) 1999,2000,2002 Edd Dumbill. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials provided // with the distribution. // // * Neither the name of the "XML-RPC for PHP" nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE // REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED // OF THE POSSIBILITY OF SUCH DAMAGE. require_once __DIR__ . "/phpxmlrpc.php"; require_once __DIR__ . "/xmlrpc_client.php"; require_once __DIR__ . "/xmlrpcresp.php"; require_once __DIR__ . "/xmlrpcmsg.php"; require_once __DIR__ . "/xmlrpcval.php"; /** * Convert a string to the correct XML representation in a target charset * To help correct communication of non-ascii chars inside strings, regardless * of the charset used when sending requests, parsing them, sending responses * and parsing responses, an option is to convert all non-ascii chars present in the message * into their equivalent 'charset entity'. Charset entities enumerated this way * are independent of the charset encoding used to transmit them, and all XML * parsers are bound to understand them. * Note that in the std case we are not sending a charset encoding mime type * along with http headers, so we are bound by RFC 3023 to emit strict us-ascii. * * @todo do a bit of basic benchmarking (strtr vs. str_replace) * @todo make usage of iconv() or recode_string() or mb_string() where available */ function xmlrpc_encode_entitites($data, $src_encoding='', $dest_encoding='') { $xmlrpc = Phpxmlrpc::instance(); if ($src_encoding == '') { // lame, but we know no better... $src_encoding = $xmlrpc->xmlrpc_internalencoding; } switch(strtoupper($src_encoding.'_'.$dest_encoding)) { case 'ISO-8859-1_': case 'ISO-8859-1_US-ASCII': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); $escaped_data = str_replace($xmlrpc->xml_iso88591_Entities['in'], $xmlrpc->xml_iso88591_Entities['out'], $escaped_data); break; case 'ISO-8859-1_UTF-8': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $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': //case 'CP1252_CP1252': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $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 xmlrpcvals 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 34: $escaped_data .= '"'; break; case 38: $escaped_data .= '&'; break; case 39: $escaped_data .= '''; break; case 60: $escaped_data .= '<'; break; case 62: $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 ('&#%d;', $ii); $escaped_data .= $ent; $nn += 1; } //3 16 1110bbbb 10bbbbbb 10bbbbbb else if ($ii>>4 == 14) { $b1 = ($ii & 15); $ii = ord($data[$nn+1]); $b2 = ($ii & 63); $ii = ord($data[$nn+2]); $b3 = ($ii & 63); $ii = ((($b1 * 64) + $b2) * 64) + $b3; $ent = sprintf ('&#%d;', $ii); $escaped_data .= $ent; $nn += 2; } //4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb else if ($ii>>3 == 30) { $b1 = ($ii & 7); $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 ('&#%d;', $ii); $escaped_data .= $ent; $nn += 3; } } break; /* case 'CP1252_': case 'CP1252_US-ASCII': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); $escaped_data = str_replace($GLOBALS['xml_iso88591_Entities']['in'], $GLOBALS['xml_iso88591_Entities']['out'], $escaped_data); $escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data); break; case 'CP1252_UTF-8': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); /// @todo we could use real UTF8 chars here instead of xml entities... (note that utf_8 encode all allone will NOT convert them) $escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data); $escaped_data = utf8_encode($escaped_data); break; case 'CP1252_ISO-8859-1': $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); // we might as well replave all funky chars with a '?' here, but we are kind and leave it to the receiving application layer to decide what to do with these weird entities... $escaped_data = str_replace($GLOBALS['xml_cp1252_Entities']['in'], $GLOBALS['xml_cp1252_Entities']['out'], $escaped_data); break; */ default: $escaped_data = ''; error_log("Converting from $src_encoding to $dest_encoding: not supported..."); } return $escaped_data; } /// xml parser handler function for opening element tags function xmlrpc_se($parser, $name, $attrs, $accept_single_vals=false) { $xmlrpc = Phpxmlrpc::instance(); // if invalid xmlrpc already detected, skip all processing if ($xmlrpc->_xh['isf'] < 2) { // check for correct element nesting // top level element can only be of 2 types /// @todo optimization creep: save this check into a bool variable, instead of using count() every time: /// there is only a single top level element in xml anyway if (count($xmlrpc->_xh['stack']) == 0) { if ($name != 'METHODRESPONSE' && $name != 'METHODCALL' && ( $name != 'VALUE' && !$accept_single_vals)) { $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = 'missing top level xmlrpc element'; return; } else { $xmlrpc->_xh['rt'] = strtolower($name); $xmlrpc->_xh['rt'] = strtolower($name); } } else { // not top level element: see if parent is OK $parent = end($xmlrpc->_xh['stack']); if (!array_key_exists($name, $xmlrpc->xmlrpc_valid_parents) || !in_array($parent, $xmlrpc->xmlrpc_valid_parents[$name])) { $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "xmlrpc element $name cannot be child of $parent"; return; } } switch($name) { // optimize for speed switch cases: most common cases first case 'VALUE': /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element $xmlrpc->_xh['vt']='value'; // indicator: no value found yet $xmlrpc->_xh['ac']=''; $xmlrpc->_xh['lv']=1; $xmlrpc->_xh['php_class']=null; break; case 'I4': case 'INT': case 'STRING': case 'BOOLEAN': case 'DOUBLE': case 'DATETIME.ISO8601': case 'BASE64': if ($xmlrpc->_xh['vt']!='value') { //two data elements inside a value: an error occurred! $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "$name element following a {$xmlrpc->_xh['vt']} element inside a single value"; return; } $xmlrpc->_xh['ac']=''; // reset the accumulator break; case 'STRUCT': case 'ARRAY': if ($xmlrpc->_xh['vt']!='value') { //two data elements inside a value: an error occurred! $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "$name element following a {$xmlrpc->_xh['vt']} element inside a single value"; return; } // create an empty array to hold child values, and push it onto appropriate stack $cur_val = array(); $cur_val['values'] = array(); $cur_val['type'] = $name; // check for out-of-band information to rebuild php objs // and in case it is found, save it if (@isset($attrs['PHP_CLASS'])) { $cur_val['php_class'] = $attrs['PHP_CLASS']; } $xmlrpc->_xh['valuestack'][] = $cur_val; $xmlrpc->_xh['vt']='data'; // be prepared for a data element next break; case 'DATA': if ($xmlrpc->_xh['vt']!='data') { //two data elements inside a value: an error occurred! $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "found two data elements inside an array element"; return; } case 'METHODCALL': case 'METHODRESPONSE': case 'PARAMS': // valid elements that add little to processing break; case 'METHODNAME': case 'NAME': /// @todo we could check for 2 NAME elements inside a MEMBER element $xmlrpc->_xh['ac']=''; break; case 'FAULT': $xmlrpc->_xh['isf']=1; break; case 'MEMBER': $xmlrpc->_xh['valuestack'][count($xmlrpc->_xh['valuestack'])-1]['name']=''; // set member name to null, in case we do not find in the xml later on //$xmlrpc->_xh['ac']=''; // Drop trough intentionally case 'PARAM': // clear value type, so we can check later if no value has been passed for this param/member $xmlrpc->_xh['vt']=null; break; case 'NIL': case 'EX:NIL': if ($xmlrpc->xmlrpc_null_extension) { if ($xmlrpc->_xh['vt']!='value') { //two data elements inside a value: an error occurred! $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "$name element following a {$xmlrpc->_xh['vt']} element inside a single value"; return; } $xmlrpc->_xh['ac']=''; // reset the accumulator break; } // we do not support the extension, so // drop through intentionally default: /// INVALID ELEMENT: RAISE ISF so that it is later recognized!!! $xmlrpc->_xh['isf'] = 2; $xmlrpc->_xh['isf_reason'] = "found not-xmlrpc xml element $name"; break; } // Save current element name to stack, to validate nesting $xmlrpc->_xh['stack'][] = $name; /// @todo optimization creep: move this inside the big switch() above if($name!='VALUE') { $xmlrpc->_xh['lv']=0; } } } /// Used in decoding xml chunks that might represent single xmlrpc values function xmlrpc_se_any($parser, $name, $attrs) { xmlrpc_se($parser, $name, $attrs, true); } /// xml parser handler function for close element tags function xmlrpc_ee($parser, $name, $rebuild_xmlrpcvals = true) { $xmlrpc = Phpxmlrpc::instance(); if ($xmlrpc->_xh['isf'] < 2) { // push this element name from stack // NB: if XML validates, correct opening/closing is guaranteed and // we do not have to check for $name == $curr_elem. // we also checked for proper nesting at start of elements... $curr_elem = array_pop($xmlrpc->_xh['stack']); switch($name) { case 'VALUE': // This if() detects if no scalar was inside if ($xmlrpc->_xh['vt']=='value') { $xmlrpc->_xh['value']=$xmlrpc->_xh['ac']; $xmlrpc->_xh['vt']=$xmlrpc->xmlrpcString; } if ($rebuild_xmlrpcvals) { // build the xmlrpc val out of the data received, and substitute it $temp = new xmlrpcval($xmlrpc->_xh['value'], $xmlrpc->_xh['vt']); // in case we got info about underlying php class, save it // in the object we're rebuilding if (isset($xmlrpc->_xh['php_class'])) $temp->_php_class = $xmlrpc->_xh['php_class']; // check if we are inside an array or struct: // if value just built is inside an array, let's move it into array on the stack $vscount = count($xmlrpc->_xh['valuestack']); if ($vscount && $xmlrpc->_xh['valuestack'][$vscount-1]['type']=='ARRAY') { $xmlrpc->_xh['valuestack'][$vscount-1]['values'][] = $temp; } else { $xmlrpc->_xh['value'] = $temp; } } else { /// @todo this needs to treat correctly php-serialized objects, /// since std deserializing is done by php_xmlrpc_decode, /// which we will not be calling... if (isset($xmlrpc->_xh['php_class'])) { } // check if we are inside an array or struct: // if value just built is inside an array, let's move it into array on the stack $vscount = count($xmlrpc->_xh['valuestack']); if ($vscount && $xmlrpc->_xh['valuestack'][$vscount-1]['type']=='ARRAY') { $xmlrpc->_xh['valuestack'][$vscount-1]['values'][] = $xmlrpc->_xh['value']; } } break; case 'BOOLEAN': case 'I4': case 'INT': case 'STRING': case 'DOUBLE': case 'DATETIME.ISO8601': case 'BASE64': $xmlrpc->_xh['vt']=strtolower($name); /// @todo: optimization creep - remove the if/elseif cycle below /// since the case() in which we are already did that if ($name=='STRING') { $xmlrpc->_xh['value']=$xmlrpc->_xh['ac']; } elseif ($name=='DATETIME.ISO8601') { if (!preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $xmlrpc->_xh['ac'])) { error_log('XML-RPC: invalid value received in DATETIME: '.$xmlrpc->_xh['ac']); } $xmlrpc->_xh['vt']=$xmlrpc->xmlrpcDateTime; $xmlrpc->_xh['value']=$xmlrpc->_xh['ac']; } elseif ($name=='BASE64') { /// @todo check for failure of base64 decoding / catch warnings $xmlrpc->_xh['value']=base64_decode($xmlrpc->_xh['ac']); } elseif ($name=='BOOLEAN') { // special case here: we translate boolean 1 or 0 into PHP // constants true or false. // Strings 'true' and 'false' are accepted, even though the // spec never mentions them (see eg. Blogger api docs) // NB: this simple checks helps a lot sanitizing input, ie no // security problems around here if ($xmlrpc->_xh['ac']=='1' || strcasecmp($xmlrpc->_xh['ac'], 'true') == 0) { $xmlrpc->_xh['value']=true; } else { // log if receiveing something strange, even though we set the value to false anyway if ($xmlrpc->_xh['ac']!='0' && strcasecmp($xmlrpc->_xh['ac'], 'false') != 0) error_log('XML-RPC: invalid value received in BOOLEAN: '.$xmlrpc->_xh['ac']); $xmlrpc->_xh['value']=false; } } elseif ($name=='DOUBLE') { // we have a DOUBLE // we must check that only 0123456789-. are characters here // NOTE: regexp could be much stricter than this... if (!preg_match('/^[+-eE0123456789 \t.]+$/', $xmlrpc->_xh['ac'])) { /// @todo: find a better way of throwing an error than this! error_log('XML-RPC: non numeric value received in DOUBLE: '.$xmlrpc->_xh['ac']); $xmlrpc->_xh['value']='ERROR_NON_NUMERIC_FOUND'; } else { // it's ok, add it on $xmlrpc->_xh['value']=(double)$xmlrpc->_xh['ac']; } } else { // we have an I4/INT // we must check that only 0123456789- are characters here if (!preg_match('/^[+-]?[0123456789 \t]+$/', $xmlrpc->_xh['ac'])) { /// @todo find a better way of throwing an error than this! error_log('XML-RPC: non numeric value received in INT: '.$xmlrpc->_xh['ac']); $xmlrpc->_xh['value']='ERROR_NON_NUMERIC_FOUND'; } else { // it's ok, add it on $xmlrpc->_xh['value']=(int)$xmlrpc->_xh['ac']; } } //$xmlrpc->_xh['ac']=''; // is this necessary? $xmlrpc->_xh['lv']=3; // indicate we've found a value break; case 'NAME': $xmlrpc->_xh['valuestack'][count($xmlrpc->_xh['valuestack'])-1]['name'] = $xmlrpc->_xh['ac']; break; case 'MEMBER': //$xmlrpc->_xh['ac']=''; // is this necessary? // add to array in the stack the last element built, // unless no VALUE was found if ($xmlrpc->_xh['vt']) { $vscount = count($xmlrpc->_xh['valuestack']); $xmlrpc->_xh['valuestack'][$vscount-1]['values'][$xmlrpc->_xh['valuestack'][$vscount-1]['name']] = $xmlrpc->_xh['value']; } else error_log('XML-RPC: missing VALUE inside STRUCT in received xml'); break; case 'DATA': //$xmlrpc->_xh['ac']=''; // is this necessary? $xmlrpc->_xh['vt']=null; // reset this to check for 2 data elements in a row - even if they're empty break; case 'STRUCT': case 'ARRAY': // fetch out of stack array of values, and promote it to current value $curr_val = array_pop($xmlrpc->_xh['valuestack']); $xmlrpc->_xh['value'] = $curr_val['values']; $xmlrpc->_xh['vt']=strtolower($name); if (isset($curr_val['php_class'])) { $xmlrpc->_xh['php_class'] = $curr_val['php_class']; } break; case 'PARAM': // add to array of params the current value, // unless no VALUE was found if ($xmlrpc->_xh['vt']) { $xmlrpc->_xh['params'][]=$xmlrpc->_xh['value']; $xmlrpc->_xh['pt'][]=$xmlrpc->_xh['vt']; } else error_log('XML-RPC: missing VALUE inside PARAM in received xml'); break; case 'METHODNAME': $xmlrpc->_xh['method']=preg_replace('/^[\n\r\t ]+/', '', $xmlrpc->_xh['ac']); break; case 'NIL': case 'EX:NIL': if ($xmlrpc->xmlrpc_null_extension) { $xmlrpc->_xh['vt']='null'; $xmlrpc->_xh['value']=null; $xmlrpc->_xh['lv']=3; break; } // drop through intentionally if nil extension not enabled case 'PARAMS': case 'FAULT': case 'METHODCALL': case 'METHORESPONSE': break; default: // End of INVALID ELEMENT! // shall we add an assert here for unreachable code??? break; } } } /// Used in decoding xmlrpc requests/responses without rebuilding xmlrpc values function xmlrpc_ee_fast($parser, $name) { xmlrpc_ee($parser, $name, false); } /// xml parser handler function for character data function xmlrpc_cd($parser, $data) { $xmlrpc = Phpxmlrpc::instance(); // skip processing if xml fault already detected if ($xmlrpc->_xh['isf'] < 2) { // "lookforvalue==3" means that we've found an entire value // and should discard any further character data if($xmlrpc->_xh['lv']!=3) { // G. Giunta 2006-08-23: useless change of 'lv' from 1 to 2 //if($xmlrpc->_xh['lv']==1) //{ // if we've found text and we're just in a then // say we've found a value //$xmlrpc->_xh['lv']=2; //} // we always initialize the accumulator before starting parsing, anyway... //if(!@isset($xmlrpc->_xh['ac'])) //{ // $xmlrpc->_xh['ac'] = ''; //} $xmlrpc->_xh['ac'].=$data; } } } /// xml parser handler function for 'other stuff', ie. not char data or /// element start/end tag. In fact it only gets called on unknown entities... function xmlrpc_dh($parser, $data) { $xmlrpc = Phpxmlrpc::instance(); // skip processing if xml fault already detected if ($xmlrpc->_xh['isf'] < 2) { if(substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') { // G. Giunta 2006-08-25: useless change of 'lv' from 1 to 2 //if($xmlrpc->_xh['lv']==1) //{ // $xmlrpc->_xh['lv']=2; //} $xmlrpc->_xh['ac'].=$data; } } return true; } // date helpers /** * Given a timestamp, return the corresponding ISO8601 encoded string. * * Really, timezones ought to be supported * but the XML-RPC spec says: * * "Don't assume a timezone. It should be specified by the server in its * documentation what assumptions it makes about timezones." * * These routines always assume localtime unless * $utc is set to 1, in which case UTC is assumed * and an adjustment for locale is made when encoding * * @param int $timet (timestamp) * @param int $utc (0 or 1) * @return string */ function iso8601_encode($timet, $utc=0) { if(!$utc) { $t=strftime("%Y%m%dT%H:%M:%S", $timet); } else { if(function_exists('gmstrftime')) { // gmstrftime doesn't exist in some versions // of PHP $t=gmstrftime("%Y%m%dT%H:%M:%S", $timet); } else { $t=strftime("%Y%m%dT%H:%M:%S", $timet-date('Z')); } } return $t; } /** * Given an ISO8601 date string, return a timet in the localtime, or UTC * @param string $idate * @param int $utc either 0 or 1 * @return int (datetime) */ function iso8601_decode($idate, $utc=0) { $t=0; if(preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/', $idate, $regs)) { if($utc) { $t=gmmktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); } else { $t=mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); } } return $t; } /** * Takes an xmlrpc value in PHP xmlrpcval object format and translates it into native PHP types. * * Works with xmlrpc message objects as input, too. * * Given proper options parameter, can rebuild generic php object instances * (provided those have been encoded to xmlrpc format using a corresponding * option in php_xmlrpc_encode()) * PLEASE NOTE that rebuilding php objects involves calling their constructor function. * This means that the remote communication end can decide which php code will * get executed on your server, leaving the door possibly open to 'php-injection' * style of attacks (provided you have some classes defined on your server that * might wreak havoc if instances are built outside an appropriate context). * Make sure you trust the remote server/client before eanbling this! * * @author Dan Libby (dan@libby.com) * * @param xmlrpcval $xmlrpc_val * @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php objects; if 'dates_as_objects' is set xmlrpc datetimes are decoded as php DateTime objects (standard is * @return mixed */ function php_xmlrpc_decode($xmlrpc_val, $options=array()) { switch($xmlrpc_val->kindOf()) { case 'scalar': if (in_array('extension_api', $options)) { reset($xmlrpc_val->me); list($typ,$val) = each($xmlrpc_val->me); switch ($typ) { case 'dateTime.iso8601': $xmlrpc_val->scalar = $val; $xmlrpc_val->xmlrpc_type = 'datetime'; $xmlrpc_val->timestamp = iso8601_decode($val); return $xmlrpc_val; case 'base64': $xmlrpc_val->scalar = $val; $xmlrpc_val->type = $typ; return $xmlrpc_val; default: return $xmlrpc_val->scalarval(); } } if (in_array('dates_as_objects', $options) && $xmlrpc_val->scalartyp() == 'dateTime.iso8601') { // we return a Datetime object instead of a string // since now the constructor of xmlrpcval accepts safely strings, ints and datetimes, // we cater to all 3 cases here $out = $xmlrpc_val->scalarval(); if (is_string($out)) { $out = strtotime($out); } if (is_int($out)) { $result = new Datetime(); $result->setTimestamp($out); return $result; } elseif (is_a($out, 'Datetime')) { return $out; } } return $xmlrpc_val->scalarval(); case 'array': $size = $xmlrpc_val->arraysize(); $arr = array(); for($i = 0; $i < $size; $i++) { $arr[] = php_xmlrpc_decode($xmlrpc_val->arraymem($i), $options); } return $arr; case 'struct': $xmlrpc_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) && $xmlrpc_val->_php_class != '' && class_exists($xmlrpc_val->_php_class)) { $obj = @new $xmlrpc_val->_php_class; while(list($key,$value)=$xmlrpc_val->structeach()) { $obj->$key = php_xmlrpc_decode($value, $options); } return $obj; } else { $arr = array(); while(list($key,$value)=$xmlrpc_val->structeach()) { $arr[$key] = php_xmlrpc_decode($value, $options); } return $arr; } case 'msg': $paramcount = $xmlrpc_val->getNumParams(); $arr = array(); for($i = 0; $i < $paramcount; $i++) { $arr[] = php_xmlrpc_decode($xmlrpc_val->getParam($i)); } return $arr; } } // This constant left here only for historical reasons... // it was used to decide if we have to define xmlrpc_encode on our own, but // we do not do it anymore if(function_exists('xmlrpc_decode')) { define('XMLRPC_EPI_ENABLED','1'); } else { define('XMLRPC_EPI_ENABLED','0'); } /** * Takes native php types and encodes them into xmlrpc PHP object format. * It will not re-encode xmlrpcval objects. * * Feature creep -- could support more types via optional type argument * (string => datetime support has been added, ??? => base64 not yet) * * If given a proper options parameter, php object instances will be encoded * into 'special' xmlrpc values, that can later be decoded into php objects * by calling php_xmlrpc_decode() with a corresponding option * * @author Dan Libby (dan@libby.com) * * @param mixed $php_val the value to be converted into an xmlrpcval object * @param array $options can include 'encode_php_objs', 'auto_dates', 'null_extension' or 'extension_api' * @return xmlrpcval */ function php_xmlrpc_encode($php_val, $options=array()) { $xmlrpc = Phpxmlrpc::instance(); $type = gettype($php_val); switch($type) { case 'string': if (in_array('auto_dates', $options) && preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $php_val)) $xmlrpc_val = new xmlrpcval($php_val, $xmlrpc->xmlrpcDateTime); else $xmlrpc_val = new xmlrpcval($php_val, $xmlrpc->xmlrpcString); break; case 'integer': $xmlrpc_val = new xmlrpcval($php_val, $xmlrpc->xmlrpcInt); break; case 'double': $xmlrpc_val = new xmlrpcval($php_val, $xmlrpc->xmlrpcDouble); break; // // Add support for encoding/decoding of booleans, since they are supported in PHP case 'boolean': $xmlrpc_val = new xmlrpcval($php_val, $xmlrpc->xmlrpcBoolean); break; // case 'array': // PHP arrays can be encoded to either xmlrpc structs 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_xmlrpc_encode($val, $options); if(!$ko && $key !== $j) { $ko = true; } $j++; } if($ko) { $xmlrpc_val = new xmlrpcval($arr, $xmlrpc->xmlrpcStruct); } else { $xmlrpc_val = new xmlrpcval($arr, $xmlrpc->xmlrpcArray); } break; case 'object': if(is_a($php_val, 'xmlrpcval')) { $xmlrpc_val = $php_val; } else if(is_a($php_val, 'DateTime')) { $xmlrpc_val = new xmlrpcval($php_val->format('Ymd\TH:i:s'), $xmlrpc->xmlrpcStruct); } else { $arr = array(); reset($php_val); while(list($k,$v) = each($php_val)) { $arr[$k] = php_xmlrpc_encode($v, $options); } $xmlrpc_val = new xmlrpcval($arr, $xmlrpc->xmlrpcStruct); if (in_array('encode_php_objs', $options)) { // let's save original class name into xmlrpcval: // might be useful later on... $xmlrpc_val->_php_class = get_class($php_val); } } break; case 'NULL': if (in_array('extension_api', $options)) { $xmlrpc_val = new xmlrpcval('', $xmlrpc->xmlrpcString); } else if (in_array('null_extension', $options)) { $xmlrpc_val = new xmlrpcval('', $xmlrpc->xmlrpcNull); } else { $xmlrpc_val = new xmlrpcval(); } break; case 'resource': if (in_array('extension_api', $options)) { $xmlrpc_val = new xmlrpcval((int)$php_val, $xmlrpc->xmlrpcInt); } else { $xmlrpc_val = new xmlrpcval(); } // catch "user function", "unknown type" default: // giancarlo pinerolo // it has to return // an empty object in case, not a boolean. $xmlrpc_val = new xmlrpcval(); break; } return $xmlrpc_val; } /** * Convert the xml representation of a method response, method request or single * xmlrpc value into the appropriate object (a.k.a. deserialize) * @param string $xml_val * @param array $options * @return mixed false on error, or an instance of either xmlrpcval, xmlrpcmsg or xmlrpcresp */ function php_xmlrpc_decode_xml($xml_val, $options=array()) { $xmlrpc = Phpxmlrpc::instance(); $xmlrpc->_xh = array(); $xmlrpc->_xh['ac'] = ''; $xmlrpc->_xh['stack'] = array(); $xmlrpc->_xh['valuestack'] = array(); $xmlrpc->_xh['params'] = array(); $xmlrpc->_xh['pt'] = array(); $xmlrpc->_xh['isf'] = 0; $xmlrpc->_xh['isf_reason'] = ''; $xmlrpc->_xh['method'] = false; $xmlrpc->_xh['rt'] = ''; /// @todo 'guestimate' encoding $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true); // What if internal encoding is not in one of the 3 allowed? // we use the broadest one, ie. utf8! if (!in_array($xmlrpc->xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); } else { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $xmlrpc->xmlrpc_internalencoding); } xml_set_element_handler($parser, 'xmlrpc_se_any', 'xmlrpc_ee'); xml_set_character_data_handler($parser, 'xmlrpc_cd'); xml_set_default_handler($parser, 'xmlrpc_dh'); if(!xml_parse($parser, $xml_val, 1)) { $errstr = sprintf('XML error: %s at line %d, column %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser), xml_get_current_column_number($parser)); error_log($errstr); xml_parser_free($parser); return false; } xml_parser_free($parser); if ($xmlrpc->_xh['isf'] > 1) // test that $xmlrpc->_xh['value'] is an obj, too??? { error_log($xmlrpc->_xh['isf_reason']); return false; } switch ($xmlrpc->_xh['rt']) { case 'methodresponse': $v =& $xmlrpc->_xh['value']; if ($xmlrpc->_xh['isf'] == 1) { $vc = $v->structmem('faultCode'); $vs = $v->structmem('faultString'); $r = new xmlrpcresp(0, $vc->scalarval(), $vs->scalarval()); } else { $r = new xmlrpcresp($v); } return $r; case 'methodcall': $m = new xmlrpcmsg($xmlrpc->_xh['method']); for($i=0; $i < count($xmlrpc->_xh['params']); $i++) { $m->addParam($xmlrpc->_xh['params'][$i]); } return $m; case 'value': return $xmlrpc->_xh['value']; default: return false; } } /** * decode a string that is encoded w/ "chunked" transfer encoding * as defined in rfc2068 par. 19.4.6 * code shamelessly stolen from nusoap library by Dietrich Ayala * * @param string $buffer the string to be decoded * @return string */ function decode_chunked($buffer) { // length := 0 $length = 0; $new = ''; // read chunk-size, chunk-extension (if any) and crlf // get the position of the linebreak $chunkend = strpos($buffer,"\r\n") + 2; $temp = substr($buffer,0,$chunkend); $chunk_size = hexdec( trim($temp) ); $chunkstart = $chunkend; while($chunk_size > 0) { $chunkend = strpos($buffer, "\r\n", $chunkstart + $chunk_size); // just in case we got a broken connection if($chunkend == false) { $chunk = substr($buffer,$chunkstart); // append chunk-data to entity-body $new .= $chunk; $length += strlen($chunk); break; } // read chunk-data and crlf $chunk = substr($buffer,$chunkstart,$chunkend-$chunkstart); // append chunk-data to entity-body $new .= $chunk; // length := length + chunk-size $length += strlen($chunk); // read chunk-size and crlf $chunkstart = $chunkend + 2; $chunkend = strpos($buffer,"\r\n",$chunkstart)+2; if($chunkend == false) { break; //just in case we got a broken connection } $temp = substr($buffer,$chunkstart,$chunkend-$chunkstart); $chunk_size = hexdec( trim($temp) ); $chunkstart = $chunkend; } return $new; } /** * xml charset encoding guessing helper function. * Tries to determine the charset encoding of an XML chunk received over HTTP. * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type, * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of unconforming (legacy?) clients/servers, * which will be most probably using UTF-8 anyway... * * @param string $httpheader the http Content-type header * @param string $xmlchunk xml content buffer * @param string $encoding_prefs comma separated list of character encodings to be used as default (when mb extension is enabled) * @return string * * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!! */ function guess_encoding($httpheader='', $xmlchunk='', $encoding_prefs=null) { $xmlrpc = Phpxmlrpc::instance(); // discussion: see http://www.yale.edu/pclt/encoding/ // 1 - test if encoding is specified in HTTP HEADERS //Details: // LWS: (\13\10)?( |\t)+ // token: (any char but excluded stuff)+ // quoted string: " (any char but double quotes and cointrol chars)* " // header: Content-type = ...; charset=value(; ...)* // where value is of type token, no LWS allowed between 'charset' and value // Note: we do not check for invalid chars in VALUE: // this had better be done using pure ereg as below // Note 2: we might be removing whitespace/tabs that ought to be left in if // the received charset is a quoted string. But nobody uses such charset names... /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it? $matches = array(); if(preg_match('/;\s*charset\s*=([^;]+)/i', $httpheader, $matches)) { return strtoupper(trim($matches[1], " \t\"")); } // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern // (source: http://www.w3.org/TR/2000/REC-xml-20001006) // NOTE: actually, according to the spec, even if we find the BOM and determine // an encoding, we should check if there is an encoding specified // in the xml declaration, and verify if they match. /// @todo implement check as described above? /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM) if(preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlchunk)) { return 'UCS-4'; } elseif(preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlchunk)) { return 'UTF-16'; } elseif(preg_match('/^(\xEF\xBB\xBF)/', $xmlchunk)) { return 'UTF-8'; } // 3 - test if encoding is specified in the xml declaration // Details: // SPACE: (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+ // EQ: SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]* if (preg_match('/^<\?xml\s+version\s*=\s*'. "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))". '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/", $xmlchunk, $matches)) { return strtoupper(substr($matches[2], 1, -1)); } // 4 - if mbstring is available, let it do the guesswork // NB: we favour finding an encoding that is compatible with what we can process if(extension_loaded('mbstring')) { if($encoding_prefs) { $enc = mb_detect_encoding($xmlchunk, $encoding_prefs); } else { $enc = mb_detect_encoding($xmlchunk); } // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII... // IANA also likes better US-ASCII, so go with it if($enc == 'ASCII') { $enc = 'US-'.$enc; } return $enc; } else { // no encoding specified: as per HTTP1.1 assume it is iso-8859-1? // Both RFC 2616 (HTTP 1.1) and 1945 (HTTP 1.0) clearly state that for text/xxx content types // this should be the standard. And we should be getting text/xml as request and response. // BUT we have to be backward compatible with the lib, which always used UTF-8 as default... return $xmlrpc->xmlrpc_defencoding; } } /** * Checks if a given charset encoding is present in a list of encodings or * if it is a valid subset of any encoding in the list * @param string $encoding charset to be tested * @param mixed $validlist comma separated list of valid charsets (or array of charsets) * @return bool */ function is_valid_charset($encoding, $validlist) { $charset_supersets = array( 'US-ASCII' => array ('ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-11', 'ISO-8859-12', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'UTF-8', 'EUC-JP', 'EUC-', 'EUC-KR', 'EUC-CN') ); if (is_string($validlist)) $validlist = explode(',', $validlist); if (@in_array(strtoupper($encoding), $validlist)) return true; else { if (array_key_exists($encoding, $charset_supersets)) foreach ($validlist as $allowed) if (in_array($allowed, $charset_supersets[$encoding])) return true; return false; } } ?>