1919declare (strict_types=1 );
2020
2121/**
22- * Class ilRpcClient
23- *
2422 * @author Fabian Wolf <wolf@leifos.com>
25- * @ingroup ServicesWebServicesRPC
2623 *
2724 * List of all known RPC methods...
2825 *
2926 * RPCIndexHandler:
30- * @method void index() index(string $client, bool $bool) Prefix/Package: RPCIndexHandler
27+ * @method bool index() index(string $client, bool $bool) Prefix/Package: RPCIndexHandler
3128 * @method void indexObjects() indexObjects(string $client, array $object_ids) Prefix/Package: RPCIndexHandler
3229 *
3330 * RPCTransformationHandler:
@@ -48,56 +45,38 @@ class ilRpcClient
4845 protected string $ url ;
4946 protected string $ prefix = '' ;
5047 protected int $ timeout = 0 ;
51- protected string $ encoding = '' ;
5248
5349 protected ilLogger $ logger ;
5450
5551 /**
56- * ilRpcClient constructor.
57- * @param string $a_url URL to connect to
58- * @param string $a_prefix Optional prefix for method names
59- * @param int $a_timeout The maximum number of seconds to allow ilRpcClient to connect.
60- * @param string $a_encoding Character encoding
52+ * @param string $url URL to connect to
53+ * @param string $prefix Optional prefix for method names
54+ * @param int $timeout The maximum number of seconds to allow ilRpcClient to connect.
6155 * @throws ilRpcClientException
6256 */
63- public function __construct (string $ a_url , string $ a_prefix = '' , int $ a_timeout = 0 , string $ a_encoding = ' utf-8 ' )
57+ public function __construct (string $ url , string $ prefix = '' , int $ timeout = 0 )
6458 {
6559 global $ DIC ;
6660
6761 $ this ->logger = $ DIC ->logger ()->wsrv ();
68-
69- if (!extension_loaded ('xmlrpc ' )) {
70- ilLoggerFactory::getLogger ('wsrv ' )->error ('RpcClient Xmlrpc extension not enabled ' );
71- throw new ilRpcClientException ('Xmlrpc extension not enabled. ' , 50 );
72- }
73-
74- $ this ->url = $ a_url ;
75- $ this ->prefix = $ a_prefix ;
76- $ this ->timeout = $ a_timeout ;
77- $ this ->encoding = $ a_encoding ;
62+ $ this ->url = $ url ;
63+ $ this ->prefix = $ prefix ;
64+ $ this ->timeout = $ timeout ;
7865 }
7966
8067 /**
81- * Magic caller to all RPC functions
82- *
83- * @param string $a_method Method name
84- * @param array $a_params Argument array
85- * @return mixed Returns either an array, or an integer, or a string, or a boolean according to the response returned by the XMLRPC method.
68+ * @param string $method Method name
69+ * @param (string|int|bool|int[])[] $parameters Argument array
70+ * @return string|stdClass Depends on the response returned by the XMLRPC method.
8671 * @throws ilRpcClientException
8772 */
88- public function __call (string $ a_method , array $ a_params )
73+ public function __call (string $ method , array $ parameters ): string | bool | stdClass
8974 {
9075 //prepare xml post data
91- $ method_name = str_replace ('_ ' , '. ' , $ this ->prefix . $ a_method );
92- $ rpc_options = array (
93- 'verbosity ' => 'newlines_only ' ,
94- 'escaping ' => 'markup '
95- );
76+ $ method_name = str_replace ('_ ' , '. ' , $ this ->prefix . $ method );
77+
78+ $ post_data = $ this ->encodeRequest ($ method_name , $ parameters );
9679
97- if ($ this ->encoding ) {
98- $ rpc_options ['encoding ' ] = $ this ->encoding ;
99- }
100- $ post_data = xmlrpc_encode_request ($ method_name , $ a_params , $ rpc_options );
10180 //try to connect to the given url
10281 try {
10382 $ curl = new ilCurlConnection ($ this ->url );
@@ -111,7 +90,7 @@ public function __call(string $a_method, array $a_params)
11190 $ curl ->setOpt (CURLOPT_TIMEOUT , $ this ->timeout );
11291 }
11392 $ this ->logger ->debug ('RpcClient request to ' . $ this ->url . ' / ' . $ method_name );
114- $ xml_resp = $ curl ->exec ();
93+ $ xml_response = $ curl ->exec ();
11594 } catch (ilCurlConnectionException $ e ) {
11695 $ this ->logger ->error (
11796 'RpcClient could not connect to ' . $ this ->url . ' ' .
@@ -120,18 +99,200 @@ public function __call(string $a_method, array $a_params)
12099 throw new ilRpcClientException ($ e ->getMessage (), $ e ->getCode ());
121100 }
122101
123- //prepare output, throw exception if rpc fault is detected
124- $ resp = xmlrpc_decode ($ xml_resp , $ this ->encoding );
102+ //return output, throw exception if rpc fault is detected
103+ return $ this ->handleResponse ($ xml_response );
104+ }
125105
126- //xmlrpc_is_fault can just handle arrays as response
127- if (is_array ($ resp ) && xmlrpc_is_fault ($ resp )) {
128- $ this ->logger ->error ('RpcClient recieved error ' . $ resp ['faultCode ' ] . ': ' . $ resp ['faultString ' ]);
129- throw new ilRpcClientException (
130- 'RPC-Server returned fault message: ' .
131- $ resp ['faultString ' ],
132- $ resp ['faultCode ' ]
133- );
106+ /**
107+ * @param (string|int|bool|int[])[] $parameters
108+ * @throws ilRpcClientException
109+ */
110+ protected function encodeRequest (string $ method , array $ parameters ): string
111+ {
112+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
113+ $ method_call = $ xml ->createElement ('methodCall ' );
114+ $ method_name = $ xml ->createElement ('methodName ' , $ method );
115+ $ params = $ xml ->createElement ('params ' );
116+
117+ foreach ($ parameters as $ parameter ) {
118+ match (true ) {
119+ is_string ($ parameter ) => $ encoded_parameter = $ this ->encodeString ($ parameter ),
120+ is_int ($ parameter ) => $ encoded_parameter = $ this ->encodeInteger ($ parameter ),
121+ is_bool ($ parameter ) => $ encoded_parameter = $ this ->encodeBoolean ($ parameter ),
122+ $ this ->isListOfIntegers ($ parameter ) => $ encoded_parameter = $ this ->encodeListOfIntegers (...$ parameter ),
123+ default => throw new ilRpcClientException (
124+ 'Invalid parameter type, only string, int, bool, and int[] are supported. '
125+ )
126+ };
127+ $ params ->appendChild ($ xml ->importNode ($ this ->wrapParameter ($ encoded_parameter )->documentElement , true ));
128+ }
129+
130+ $ method_call ->appendChild ($ method_name );
131+ $ method_call ->appendChild ($ params );
132+
133+ $ xml ->appendChild ($ method_call );
134+ return $ xml ->saveXML ();
135+ }
136+
137+ protected function isListOfIntegers (mixed $ parameter ): bool
138+ {
139+ if (!is_array ($ parameter )) {
140+ return false ;
141+ }
142+ foreach ($ parameter as $ entries ) {
143+ if (!is_int ($ entries )) {
144+ return false ;
145+ }
146+ }
147+ return true ;
148+ }
149+
150+ protected function wrapParameter (DOMDocument $ encoded_parameter ): DOMDocument
151+ {
152+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
153+ $ param = $ xml ->createElement ('param ' );
154+ $ value = $ xml ->createElement ('value ' );
155+
156+ $ value ->appendChild ($ xml ->importNode ($ encoded_parameter ->documentElement , true ));
157+ $ param ->appendChild ($ value );
158+
159+ $ xml ->appendChild ($ param );
160+ return $ xml ;
161+ }
162+
163+ protected function encodeString (string $ parameter ): DOMDocument
164+ {
165+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
166+ $ xml ->appendChild ($ xml ->createElement ('string ' , $ parameter ));
167+ return $ xml ;
168+ }
169+
170+ protected function encodeInteger (int $ parameter ): DOMDocument
171+ {
172+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
173+ $ xml ->appendChild ($ xml ->createElement ('int ' , (string ) $ parameter ));
174+ return $ xml ;
175+ }
176+
177+ protected function encodeBoolean (bool $ parameter ): DOMDocument
178+ {
179+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
180+ $ xml ->appendChild ($ xml ->createElement ('boolean ' , $ parameter ? '1 ' : '0 ' ));
181+ return $ xml ;
182+ }
183+
184+ protected function encodeListOfIntegers (int ...$ parameters ): DOMDocument
185+ {
186+ $ xml = new DOMDocument ('1.0 ' , 'UTF-8 ' );
187+ $ array = $ xml ->createElement ('array ' );
188+ $ data = $ xml ->createElement ('data ' );
189+
190+ foreach ($ parameters as $ parameter ) {
191+ $ value = $ xml ->createElement ('value ' );
192+ $ value ->appendChild ($ xml ->importNode ($ this ->encodeInteger ($ parameter )->documentElement , true ));
193+ $ data ->appendChild ($ value );
194+ }
195+ $ array ->appendChild ($ data );
196+
197+ $ xml ->appendChild ($ array );
198+ return $ xml ;
199+ }
200+
201+ /**
202+ * Returns decoded response if not faulty, otherwise throws exception.
203+ * @throws ilRpcClientException
204+ */
205+ public function handleResponse (string $ xml ): string |bool |stdClass
206+ {
207+ $ response = new DOMDocument ('1.0 ' , 'UTF-8 ' );
208+ $ response ->preserveWhiteSpace = false ;
209+ $ response ->loadXML ($ xml );
210+
211+ if (!$ response ) {
212+ throw new ilRpcClientException ('Invalid XML response ' );
213+ }
214+
215+ $ response_body = $ response ->documentElement ->childNodes ->item (0 );
216+
217+ if ($ response_body === null ) {
218+ throw new ilRpcClientException ('Empty response ' );
219+ }
220+
221+ $ this ->logger ->dump ($ response_body );
222+
223+ return match ($ response_body ->nodeName ) {
224+ 'params ' => $ this ->decodeOKResponse ($ response_body ),
225+ 'fault ' => $ this ->handleFaultResponse ($ response_body ),
226+ default => throw new ilRpcClientException ('Unexpected element in response: ' . get_class ($ response_body )),
227+ };
228+ }
229+
230+ protected function decodeOKResponse (DOMElement $ response_body ): string |bool |stdClass
231+ {
232+ $ param_child = $ response_body ->getElementsByTagName ('value ' )->item (0 )?->childNodes?->item(0 );
233+
234+ if ($ param_child === null ) {
235+ throw new ilRpcClientException ('No value in response ' );
236+ }
237+
238+ return match ($ param_child ->nodeName ) {
239+ 'string ' => $ this ->decodeString ($ param_child ),
240+ '#text ' => $ this ->decodeString ($ param_child ), // org.apache.xmlrpc returns java strings as unwrapped text node
241+ 'base64 ' => $ this ->decodeBase64 ($ param_child ),
242+ 'boolean ' => $ this ->decodeBoolean ($ param_child ),
243+ default => throw new ilRpcClientException ('Unexpected element in response value: ' . $ param_child ->nodeName ),
244+ };
245+ }
246+
247+ protected function decodeString (DOMNode $ string ): string
248+ {
249+ return (string ) $ string ->nodeValue ;
250+ }
251+
252+ protected function decodeBase64 (DOMNode $ base64 ): stdClass
253+ {
254+ return (object ) base64_decode ((string ) $ base64 ->nodeValue );
255+ }
256+
257+ protected function decodeBoolean (DOMNode $ boolean ): bool
258+ {
259+ return (bool ) $ boolean ->nodeValue ;
260+ }
261+
262+ /**
263+ * @throws ilRpcClientException
264+ */
265+ protected function handleFaultResponse (DOMElement $ response_body ): string
266+ {
267+ $ fault_code = null ;
268+ $ fault_string = null ;
269+
270+ $ members = $ response_body ->getElementsByTagName ('member ' );
271+ foreach ($ members as $ member ) {
272+ $ name = $ member ->getElementsByTagName ('name ' )->item (0 )?->nodeValue;
273+ if ($ name === 'faultCode ' ) {
274+ if ($ fault_code !== null ) {
275+ throw new ilRpcClientException ('Multiple codes in fault response. ' );
276+ }
277+ $ fault_code = (int ) $ member ->getElementsByTagName ('int ' )->item (0 )?->nodeValue;
278+ }
279+ if ($ name === 'faultString ' ) {
280+ if ($ fault_string !== null ) {
281+ throw new ilRpcClientException ('Multiple strings in fault response. ' );
282+ }
283+ $ fault_string = $ member ->getElementsByTagName ('string ' )->item (0 )?->nodeValue;
284+ }
285+ }
286+
287+ if ($ fault_code === null || $ fault_string === null ) {
288+ throw new ilRpcClientException ('No code or no string in fault respsonse ' );
134289 }
135- return $ resp ;
290+
291+ $ this ->logger ->error ('RpcClient recieved error ' . $ fault_code . ': ' . $ fault_string );
292+ throw new ilRpcClientException (
293+ 'RPC-Server returned fault message: ' .
294+ $ fault_string ,
295+ $ fault_code
296+ );
136297 }
137298}
0 commit comments