. /** * Plugin to check submitted notices with Mollom * * Mollom is a bayesian spam checker, wrapped into a webservice * This plugin is based on the Drupal Mollom Plugin * * @category Plugin * @package GNUsocial * @author Brenda Wallace * @copyright 2010 StatusNet, Inc. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ defined('GNUsocial') || die(); define('MOLLOMPLUGIN_VERSION', '0.1'); define('MOLLOM_API_VERSION', '1.0'); define('MOLLOM_ANALYSIS_UNKNOWN', 0); define('MOLLOM_ANALYSIS_HAM', 1); define('MOLLOM_ANALYSIS_SPAM', 2); define('MOLLOM_ANALYSIS_UNSURE', 3); define('MOLLOM_MODE_DISABLED', 0); define('MOLLOM_MODE_CAPTCHA', 1); define('MOLLOM_MODE_ANALYSIS', 2); define('MOLLOM_FALLBACK_BLOCK', 0); define('MOLLOM_FALLBACK_ACCEPT', 1); define('MOLLOM_ERROR', 1000); define('MOLLOM_REFRESH', 1100); define('MOLLOM_REDIRECT', 1200); /** * Plugin to check submitted notices with Mollom * * Mollom is a bayesian spam filter provided by webservice. * * @category Plugin * @package GNUsocial * @author Brenda Wallace * @copyright 2010 StatusNet, Inc. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * * @see Event */ class MollomPlugin extends Plugin { public $public_key; public $private_key; public $servers = null; public function onStartNoticeSave($notice) { if ($this->public_key) { //Check spam $data = [ 'post_body' => $notice->content, 'author_name' => $profile->nickname, 'author_url' => $profile->homepage, 'author_id' => $profile->id, 'author_ip' => $this->getClientIp(), ]; $response = $this->mollom('mollom.checkContent', $data); switch ($response['spam']) { case MOLLOM_ANALYSIS_SPAM: // TRANS: Client exception thrown when notice content triggers the spam filter. throw new ClientException(_m('Spam Detected.'), 400); case MOLLOM_ANALYSIS_UNSURE: //if unsure, let through break; case MOLLOM_ANALYSIS_HAM: // all good! :-) break; } } return true; } public function getClientIP() { if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { // Note: order matters here; use proxy-forwarded stuff first foreach (array('HTTP_X_FORWARDED_FOR', 'CLIENT-IP', 'REMOTE_ADDR') as $k) { if (isset($_SERVER[$k])) { return $_SERVER[$k]; } } } return '127.0.0.1'; } /** * Call a remote procedure at the Mollom server. This function will * automatically add the information required to authenticate against * Mollom. */ public function mollom($method, $data = []) { if (!extension_loaded('xmlrpc')) { if (!dl('xmlrpc.so')) { common_log(LOG_ERR, "Can't pingback; xmlrpc extension not available."); } } // Construct the server URL: $public_key = $this->public_key; // Retrieve the list of Mollom servers from the database: $servers = $this->servers; if (is_null($servers)) { // Retrieve a list of valid Mollom servers from mollom.com: $servers = $this->xmlrpc('http://xmlrpc.mollom.com/'. MOLLOM_API_VERSION, 'mollom.getServerList', $this->authentication()); // Store the list of servers in the database: // @todo variable_set('mollom_servers', $servers); } if (is_array($servers)) { // Send the request to the first server, if that fails, try the other servers in the list: foreach ($servers as $server) { $auth = $this->authentication(); $data = array_merge($data, $auth); $result = $this->xmlrpc( $server . '/' . MOLLOM_API_VERSION, $method, $data ); // Debug output: if (array_key_exists('session_id', $data)) { common_debug( "called {$method} at server {$server} with session ID " . "'{$data['session_id']}'" ); } else { common_debug( "called {$method} at server {$server} with no session ID" ); } if (!empty($errno = $this->xmlrpc_errno())) { common_log(LOG_ERR, sprintf( 'Error @errno: %s - %s - %s -
%s
', $this->xmlrpc_errno(), $server, $this->xmlrpc_error_msg(), $method, print_r($data, true) )); if ($errno === MOLLOM_REFRESH) { // Retrieve a list of valid Mollom servers from mollom.com: $servers = $this->xmlrpc( 'http://xmlrpc.mollom.com/' . MOLLOM_API_VERSION, 'mollom.getServerList', $this->authentication() ); // Store the updated list of servers in the database: // @todo variable_set('mollom_servers', $servers); } elseif ($errno === MOLLOM_ERROR) { return $result; } elseif ($errno === MOLLOM_REDIRECT) { // Do nothing, we select the next client automatically. } // Reset the XMLRPC error: $this->xmlrpc_error(0); // FIXME: this is crazy. } else { common_debug('Result = ' . print_r($result, true)); return $result; } } } // If none of the servers worked, activate the fallback mechanism: common_debug('none of the servers worked'); // _mollom_fallback(); // If everything failed, we reset the server list to force Mollom to request a new list: //TODO variable_set('mollom_servers', array()); } /** * This function generate an array with all the information required to * authenticate against Mollom. To prevent that requests are forged and * that you are impersonated, each request is signed with a hash computed * based on a private key and a timestamp. * * Both the client and the server share the secret key that is used to * create the authentication hash based on a timestamp. They both hash * the timestamp with the secret key, and if the hashes match, the * authenticity of the message has been validated. * * To avoid that someone can intercept a (hash, timestamp)-pair and * use that to impersonate a client, Mollom will reject the request * when the timestamp is more than 15 minutes off. * * Make sure your server's time is synchronized with the world clocks, * and that you don't share your private key with anyone else. */ private function authentication() { $public_key = $this->public_key; $private_key = $this->private_key; $hash = hash_hmac('sha1', $private_key, $private_key, true); // Store everything in an array. Elsewhere in the code, we'll add the // acutal data before we pass it onto the XML-RPC library: $data['public_key'] = $public_key; $data['time'] = $time; $data['hash'] = $hash; return $data; } public function xmlrpc($url) { //require_once './includes/xmlrpc.inc'; $args = func_get_args(); return call_user_func_array(['MollomPlugin', '_xmlrpc'], $args); } /** * Recursively turn a data structure into objects with 'data' and 'type' attributes. * * @param $data * The data structure. * @param $type * Optional type assign to $data. * @return * Object. */ public function xmlrpc_value($data, $type = false) { $xmlrpc_value = new stdClass(); $xmlrpc_value->data = $data; if (!$type) { $type = $this->xmlrpc_value_calculate_type($xmlrpc_value); } $xmlrpc_value->type = $type; if ($type === 'struct') { // Turn all the values in the array into new xmlrpc_values foreach ($xmlrpc_value->data as $key => $value) { $xmlrpc_value->data[$key] = $this->xmlrpc_value($value); } } if ($type === 'array') { for ($i = 0, $j = count($xmlrpc_value->data); $i < $j; ++$i) { $xmlrpc_value->data[$i] = $this->xmlrpc_value($xmlrpc_value->data[$i]); } } return $xmlrpc_value; } /** * Map PHP type to XML-RPC type. * * @param $xmlrpc_value * Variable whose type should be mapped. * @return * XML-RPC type as string. * @see * http://www.xmlrpc.com/spec#scalars */ public function xmlrpc_value_calculate_type(&$xmlrpc_value) { // http://www.php.net/gettype: Never use gettype() to test for a certain type [...] Instead, use the is_* functions. if (is_bool($xmlrpc_value->data)) { return 'boolean'; } elseif (is_double($xmlrpc_value->data)) { return 'double'; } elseif (is_int($xmlrpc_value->data)) { return 'int'; } elseif (is_array($xmlrpc_value->data)) { // empty or integer-indexed arrays are 'array', string-indexed arrays 'struct' return empty($xmlrpc_value->data) || (range(0, count($xmlrpc_value->data) - 1) === array_keys($xmlrpc_value->data) ? 'array' : 'struct'); } if (is_object($xmlrpc_value->data)) { if ($xmlrpc_value->data->is_date) { return 'date'; } elseif ($xmlrpc_value->data->is_base64) { return 'base64'; } $xmlrpc_value->data = get_object_vars($xmlrpc_value->data); return 'struct'; } // default return 'string'; } /** * Generate XML representing the given value. * * @param $xmlrpc_value * @return * XML representation of value. */ public function xmlrpc_value_get_xml($xmlrpc_value) { switch ($xmlrpc_value->type) { case 'boolean': return ''. (($xmlrpc_value->data) ? '1' : '0') .''; break; case 'int': return ''. $xmlrpc_value->data .''; break; case 'double': return ''. $xmlrpc_value->data .''; break; case 'string': // Note: we don't escape apostrophes because of the many blogging clients // that don't support numerical entities (and XML in general) properly. return ''. htmlspecialchars($xmlrpc_value->data) .''; break; case 'array': $return = "\n"; foreach ($xmlrpc_value->data as $item) { $return .= ' '. $this->xmlrpc_value_get_xml($item) ."\n"; } $return .= ''; return $return; break; case 'struct': $return = "\n"; foreach ($xmlrpc_value->data as $name => $value) { $return .= ' ' . htmlentities($name) . ''; $return .= $this->xmlrpc_value_get_xml($value) . "\n"; } $return .= ''; return $return; break; case 'date': return $this->xmlrpc_date_get_xml($xmlrpc_value->data); break; case 'base64': return $this->xmlrpc_base64_get_xml($xmlrpc_value->data); break; } return false; } /** * Perform an HTTP request. * * This is a flexible and powerful HTTP client implementation. Correctly handles * GET, POST, PUT or any other HTTP requests. Handles redirects. * * @param $url * A string containing a fully qualified URI. * @param $headers * An array containing an HTTP header => value pair. * @param $method * A string defining the HTTP request to use. * @param $data * A string containing data to include in the request. * @param $retry * An integer representing how many times to retry the request in case of a * redirect. * @return * An object containing the HTTP request headers, response code, headers, * data and redirect status. */ public function http_request( $url, $headers = [], $method = 'GET', $data = null, $retry = 3 ) { global $db_prefix; $result = new stdClass(); // Parse the URL and make sure we can handle the schema. $uri = parse_url($url); if ($uri === false) { $result->error = 'unable to parse URL'; return $result; } if (!array_key_exists('scheme', $uri)) { $result->error = 'missing schema'; return $result; } switch ($uri['scheme']) { case 'http': $port = isset($uri['port']) ? $uri['port'] : 80; $host = $uri['host'] . ($port != 80 ? ':'. $port : ''); $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15); break; case 'https': // Note: Only works for PHP 4.3 compiled with OpenSSL. $port = isset($uri['port']) ? $uri['port'] : 443; $host = $uri['host'] . ($port != 443 ? ':'. $port : ''); $fp = @fsockopen('ssl://'. $uri['host'], $port, $errno, $errstr, 20); break; default: $result->error = 'invalid schema '. $uri['scheme']; return $result; } // Make sure the socket opened properly. if ($fp === false) { // When a network error occurs, we use a negative number so it does not // clash with the HTTP status codes. $result->code = -$errno; $result->error = trim($errstr); // Mark that this request failed. This will trigger a check of the web // server's ability to make outgoing HTTP requests the next time that // requirements checking is performed. // @see system_requirements() // @todo variable_set('drupal_http_request_fails', TRUE); return $result; } // Construct the path to act on. $path = $uri['path'] ?? '/'; if (array_key_exists('query', $uri)) { $path .= '?' . $uri['query']; } // Create HTTP request. $defaults = [ // RFC 2616: "non-standard ports MUST, default ports MAY be included". // We don't add the port to prevent from breaking rewrite rules checking the // host that do not take into account the port number. 'Host' => "Host: {$host}", 'User-Agent' => 'User-Agent: Drupal (+http://drupal.org/)', 'Content-Length' => 'Content-Length: ' . strlen($data), ]; // If the server url has a user then attempt to use basic authentication if (array_key_exists('user', $uri)) { $defaults['Authorization'] = 'Authorization: Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ':' . $uri['pass'] : '')); } // If the database prefix is being used by SimpleTest to run the tests in a copied // database then set the user-agent header to the database prefix so that any // calls to other Drupal pages will run the SimpleTest prefixed database. The // user-agent is used to ensure that multiple testing sessions running at the // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. if ( is_string($db_prefix) && preg_match('/^simpletest\d+$/', $db_prefix, $matches) ) { $defaults['User-Agent'] = 'User-Agent: ' . $matches[0]; } foreach ($headers as $header => $value) { $defaults[$header] = $header . ': ' . $value; } $request = $method . ' ' . $path . " HTTP/1.0\r\n"; $request .= implode("\r\n", $defaults); $request .= "\r\n\r\n"; $request .= $data; $result->request = $request; fwrite($fp, $request); // Fetch response. $response = ''; while (!feof($fp) && ($chunk = fread($fp, 1024)) !== false) { $response .= $chunk; } fclose($fp); // Parse response. [$split, $result->data] = explode("\r\n\r\n", $response, 2); $split = preg_split("/\r\n|\n|\r/", $split); [$protocol, $code, $text] = explode(' ', trim(array_shift($split)), 3); $result->headers = []; // Parse headers. while (($line = trim(array_shift($split))) !== '') { [$header, $value] = explode(':', $line, 2); if ( array_key_exists($header, $result->headers) && $header === 'Set-Cookie' ) { // RFC 2109: the Set-Cookie response header comprises the token Set- // Cookie:, followed by a comma-separated list of one or more cookies. $result->headers[$header] .= ',' . trim($value); } else { $result->headers[$header] = trim($value); } } $responses = [ 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', ]; // RFC 2616 states that all unknown HTTP codes must be treated the same as the // base code in their class. if (!array_key_exists($code, $responses)) { $code = floor($code / 100) * 100; } switch ($code) { case 200: // OK case 304: // Not modified break; case 301: // Moved permanently case 302: // Moved temporarily case 307: // Moved temporarily $location = $result->headers['Location']; if ($retry) { $result = drupal_http_request($result->headers['Location'], $headers, $method, $data, --$retry); $result->redirect_code = $result->code; } $result->redirect_url = $location; break; default: $result->error = $text; } $result->code = $code; return $result; } /** * Construct an object representing an XML-RPC message. * * @param $message * String containing XML as defined at http://www.xmlrpc.com/spec * @return * Object */ public function xmlrpc_message($message) { $xmlrpc_message = new stdClass(); // The stack used to keep track of the current array/struct $xmlrpc_message->array_structs = []; // The stack used to keep track of if things are structs or array $xmlrpc_message->array_structs_types = []; // A stack as well $xmlrpc_message->current_struct_name = []; $xmlrpc_message->message = $message; return $xmlrpc_message; } /** * Parse an XML-RPC message. If parsing fails, the faultCode and faultString * will be added to the message object. * * @param $xmlrpc_message * Object generated by xmlrpc_message() * @return * TRUE if parsing succeeded; FALSE otherwise */ public function xmlrpc_message_parse(&$xmlrpc_message) { // First remove the XML declaration $xmlrpc_message->message = preg_replace( '/<\?xml(.*)?\?'.'>/', '', $xmlrpc_message->message ); if (trim($xmlrpc_message->message) === '') { return false; } $xmlrpc_message->_parser = xml_parser_create(); // Set XML parser to take the case of tags into account. xml_parser_set_option( $xmlrpc_message->_parser, XML_OPTION_CASE_FOLDING, false ); // Set XML parser callback functions xml_set_element_handler( $xmlrpc_message->_parser, ['MollomPlugin', 'xmlrpc_message_tag_open'], ['MollomPlugin', 'xmlrpc_message_tag_close'] ); xml_set_character_data_handler( $xmlrpc_message->_parser, ['MollomPlugin', 'xmlrpc_message_cdata'] ); $this->xmlrpc_message_set($xmlrpc_message); if (!xml_parse($xmlrpc_message->_parser, $xmlrpc_message->message)) { return false; } xml_parser_free($xmlrpc_message->_parser); // Grab the error messages, if any $xmlrpc_message = $this->xmlrpc_message_get(); if ($xmlrpc_message->messagetype === 'fault') { $xmlrpc_message->fault_code = $xmlrpc_message->params[0]['faultCode']; $xmlrpc_message->fault_string = $xmlrpc_message->params[0]['faultString']; } return true; } /** * Store a copy of the $xmlrpc_message object temporarily. * * @param $value * Object * @return * The most recently stored $xmlrpc_message */ public function xmlrpc_message_set($value = null) { static $xmlrpc_message; if ($value) { $xmlrpc_message = $value; } return $xmlrpc_message; } public function xmlrpc_message_get() { return $this->xmlrpc_message_set(); } public function xmlrpc_message_tag_open($parser, $tag, $attr) { $xmlrpc_message = $this->xmlrpc_message_get(); $xmlrpc_message->current_tag_contents = ''; $xmlrpc_message->last_open = $tag; switch ($tag) { case 'methodCall': case 'methodResponse': case 'fault': $xmlrpc_message->messagetype = $tag; break; // Deal with stacks of arrays and structs case 'data': $xmlrpc_message->array_structs_types[] = 'array'; $xmlrpc_message->array_structs[] = array(); break; case 'struct': $xmlrpc_message->array_structs_types[] = 'struct'; $xmlrpc_message->array_structs[] = array(); break; } $this->xmlrpc_message_set($xmlrpc_message); } public function xmlrpc_message_cdata($parser, $cdata) { $xmlrpc_message = $this->xmlrpc_message_get(); $xmlrpc_message->current_tag_contents .= $cdata; $this->xmlrpc_message_set($xmlrpc_message); } public function xmlrpc_message_tag_close($parser, $tag) { $xmlrpc_message = $this->xmlrpc_message_get(); $value_flag = false; switch ($tag) { case 'int': case 'i4': $value = (int)trim($xmlrpc_message->current_tag_contents); $value_flag = true; break; case 'double': $value = (double)trim($xmlrpc_message->current_tag_contents); $value_flag = true; break; case 'string': $value = $xmlrpc_message->current_tag_contents; $value_flag = true; break; case 'dateTime.iso8601': $value = xmlrpc_date(trim($xmlrpc_message->current_tag_contents)); // $value = $iso->getTimestamp(); $value_flag = true; break; case 'value': // If no type is indicated, the type is string // We take special care for empty values if (trim($xmlrpc_message->current_tag_contents) != '' || (isset($xmlrpc_message->last_open) && ($xmlrpc_message->last_open == 'value'))) { $value = (string)$xmlrpc_message->current_tag_contents; $value_flag = true; } unset($xmlrpc_message->last_open); break; case 'boolean': $value = (boolean)trim($xmlrpc_message->current_tag_contents); $value_flag = true; break; case 'base64': $value = base64_decode(trim($xmlrpc_message->current_tag_contents)); $value_flag = true; break; // Deal with stacks of arrays and structs case 'data': case 'struct': $value = array_pop($xmlrpc_message->array_structs); array_pop($xmlrpc_message->array_structs_types); $value_flag = true; break; case 'member': array_pop($xmlrpc_message->current_struct_name); break; case 'name': $xmlrpc_message->current_struct_name[] = trim($xmlrpc_message->current_tag_contents); break; case 'methodName': $xmlrpc_message->methodname = trim($xmlrpc_message->current_tag_contents); break; } if ($value_flag) { if (count($xmlrpc_message->array_structs) > 0) { // Add value to struct or array if ($xmlrpc_message->array_structs_types[count($xmlrpc_message->array_structs_types) - 1] === 'struct') { // Add to struct $xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][$xmlrpc_message->current_struct_name[count($xmlrpc_message->current_struct_name) - 1]] = $value; } else { // Add to array $xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][] = $value; } } else { // Just add as a parameter $xmlrpc_message->params[] = $value; } } if (!in_array($tag, ['data', 'struct', 'member'])) { $xmlrpc_message->current_tag_contents = ''; } $this->xmlrpc_message_set($xmlrpc_message); } /** * Construct an object representing an XML-RPC request * * @param $method * The name of the method to be called * @param $args * An array of parameters to send with the method. * @return * Object */ public function xmlrpc_request($method, $args) { $xmlrpc_request = new stdClass(); $xmlrpc_request->method = $method; $xmlrpc_request->args = $args; $xmlrpc_request->xml = << {$xmlrpc_request->method} EOD; foreach ($xmlrpc_request->args as $arg) { $xmlrpc_request->xml .= ''; $v = $this->xmlrpc_value($arg); $xmlrpc_request->xml .= $this->xmlrpc_value_get_xml($v); $xmlrpc_request->xml .= "\n"; } $xmlrpc_request->xml .= ''; return $xmlrpc_request; } public function xmlrpc_error($code = null, $message = null, $reset = false) { static $xmlrpc_error; if (!is_null($code)) { $xmlrpc_error = new stdClass(); $xmlrpc_error->is_error = true; $xmlrpc_error->code = $code; $xmlrpc_error->message = $message; } elseif ($reset) { $xmlrpc_error = null; } return $xmlrpc_error; } public function xmlrpc_error_get_xml($xmlrpc_error) { return << faultCode {$xmlrpc_error->code} faultString {$xmlrpc_error->message} EOD; } public function xmlrpc_date($time) { $xmlrpc_date = new stdClass(); $xmlrpc_date->is_date = true; // $time can be a PHP timestamp or an ISO one if (is_numeric($time)) { $xmlrpc_date->year = gmdate('Y', $time); $xmlrpc_date->month = gmdate('m', $time); $xmlrpc_date->day = gmdate('d', $time); $xmlrpc_date->hour = gmdate('H', $time); $xmlrpc_date->minute = gmdate('i', $time); $xmlrpc_date->second = gmdate('s', $time); $xmlrpc_date->iso8601 = gmdate('Ymd\TH:i:s', $time); } else { $xmlrpc_date->iso8601 = $time; $time = str_replace(array('-', ':'), '', $time); $xmlrpc_date->year = substr($time, 0, 4); $xmlrpc_date->month = substr($time, 4, 2); $xmlrpc_date->day = substr($time, 6, 2); $xmlrpc_date->hour = substr($time, 9, 2); $xmlrpc_date->minute = substr($time, 11, 2); $xmlrpc_date->second = substr($time, 13, 2); } return $xmlrpc_date; } public function xmlrpc_date_get_xml($xmlrpc_date) { return '' . $xmlrpc_date->year . $xmlrpc_date->month . $xmlrpc_date->day . 'T' . $xmlrpc_date->hour . ':'. $xmlrpc_date->minute . ':'. $xmlrpc_date->second . ''; } public function xmlrpc_base64($data) { $xmlrpc_base64 = new stdClass(); $xmlrpc_base64->is_base64 = true; $xmlrpc_base64->data = $data; return $xmlrpc_base64; } public function xmlrpc_base64_get_xml($xmlrpc_base64) { return ''. base64_encode($xmlrpc_base64->data) .''; } /** * Execute an XML remote procedural call. This is private function; call xmlrpc() * in common.inc instead of this function. * * @return * A $xmlrpc_message object if the call succeeded; FALSE if the call failed */ public function _xmlrpc() { $args = func_get_args(); $url = array_shift($args); $this->xmlrpc_clear_error(); if (is_array($args[0])) { $method = 'system.multicall'; $multicall_args = []; foreach ($args[0] as $call) { $multicall_args[] = [ 'methodName' => array_shift($call), 'params' => $call, ]; } $args = [$multicall_args]; } else { $method = array_shift($args); } $xmlrpc_request = $this->xmlrpc_request($method, $args); $result = $this->http_request($url, [ 'Content-Type' => 'text/xml', ], 'POST', $xmlrpc_request->xml); if ($result->code !== 200) { $this->xmlrpc_error($result->code, $result->error); return false; } $message = $this->xmlrpc_message($result->data); // Now parse what we've got back if (!$this->xmlrpc_message_parse($message)) { // XML error $this->xmlrpc_error(-32700, t('Parse error. Not well formed')); return false; } // Is the message a fault? if ($message->messagetype === 'fault') { $this->xmlrpc_error($message->fault_code, $message->fault_string); return false; } // Message must be OK return $message->params[0]; } /** * Returns the last XML-RPC client error number */ public function xmlrpc_errno() { return $this->xmlrpc_error()->code ?? null; } /** * Returns the last XML-RPC client error message */ public function xmlrpc_error_msg() { return xmlrpc_error()->message ?? null; } /** * Clears any previous error. */ public function xmlrpc_clear_error() { $this->xmlrpc_error(null, null, true); } }