From f6b0d7b900ac118889bfe01aba31c8bc24f15b63 Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Mon, 9 Feb 2026 18:09:16 +0100 Subject: [PATCH 1/2] initial implementation --- .../RPC/classes/class.ilRpcClient.php | 225 ++++++++++++++---- .../RPC/classes/class.ilRpcClientFactory.php | 16 +- 2 files changed, 191 insertions(+), 50 deletions(-) diff --git a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php index c0c9bae7e5ff..7250737daca5 100755 --- a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php +++ b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * Class ilRpcClient - * * @author Fabian Wolf - * @ingroup ServicesWebServicesRPC * * List of all known RPC methods... * @@ -48,19 +45,16 @@ class ilRpcClient protected string $url; protected string $prefix = ''; protected int $timeout = 0; - protected string $encoding = ''; protected ilLogger $logger; /** - * ilRpcClient constructor. - * @param string $a_url URL to connect to - * @param string $a_prefix Optional prefix for method names - * @param int $a_timeout The maximum number of seconds to allow ilRpcClient to connect. - * @param string $a_encoding Character encoding + * @param string $url URL to connect to + * @param string $prefix Optional prefix for method names + * @param int $timeout The maximum number of seconds to allow ilRpcClient to connect. * @throws ilRpcClientException */ - public function __construct(string $a_url, string $a_prefix = '', int $a_timeout = 0, string $a_encoding = 'utf-8') + public function __construct(string $url, string $prefix = '', int $timeout = 0) { global $DIC; @@ -71,33 +65,24 @@ public function __construct(string $a_url, string $a_prefix = '', int $a_timeout throw new ilRpcClientException('Xmlrpc extension not enabled.', 50); } - $this->url = $a_url; - $this->prefix = $a_prefix; - $this->timeout = $a_timeout; - $this->encoding = $a_encoding; + $this->url = $url; + $this->prefix = $prefix; + $this->timeout = $timeout; } /** - * Magic caller to all RPC functions - * - * @param string $a_method Method name - * @param array $a_params Argument array - * @return mixed Returns either an array, or an integer, or a string, or a boolean according to the response returned by the XMLRPC method. + * @param string $method Method name + * @param (string|int|bool|int[])[] $parameters Argument array + * @return string|stdClass Depends on the response returned by the XMLRPC method. * @throws ilRpcClientException */ - public function __call(string $a_method, array $a_params) + public function __call(string $method, array $parameters): string|stdClass { //prepare xml post data - $method_name = str_replace('_', '.', $this->prefix . $a_method); - $rpc_options = array( - 'verbosity' => 'newlines_only', - 'escaping' => 'markup' - ); - - if ($this->encoding) { - $rpc_options['encoding'] = $this->encoding; - } - $post_data = xmlrpc_encode_request($method_name, $a_params, $rpc_options); + $method_name = str_replace('_', '.', $this->prefix . $method); + + $post_data = $this->encodeRequest($method_name, $parameters); + //try to connect to the given url try { $curl = new ilCurlConnection($this->url); @@ -111,7 +96,7 @@ public function __call(string $a_method, array $a_params) $curl->setOpt(CURLOPT_TIMEOUT, $this->timeout); } $this->logger->debug('RpcClient request to ' . $this->url . ' / ' . $method_name); - $xml_resp = $curl->exec(); + $xml_response = $curl->exec(); } catch (ilCurlConnectionException $e) { $this->logger->error( 'RpcClient could not connect to ' . $this->url . ' ' . @@ -120,18 +105,178 @@ public function __call(string $a_method, array $a_params) throw new ilRpcClientException($e->getMessage(), $e->getCode()); } - //prepare output, throw exception if rpc fault is detected - $resp = xmlrpc_decode($xml_resp, $this->encoding); + //return output, throw exception if rpc fault is detected + return $this->handleResponse($xml_response); + } + + /** + * @param (string|int|bool|int[])[] $parameters + * @throws ilRpcClientException + */ + protected function encodeRequest(string $method, array $parameters): string + { + $request = new DOMDocument('1.0', 'UTF-8'); + $method_name = new DOMElement('methodName', $method); + $params = new DOMElement('params'); + + foreach ($parameters as $parameter) { + match (true) { + is_string($parameter) => $encoded_parameter = $this->encodeString($parameter), + is_int($parameter) => $encoded_parameter = $this->encodeInteger($parameter), + is_bool($parameter) => $encoded_parameter = $this->encodeBoolean($parameter), + $this->isListOfIntegers($parameter) => $encoded_parameter = $this->encodeListOfIntegers(...$parameter), + default => throw new ilRpcClientException( + 'Invalid parameter type, only string, int, bool, and int[] are supported.' + ) + }; + $params->appendChild($this->wrapParameter($encoded_parameter)); + } + + $request->appendChild($method_name); + $request->appendChild($params); + + return $request->saveXML(); + } + + protected function isListOfIntegers(mixed $parameter): bool + { + if (!is_array($parameter)) { + return false; + } + foreach ($parameter as $entries) { + if (!is_int($entries)) { + return false; + } + } + return true; + } + + protected function wrapParameter(DOMElement $encoded_parameter): DomElement + { + $param = new DomElement('param'); + $value = new DomElement('value'); + + $value->appendChild($encoded_parameter); + $param->appendChild($value); + + return $param; + } + + protected function encodeString(string $parameter): DOMElement + { + return new DOMElement('string', $parameter); + } + + protected function encodeInteger(int $parameter): DOMElement + { + return new DOMElement('int', (string) $parameter); + } + + protected function encodeBoolean(bool $parameter): DOMElement + { + return new DOMElement('bool', $parameter ? '1' : '0'); + } + + protected function encodeListOfIntegers(int ...$parameters): DOMElement + { + $array = new DomElement('array'); + $data = new DomElement('data'); + + foreach ($parameters as $parameter) { + $value = new DomElement('value'); + $value->appendChild($this->encodeInteger($parameter)); + $data->appendChild($value); + } + $array->appendChild($data); - //xmlrpc_is_fault can just handle arrays as response - if (is_array($resp) && xmlrpc_is_fault($resp)) { - $this->logger->error('RpcClient recieved error ' . $resp['faultCode'] . ': ' . $resp['faultString']); + return $array; + } + + /** + * Returns decoded response if not faulty, otherwise throws exception. + * @throws ilRpcClientException + */ + protected function handleResponse(string $xml): string|stdClass + { + $response = new DOMDocument('1.0', 'UTF-8'); + $response->loadXML($xml); + + if (!$response) { + throw new ilRpcClientException('Invalid XML response'); + } + + $response_body = $response->childNodes->item(0); + + if ($response_body === null) { + throw new ilRpcClientException('Empty response'); + } + + return match ($response_body->nodeName) { + 'params' => $this->decodeOKResponse($response_body), + 'fault' => $this->handleFaultResponse($response_body), + default => throw new ilRpcClientException('Unexpected element in response: ' . $response_body->nodeName), + }; + } + + protected function decodeOKResponse(DOMElement $response_body): string|stdClass + { + $param_child = $response_body->getElementsByTagName('value')->item(0)?->childNodes?->item(0); + + if ($param_child === null) { + throw new ilRpcClientException('No value in response'); + } + + return match ($param_child->nodeName) { + 'string' => $this->decodeString($param_child), + 'base64' => $this->decodeBase64($param_child), + default => throw new ilRpcClientException('Unexpected element in response value: ' . $param_child->nodeName), + }; + } + + protected function decodeString(DOMElement $string): string + { + return (string) $string->nodeValue; + } + + protected function decodeBase64(DOMElement $base64): stdClass + { + return (object) base64_decode((string) $base64->nodeValue); + } + + /** + * @throws ilRpcClientException + */ + protected function handleFaultResponse(DOMElement $response_body): string + { + $fault_code = null; + $fault_string = null; + + $members = $response_body->getElementsByTagName('member'); + foreach ($members as $member) { + $name = $member->getElementsByTagName('name')->item(0)?->nodeName; + if ($name === 'faultCode') { + if ($fault_code !== null) { + throw new ilRpcClientException('Multiple codes in fault response.'); + } + $fault_code = $member->getElementsByTagName('int')->item(0)?->nodeValue; + } + if ($name === 'faultString') { + if ($fault_string !== null) { + throw new ilRpcClientException('Multiple strings in fault response.'); + } + $fault_string = $member->getElementsByTagName('string')->item(0)?->nodeValue; + } + } + + if ($fault_code === null || $fault_string === null) { + throw new ilRpcClientException('No code or no string in fault respsonse'); + } + + $this->logger->error('RpcClient recieved error ' . $fault_code . ': ' . $fault_string); throw new ilRpcClientException( 'RPC-Server returned fault message: ' . - $resp['faultString'], - $resp['faultCode'] + $fault_string, + $fault_code ); } - return $resp; - } } diff --git a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClientFactory.php b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClientFactory.php index 7f623b17b515..d09ea25de439 100755 --- a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClientFactory.php +++ b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClientFactory.php @@ -17,26 +17,22 @@ *********************************************************************/ declare(strict_types=1); + /** - * @classDescription Factory for ILIAS rpc client * @author Stefan Meyer */ class ilRpcClientFactory { /** - * Creates an ilRpcClient instance to our ilServer - * - * @param string $a_package Package name - * @param int $a_timeout The maximum number of seconds to allow ilRpcClient to connect. - * @return ilRpcClient + * @param string $package Package name + * @param int $timeout The maximum number of seconds to allow ilRpcClient to connect. */ - public static function factory(string $a_package, int $a_timeout = 0): ilRpcClient + public static function factory(string $package, int $timeout = 0): ilRpcClient { return new ilRpcClient( ilRPCServerSettings::getInstance()->getServerUrl(), - $a_package . '.', - $a_timeout, - 'UTF-8' + $package . '.', + $timeout ); } } From 56463b2ca953f056ae9c5a511044a098d376e400 Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Wed, 11 Feb 2026 15:43:04 +0100 Subject: [PATCH 2/2] Search RPC: fixed mail search; fixed boolean; fixed unwrapped xml params --- .../classes/class.ilMailLuceneSearcher.php | 7 +- .../RPC/classes/class.ilRpcClient.php | 104 ++++++++++-------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/components/ILIAS/Mail/classes/class.ilMailLuceneSearcher.php b/components/ILIAS/Mail/classes/class.ilMailLuceneSearcher.php index 4d5f687a00e5..301efb7ea3ea 100755 --- a/components/ILIAS/Mail/classes/class.ilMailLuceneSearcher.php +++ b/components/ILIAS/Mail/classes/class.ilMailLuceneSearcher.php @@ -35,7 +35,12 @@ public function search(int $user_id, int $mail_folder_id): void } try { - $xml = ilRpcClientFactory::factory('RPCSearchHandler')->searchMail(); + $xml = ilRpcClientFactory::factory('RPCSearchHandler')->searchMail( + CLIENT_ID . '_' . $this->settings->get('inst_id', '0'), + $user_id, + $this->query_parser->getQuery(), + $mail_folder_id + ); } catch (Exception $e) { ilLoggerFactory::getLogger('mail')->critical($e->getMessage()); throw $e; diff --git a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php index 7250737daca5..3b91331c007c 100755 --- a/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php +++ b/components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php @@ -24,7 +24,7 @@ * List of all known RPC methods... * * RPCIndexHandler: - * @method void index() index(string $client, bool $bool) Prefix/Package: RPCIndexHandler + * @method bool index() index(string $client, bool $bool) Prefix/Package: RPCIndexHandler * @method void indexObjects() indexObjects(string $client, array $object_ids) Prefix/Package: RPCIndexHandler * * RPCTransformationHandler: @@ -59,12 +59,6 @@ public function __construct(string $url, string $prefix = '', int $timeout = 0) global $DIC; $this->logger = $DIC->logger()->wsrv(); - - if (!extension_loaded('xmlrpc')) { - ilLoggerFactory::getLogger('wsrv')->error('RpcClient Xmlrpc extension not enabled'); - throw new ilRpcClientException('Xmlrpc extension not enabled.', 50); - } - $this->url = $url; $this->prefix = $prefix; $this->timeout = $timeout; @@ -76,7 +70,7 @@ public function __construct(string $url, string $prefix = '', int $timeout = 0) * @return string|stdClass Depends on the response returned by the XMLRPC method. * @throws ilRpcClientException */ - public function __call(string $method, array $parameters): string|stdClass + public function __call(string $method, array $parameters): string|bool|stdClass { //prepare xml post data $method_name = str_replace('_', '.', $this->prefix . $method); @@ -115,9 +109,10 @@ public function __call(string $method, array $parameters): string|stdClass */ protected function encodeRequest(string $method, array $parameters): string { - $request = new DOMDocument('1.0', 'UTF-8'); - $method_name = new DOMElement('methodName', $method); - $params = new DOMElement('params'); + $xml = new DOMDocument('1.0', 'UTF-8'); + $method_call = $xml->createElement('methodCall'); + $method_name = $xml->createElement('methodName', $method); + $params = $xml->createElement('params'); foreach ($parameters as $parameter) { match (true) { @@ -129,13 +124,14 @@ protected function encodeRequest(string $method, array $parameters): string 'Invalid parameter type, only string, int, bool, and int[] are supported.' ) }; - $params->appendChild($this->wrapParameter($encoded_parameter)); + $params->appendChild($xml->importNode($this->wrapParameter($encoded_parameter)->documentElement, true)); } - $request->appendChild($method_name); - $request->appendChild($params); + $method_call->appendChild($method_name); + $method_call->appendChild($params); - return $request->saveXML(); + $xml->appendChild($method_call); + return $xml->saveXML(); } protected function isListOfIntegers(mixed $parameter): bool @@ -151,74 +147,87 @@ protected function isListOfIntegers(mixed $parameter): bool return true; } - protected function wrapParameter(DOMElement $encoded_parameter): DomElement + protected function wrapParameter(DOMDocument $encoded_parameter): DOMDocument { - $param = new DomElement('param'); - $value = new DomElement('value'); + $xml = new DOMDocument('1.0', 'UTF-8'); + $param = $xml->createElement('param'); + $value = $xml->createElement('value'); - $value->appendChild($encoded_parameter); + $value->appendChild($xml->importNode($encoded_parameter->documentElement, true)); $param->appendChild($value); - return $param; + $xml->appendChild($param); + return $xml; } - protected function encodeString(string $parameter): DOMElement + protected function encodeString(string $parameter): DOMDocument { - return new DOMElement('string', $parameter); + $xml = new DOMDocument('1.0', 'UTF-8'); + $xml->appendChild($xml->createElement('string', $parameter)); + return $xml; } - protected function encodeInteger(int $parameter): DOMElement + protected function encodeInteger(int $parameter): DOMDocument { - return new DOMElement('int', (string) $parameter); + $xml = new DOMDocument('1.0', 'UTF-8'); + $xml->appendChild($xml->createElement('int', (string) $parameter)); + return $xml; } - protected function encodeBoolean(bool $parameter): DOMElement + protected function encodeBoolean(bool $parameter): DOMDocument { - return new DOMElement('bool', $parameter ? '1' : '0'); + $xml = new DOMDocument('1.0', 'UTF-8'); + $xml->appendChild($xml->createElement('boolean', $parameter ? '1' : '0')); + return $xml; } - protected function encodeListOfIntegers(int ...$parameters): DOMElement + protected function encodeListOfIntegers(int ...$parameters): DOMDocument { - $array = new DomElement('array'); - $data = new DomElement('data'); + $xml = new DOMDocument('1.0', 'UTF-8'); + $array = $xml->createElement('array'); + $data = $xml->createElement('data'); foreach ($parameters as $parameter) { - $value = new DomElement('value'); - $value->appendChild($this->encodeInteger($parameter)); + $value = $xml->createElement('value'); + $value->appendChild($xml->importNode($this->encodeInteger($parameter)->documentElement, true)); $data->appendChild($value); } $array->appendChild($data); - return $array; + $xml->appendChild($array); + return $xml; } /** * Returns decoded response if not faulty, otherwise throws exception. * @throws ilRpcClientException */ - protected function handleResponse(string $xml): string|stdClass + public function handleResponse(string $xml): string|bool|stdClass { $response = new DOMDocument('1.0', 'UTF-8'); + $response->preserveWhiteSpace = false; $response->loadXML($xml); if (!$response) { throw new ilRpcClientException('Invalid XML response'); } - $response_body = $response->childNodes->item(0); + $response_body = $response->documentElement->childNodes->item(0); if ($response_body === null) { throw new ilRpcClientException('Empty response'); } + $this->logger->dump($response_body); + return match ($response_body->nodeName) { 'params' => $this->decodeOKResponse($response_body), 'fault' => $this->handleFaultResponse($response_body), - default => throw new ilRpcClientException('Unexpected element in response: ' . $response_body->nodeName), + default => throw new ilRpcClientException('Unexpected element in response: ' . get_class($response_body)), }; } - protected function decodeOKResponse(DOMElement $response_body): string|stdClass + protected function decodeOKResponse(DOMElement $response_body): string|bool|stdClass { $param_child = $response_body->getElementsByTagName('value')->item(0)?->childNodes?->item(0); @@ -228,21 +237,28 @@ protected function decodeOKResponse(DOMElement $response_body): string|stdClass return match ($param_child->nodeName) { 'string' => $this->decodeString($param_child), + '#text' => $this->decodeString($param_child), // org.apache.xmlrpc returns java strings as unwrapped text node 'base64' => $this->decodeBase64($param_child), + 'boolean' => $this->decodeBoolean($param_child), default => throw new ilRpcClientException('Unexpected element in response value: ' . $param_child->nodeName), }; } - protected function decodeString(DOMElement $string): string + protected function decodeString(DOMNode $string): string { return (string) $string->nodeValue; } - protected function decodeBase64(DOMElement $base64): stdClass + protected function decodeBase64(DOMNode $base64): stdClass { return (object) base64_decode((string) $base64->nodeValue); } + protected function decodeBoolean(DOMNode $boolean): bool + { + return (bool) $boolean->nodeValue; + } + /** * @throws ilRpcClientException */ @@ -253,12 +269,12 @@ protected function handleFaultResponse(DOMElement $response_body): string $members = $response_body->getElementsByTagName('member'); foreach ($members as $member) { - $name = $member->getElementsByTagName('name')->item(0)?->nodeName; + $name = $member->getElementsByTagName('name')->item(0)?->nodeValue; if ($name === 'faultCode') { if ($fault_code !== null) { throw new ilRpcClientException('Multiple codes in fault response.'); } - $fault_code = $member->getElementsByTagName('int')->item(0)?->nodeValue; + $fault_code = (int) $member->getElementsByTagName('int')->item(0)?->nodeValue; } if ($name === 'faultString') { if ($fault_string !== null) { @@ -273,10 +289,10 @@ protected function handleFaultResponse(DOMElement $response_body): string } $this->logger->error('RpcClient recieved error ' . $fault_code . ': ' . $fault_string); - throw new ilRpcClientException( - 'RPC-Server returned fault message: ' . + throw new ilRpcClientException( + 'RPC-Server returned fault message: ' . $fault_string, $fault_code - ); - } + ); + } }