* Invision Power Services * IP.Board v3.4.9 * XML-RPC Server: Send, receive and process XML-RPC requests * Last Updated: $Date: 2012-05-10 16:10:13 -0400 (Thu, 10 May 2012) $ * * * @author $Author: bfarber $ * @copyright (c) 2001 - 2009 Invision Power Services, Inc. * @license http://www.invisionpower.com/company/standards.php#license * @package IP.Board * @subpackage Kernel * @link http://www.invisionpower.com * @since 6th January 2006 * @version $Revision: 10721 $ * * * Example Usage: * * SENDING XML-RPC Request * (Optional) * $xmlrpc->map_type_to_key['key'] = 'string'; * $xmlrpc->map_type_to_key['key2'] = 'base64'; * $return = $xmlrpc->sendXmlRpc( 'http://domain.com/xml-rpc_server.php', 'methodNameHere', array( 'key' => 'value', 'key2' => 'value2' ) ); * if ( $xmlrpc->errors ) * { * print_r( $xmlrpc->errors ); * } * * Decoding XML-RPC * $xmlrpc->decode( $raw_xmlrpc_text ); * * print_r( $xmlrpc->xmlrpc_params ); * RETURN * $xmlrpc->returnTrue(); * * */ class classXmlRpc { /** * XML header - this is prepended to every request * * @var string */ public $header = ""; /** * Character-set encoding to use * * @var string */ public $doc_type = 'UTF-8'; /** * Array of error messages * * @var array */ public $errors = array(); /** * Variable types (do not change - the specification only accounts for certain variable types) * * @var array */ public $var_types = array('string', 'int', 'i4', 'double', 'dateTime.iso8601', 'base64', 'boolean'); /** * Method to call * * @var string */ public $xmlrpc_method_call = ''; /** * Extracted xmlrpc params * * @var array */ public $xmlrpc_params = array(); /** * Optionally map types to key * * @var array */ public $map_type_to_key = array(); /**#@+ * HTTP-authentication username and password * * @var string */ public $auth_user = ''; public $auth_pass = ''; /**#@-*/ /** * Decode an XML RPC document * * @param string XML-RPC data * @return @e string */ public function decodeXmlRpc( $_xml ) { $xml_parser = new xmlRpcParser(); $data = $xml_parser->parse( $_xml ); if ( isset( $data['methodResponse']['fault'] ) ) { $tmp = $this->adjustValue( $data['methodResponse']['fault']['value'] ); $this->errors[] = $tmp['faultString']; } $this->xmlrpc_params = $this->getParams( $data ); $this->xmlrpc_method_call = $this->getMethodName( $data ); //----------------------------------------- // Debug? //----------------------------------------- if ( IPS_XML_RPC_DEBUG_ON ) { $this->addDebug( "DECODING XML data: " . $_xml ); $this->addDebug( "DECODE RESULT XML data: " . var_export( $data, TRUE ) ); } return $data; } /** * Adjust value of parameter and return it properly cast * * @param string Curernt node * @return @e mixed */ public function & adjustValue( &$current_node ) { if ( is_array( $current_node ) ) { if ( isset($current_node['array']) ) { if ( ! is_array($current_node['array']['data']) ) { $temp = array(); } else { $temp = &$current_node['array']['data']['value']; if ( is_array($temp) and array_key_exists(0, $temp) ) { $count = count($temp); for( $n = 0 ; $n < $count ; $n++ ) { $temp2[$n] = & $this->adjustValue($temp[$n]); } $temp = &$temp2; } else { $temp2 = & $this->adjustValue($temp); $temp = array(&$temp2); } } } elseif ( isset($current_node['struct']) ) { if ( ! is_array($current_node['struct']) ) { return array(); } else { $temp = &$current_node['struct']['member']; if ( is_array($temp) and array_key_exists(0, $temp) ) { $count = count($temp); for( $n = 0 ; $n < $count ; $n++ ) { $temp2[$temp[$n]['name']] = & $this->adjustValue($temp[$n]['value']); } } else { $temp2[$temp['name']] = & $this->adjustValue($temp['value']); } $temp = &$temp2; } } else { $got_it = false; foreach( $this->var_types as $type ) { if ( array_key_exists($type, $current_node) ) { $temp = &$current_node[$type]; $got_it = true; break; } } if ( ! $got_it ) { $type = 'string'; } switch ($type) { case 'int': case 'i4': case 'integer': case 'integar': $temp = (int) $temp; break; case 'string': $temp = (string) $temp; break; case 'double': $temp = (double) $temp; break; case 'boolean': $temp = (bool) $temp; break; case 'base64': $temp = base64_decode(trim($temp)); break; } } } else { $temp = (string) $current_node; } return $temp; } /** * Get the params from the XML RPC return * * @param array Request data * @return @e array */ public function getParams( $request ) { if ( isset( $request['methodCall']['params'] ) AND is_array( $request['methodCall']['params'] ) ) { $temp = & $request['methodCall']['params']['param']; } else if ( isset( $request['methodResponse']['params'] ) AND is_array( $request['methodResponse']['params'] ) ) { $temp = & $request['methodResponse']['params']['param']; } else { return array(); } if ( is_array( $temp ) and array_key_exists( 0, $temp ) ) { $count = count($temp); for( $n = 0 ; $n < $count ; $n++) { $temp2[$n] = & $this->adjustValue($temp[$n]['value']); } } else { $temp2[0] = & $this->adjustValue($temp['value']); } $temp = &$temp2; return $temp; } /** * Get RPC method name * * @param array Request params * @return @e string */ public function getMethodName( $request ) { return isset( $request['methodCall']['methodName'] ) ? $request['methodCall']['methodName'] : ''; } /** * Create and send an XML document, returning boolean if there is a problem, or the decoded response * * @param string URL to send XML-RPC data to * @param string Method name to request * @param array Array of fields to send (must be in key => value pairings) * @return @e mixed */ public function sendXmlRpc( $url, $method_name='', $data_array=array() ) { //----------------------------------------- // Build RPC request //----------------------------------------- $xmldata = $this->buildDocument( $data_array, $method_name ); if ( $xmldata ) { //----------------------------------------- // Debug? //----------------------------------------- if ( IPS_XML_RPC_DEBUG_ON ) { $this->addDebug( "SENDING XML data: " . $xmldata ); } //----------------------------------------- // Continue //----------------------------------------- return $this->post( $url, $xmldata ); } else { return false; } } /** * Prints a document with a single value and exits * * @param string Value * @return @e void */ public function returnValue( $value ) { $this->generateHeader(); $to_print = $this->header." {$value} "; @header( "Connection: close" ); @header( "Content-length: " . strlen($to_print) ); @header( "Content-type: text/xml" ); @header( "Date: " . date("r") ); print $to_print; $this->addDebug( "ReturnValue: {$to_print}" ); exit(); } /** * Creates an XML-RPC complex document * * @param array Array of fields to send (must be in key => value pairings) * @param string Method name (optional) * @return @e string */ public function buildDocument( $data_array, $method_name='' ) { //----------------------------------------- // INIT //----------------------------------------- $xmldata = ""; $root_tag = 'methodCall'; //----------------------------------------- // Test //----------------------------------------- if ( ! is_array( $data_array ) or ! count( $data_array ) ) { return false; } if ( ! $method_name ) { $root_tag = 'methodResponse'; } $this->generateHeader(); $xmldata = $this->header . "\n"; $xmldata .= "<".$root_tag.">\n"; if ( $method_name ) { $xmldata .= "\t".$method_name."\n"; } $xmldata .= "\t\n"; $xmldata .= "\t\t\n"; $xmldata .= "\t\t\t\n"; if ( isset( $data_array[0] ) AND is_array( $data_array[0] ) ) { $xmldata .= "\t\t\t\n"; $xmldata .= "\t\t\t\t\n"; foreach( $data_array as $k => $v ) { $xmldata .= "\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\t\n"; foreach( $v as $k2 => $v2 ) { $_type = $this->map_type_to_key[ $k2 ] ? $this->map_type_to_key[ $k2 ] : $this->getStringType( $v2 ); $xmldata .= "\t\t\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\t\t\t".$k2."\n"; if ( strpos( $v2, '>' ) !== false OR strpos( $v2, '<' ) !== false OR strpos( $v2, '&' ) !== false ) { $xmldata .= "\t\t\t\t\t\t\t\t" . base64_encode($v2) . "\n"; } else { $xmldata .= "\t\t\t\t\t\t\t\t<".$_type.">" . htmlspecialchars($v2) . "\n"; } $xmldata .= "\t\t\t\t\t\t\t\n"; } $xmldata .= "\t\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\n"; } $xmldata .= "\t\t\t\t\n"; $xmldata .= "\t\t\t\n"; } else { $xmldata .= "\t\t\t\n"; foreach( $data_array as $k => $v ) { if ( is_array( $v ) ) { $xmldata .= $this->_buildDocumentRecurse( "", $k, $v, 4 ); } else { $_type = isset( $this->map_type_to_key[ $k ] ) ? $this->map_type_to_key[ $k ] : $this->getStringType( $v ); $xmldata .= "\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t".$k."\n"; if ( strpos( $v, '>' ) !== false OR strpos( $v, '<' ) !== false OR strpos( $v, '&' ) !== false ) { $xmldata .= "\t\t\t\t\t\t\t\t" . base64_encode($v) . "\n"; } else { $xmldata .= "\t\t\t\t\t<".$_type.">" . htmlspecialchars($v) . "\n"; } $xmldata .= "\t\t\t\t\n"; } } $xmldata .= "\t\t\t\n"; } $xmldata .= "\t\t\t\n"; $xmldata .= "\t\t\n"; $xmldata .= "\t\n"; $xmldata .= ""; return $xmldata; } /** * Recursive method to build document * * @param string XML Data * @param string Key * @param string Value * @param integer Depth * @return @e string */ protected function _buildDocumentRecurse( $xmldata, $k, $v, $depth=4 ) { $xmldata .= "\t\n"; $xmldata .= "\t\t".$k."\n"; $xmldata .= "\t\t\n"; $xmldata .= "\t\t\t\n"; $xmldata .= "\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\t\n"; foreach( $v as $_k => $_v ) { if ( is_array( $_v ) ) { $depth++; $xmldata .= $this->_buildDocumentRecurse( $xmldata, $k, $v, $depth ); } else { $_type = isset( $this->map_type_to_key[ $_k ] ) ? $this->map_type_to_key[ $_k ] : $this->getStringType( $_v ); $xmldata .= "\t\t\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\t\t\t".$_k."\n"; if ( strpos( $_v, '>' ) !== false OR strpos( $_v, '<' ) !== false OR strpos( $_v, '&' ) !== false ) { $xmldata .= "\t\t\t\t\t\t\t\t" . base64_encode($_v) . "\n"; } else { $xmldata .= "\t\t\t\t\t<".$_type.">" . htmlspecialchars($_v) . "\n"; } $xmldata .= "\t\t\t\t\t\t\t\n"; } } $xmldata .= "\t\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\t\n"; $xmldata .= "\t\t\t\t\n"; $xmldata .= "\t\t\t\n"; $xmldata .= "\t\t\n"; $xmldata .= "\t\n"; return $xmldata; } /** * Prints a true document and exits * * @return @e void */ public function returnTrue() { $this->generateHeader(); $to_print = $this->header." 1 "; @header( "Connection: close" ); @header( "Content-length: ".strlen($to_print) ); @header( "Content-type: text/xml" ); @header( "Date: " . date("r") ); print $to_print; $this->addDebug( "ReturnTrue: {$to_print}" ); exit(); } /** * Prints a document and exits * * @param array Array of params to return in key => value pairs * @return @e void */ public function returnParams( $data_array ) { $to_print = $this->buildDocument( $data_array ); @header( "Connection: close" ); @header( "Content-length: ".strlen($to_print) ); @header( "Content-type: text/xml" ); @header( "Date: " . date("r") ); @header( "Pragma: no-cache" ); @header( "Cache-Control: no-cache" ); print $to_print; $this->addDebug( "ReturnParams: {$to_print}" ); exit(); } /** * Prints an error document and exits * * @param int Error code * @param string Error Message * @return @e void */ public function returnError( $error_code, $error_msg ) { $this->generateHeader(); $to_print = $this->header . " faultCode ".intval($error_code)." faultString ".$error_msg." "; @header( "Connection: close" ); @header( "Content-length: ".strlen($to_print) ); @header( "Content-type: text/xml" ); @header( "Date: " . date("r") ); print $to_print; exit(); } /** * Create and send an XML document. Returns false on error, or the decoded XML-RPC response as a string. * * @param string URL to send XML-RPC data to * @param array XML-RPC data * @return @e mixed */ public function post( $file_location, $xmldata='' ) { //----------------------------------------- // INIT //----------------------------------------- $data = null; $fsocket_timeout = 10; $header = ""; //----------------------------------------- // Send it.. //----------------------------------------- $url_parts = parse_url($file_location); if ( ! $url_parts['host'] ) { $this->errors[] = "No host found in the URL '{$file_location}'!"; return false; } //------------------------------- // Finalize //------------------------------- $host = $url_parts['host']; $_host = $url_parts['scheme'] == 'https' ? 'ssl://' . $host : $host; $port = ( isset($url_parts['port']) ) ? $url_parts['port'] : ( $url_parts['scheme'] == 'https' ? 443 : 80 ); //----------------------------------------- // User and pass? //----------------------------------------- if ( ! $this->auth_user AND !empty($url_parts['user']) ) { $this->auth_user = $url_parts['user']; $this->auth_pass = $url_parts['pass']; } //------------------------------- // Tidy up path //------------------------------- if ( ! empty( $url_parts["path"] ) ) { $path = $url_parts["path"]; } else { $path = "/"; } if ( ! empty( $url_parts["query"] ) ) { $path .= "?" . $url_parts["query"]; } if ( ! $fp = @fsockopen( $_host, $port, $errno, $errstr, $fsocket_timeout ) ) { $this->errors[] = "CONNECTION REFUSED FROM {$host}"; return FALSE; } else { $header = "POST $path HTTP/1.0\r\n"; $header .= "User-Agent: IPS XML-RPC Client Library (\$Revision: 10721 $)\r\n"; $header .= "Host: $host\r\n"; if ( $this->auth_user && $this->auth_pass ) { $this->addDebug( "Authorization: Basic Performed" ); $header .= "Authorization: Basic ".base64_encode("{$this->auth_user}:{$this->auth_pass}")."\r\n"; } $header .= "Connection: close\r\n"; $header .= "Content-Type: text/xml\r\n"; $header .= "Content-Length: " . strlen($xmldata) . "\r\n\r\n"; if ( ! fputs( $fp, $header . $xmldata ) ) { $this->errors[] = "Unable to send request to $host!"; return FALSE; } } @stream_set_timeout($fp, $fsocket_timeout); $status = @socket_get_status($fp); while( ! feof($fp) && ! $status['timed_out'] ) { $data .= fgets ( $fp, 8192 ); $status = socket_get_status($fp); } fclose ($fp); //------------------------------- // Strip headers //------------------------------- $tmp = explode("\r\n\r\n", $data, 2); $data = trim($tmp[1]); //----------------------------------------- // Debug? //----------------------------------------- if ( IPS_XML_RPC_DEBUG_ON ) { if( $this->auth_pass ) { $_pass = str_repeat( 'x', strlen( $this->auth_pass ) - 1 ) . substr( $this->auth_pass, -1 ); } else { $_pass = ''; } $this->addDebug( "POST RESPONSE to {$this->auth_user}:{$_pass}@$host{$path}: " . $data ); } //----------------------------------------- // Continue //----------------------------------------- return $this->decodeXmlRpc( $data ); } /** * Get the XML-RPC string type * * @param string String * @return @e string */ public function getStringType( $string ) { $type = gettype( $string ); switch( $type ) { default: case 'string': $type = 'string'; break; case 'integer': $type = 'int'; break; case 'double': $type = 'double'; break; case 'null': case 'boolean': $type = 'boolean'; break; } return $type; } /** * Add debug message * * @param string Log message * @return @e boolean */ public function addDebug( $msg ) { if ( IPS_XML_RPC_DEBUG_FILE AND IPS_XML_RPC_DEBUG_ON ) { $full_msg = "==================================================================\n" . "SCRIPT NAME: " . $_SERVER["SCRIPT_NAME"] . "\n" . gmdate( 'r' ) . ' - ' . $_SERVER['REMOTE_ADDR'] . ' - ' . $msg . "\n" . "==================================================================\n"; if ( $FH = @fopen( IPS_XML_RPC_DEBUG_FILE, 'a+' ) ) { fwrite( $FH, $full_msg, strlen( $full_msg ) ); fclose( $FH ); } } return true; } /** * Create the XML header * * @access protected * @return @e void */ protected function generateHeader() { $this->header = 'doc_type . '" ?>'; } } class xmlRpcParser { /** * Parser object * * @var object */ public $parser; /** * Current document * * @var string */ public $document; /** * Current tag * * @var string */ public $current; /** * Parent tag * * @var string */ public $parent; /** * Parents * * @var array */ public $parents; /** * Last opened tag * * @var string */ public $last_opened_tag; /** * Constructor * * @param string Data * @return @e void */ public function __construct( $data=null ) { $this->parser = xml_parser_create(); xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0); xml_set_object( $this->parser, $this); xml_set_element_handler( $this->parser, "_rpcOpen", "_rpcClose"); xml_set_character_data_handler($this->parser, "_rpcData"); } /** * Destructor * * @return @e void */ public function __destruct() { xml_parser_free( $this->parser ); } /** * Parse the XML data * * @param string Data * @return @e string */ public function parse( $data ) { //----------------------------------------- // INIT //----------------------------------------- $this->document = array(); $this->parent = &$this->document; $this->parents = array(); $this->last_opened_tag = NULL; //----------------------------------------- // Parse //----------------------------------------- xml_parse($this->parser, $data); //----------------------------------------- // Return... //----------------------------------------- $tmp = $this->document; return $tmp; } /** * Open handler for XML object * * @param object Parser reference * @param string Tag * @param array Attributes * @return @e void */ protected function _rpcOpen($parser, $tag, $attributes) { //----------------------------------------- // INIT //----------------------------------------- $this->data = ""; $this->last_opened_tag = $tag; if ( array_key_exists( $tag, $this->parent ) ) { if ( is_array( $this->parent[$tag] ) and array_key_exists( 0, $this->parent[$tag] ) ) { $key = is_array( $this->parent[$tag] ) ? count( array_filter( array_keys($this->parent[$tag]), 'is_numeric' ) ) : 0; } else { $temp = &$this->parent[$tag]; unset($this->parent[$tag]); $this->parent[$tag][0] = &$temp; if ( array_key_exists( $tag ." attr", $this->parent ) ) { $temp = &$this->parent[ $tag ." attr" ]; unset($this->parent[ $tag ." attr" ]); $this->parent[$tag]["0 attr"] = &$temp; } $key = 1; } $this->parent = &$this->parent[$tag]; } else { $key = $tag; } if ( $attributes ) { $this->parent[ $key ." attr" ] = $attributes; } $this->parent[$key] = array(); $this->parent = &$this->parent[$key]; //array_unshift($this->parents, &$this->parent); $this->_arrayUnshiftReference($this->parents, $this->parent); } /** * Array unshift wrapper * * @param array Array * @param string Value * @return @e array */ protected function _arrayUnshiftReference(&$array, &$value) { $return = array_unshift($array,''); $array[0] =& $value; return $return; } /** * XML data handler * * @param object Parser reference * @param string Data * @return @e void */ protected function _rpcData($parser, $data) { if ( $this->last_opened_tag != NULL ) { $this->data .= $data; } } /** * XML close handler * * @param object Parser reference * @param string Tag * @return @e void */ protected function _rpcClose($parser, $tag) { if ( $this->last_opened_tag == $tag ) { $this->parent = $this->data; $this->last_opened_tag = NULL; } array_shift($this->parents); $this->parent = &$this->parents[0]; } }