Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion components/ILIAS/Mail/classes/class.ilMailLuceneSearcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
257 changes: 209 additions & 48 deletions components/ILIAS/WebServices/RPC/classes/class.ilRpcClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@
declare(strict_types=1);

/**
* Class ilRpcClient
*
* @author Fabian Wolf <wolf@leifos.com>
* @ingroup ServicesWebServicesRPC
*
* 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:
Expand All @@ -48,56 +45,38 @@ 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;

$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 = $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|bool|stdClass
{
//prepare xml post data
$method_name = str_replace('_', '.', $this->prefix . $a_method);
$rpc_options = array(
'verbosity' => 'newlines_only',
'escaping' => 'markup'
);
$method_name = str_replace('_', '.', $this->prefix . $method);

$post_data = $this->encodeRequest($method_name, $parameters);

if ($this->encoding) {
$rpc_options['encoding'] = $this->encoding;
}
$post_data = xmlrpc_encode_request($method_name, $a_params, $rpc_options);
//try to connect to the given url
try {
$curl = new ilCurlConnection($this->url);
Expand All @@ -111,7 +90,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 . ' ' .
Expand All @@ -120,18 +99,200 @@ 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);
}

//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']);
throw new ilRpcClientException(
'RPC-Server returned fault message: ' .
$resp['faultString'],
$resp['faultCode']
);
/**
* @param (string|int|bool|int[])[] $parameters
* @throws ilRpcClientException
*/
protected function encodeRequest(string $method, array $parameters): string
{
$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) {
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($xml->importNode($this->wrapParameter($encoded_parameter)->documentElement, true));
}

$method_call->appendChild($method_name);
$method_call->appendChild($params);

$xml->appendChild($method_call);
return $xml->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(DOMDocument $encoded_parameter): DOMDocument
{
$xml = new DOMDocument('1.0', 'UTF-8');
$param = $xml->createElement('param');
$value = $xml->createElement('value');

$value->appendChild($xml->importNode($encoded_parameter->documentElement, true));
$param->appendChild($value);

$xml->appendChild($param);
return $xml;
}

protected function encodeString(string $parameter): DOMDocument
{
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->appendChild($xml->createElement('string', $parameter));
return $xml;
}

protected function encodeInteger(int $parameter): DOMDocument
{
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->appendChild($xml->createElement('int', (string) $parameter));
return $xml;
}

protected function encodeBoolean(bool $parameter): DOMDocument
{
$xml = new DOMDocument('1.0', 'UTF-8');
$xml->appendChild($xml->createElement('boolean', $parameter ? '1' : '0'));
return $xml;
}

protected function encodeListOfIntegers(int ...$parameters): DOMDocument
{
$xml = new DOMDocument('1.0', 'UTF-8');
$array = $xml->createElement('array');
$data = $xml->createElement('data');

foreach ($parameters as $parameter) {
$value = $xml->createElement('value');
$value->appendChild($xml->importNode($this->encodeInteger($parameter)->documentElement, true));
$data->appendChild($value);
}
$array->appendChild($data);

$xml->appendChild($array);
return $xml;
}

/**
* Returns decoded response if not faulty, otherwise throws exception.
* @throws ilRpcClientException
*/
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->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: ' . get_class($response_body)),
};
}

protected function decodeOKResponse(DOMElement $response_body): string|bool|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),
'#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(DOMNode $string): string
{
return (string) $string->nodeValue;
}

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
*/
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)?->nodeValue;
if ($name === 'faultCode') {
if ($fault_code !== null) {
throw new ilRpcClientException('Multiple codes in fault response.');
}
$fault_code = (int) $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');
}
return $resp;

$this->logger->error('RpcClient recieved error ' . $fault_code . ': ' . $fault_string);
throw new ilRpcClientException(
'RPC-Server returned fault message: ' .
$fault_string,
$fault_code
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,22 @@
*********************************************************************/

declare(strict_types=1);

/**
* @classDescription Factory for ILIAS rpc client
* @author Stefan Meyer <meyer@leifos.com>
*/
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
);
}
}
6 changes: 3 additions & 3 deletions components/ILIAS/WebServices/RPC/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ This Java server has been tested with Open JDK Java Runtime 17.
To be able to index and search for non-ASCII characters your system should
support UTF-8 encodings.

PHP curl and xmlrpc are required for using the Java server features.
PHP curl is required for using the Java server features.

On Debian-based systems try:

````shell
> apt-get install php-curl php-xmlrpc openjdk-17-jdk-headless
> apt-get install php-curl openjdk-17-jdk-headless
````
Dependencies and the build process is managed via maven
```shell
Expand Down Expand Up @@ -180,4 +180,4 @@ if no installation id is given.

```shell
> java -jar /foo/bar/target/ilServer.jar /foo/bar/ilServer.ini search <CLIENT_INFO> "ilias"
```
```
3 changes: 1 addition & 2 deletions docs/configuration/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ gd, dom, xsl, pdo, pdo_mysql, curl, json, simplexml, libxml, xml, zip, imagick,

**Optional PHP Extensions:**

* `xmlrpc` for the ILIAS RPC server
* `soap` for SOAP user administration
* `ldap` for LDAP user authentication

Expand All @@ -128,7 +127,7 @@ Alternatively, it can be obtained directly from [getcomposer.org](https://getcom
Composer may be optional when using the prepacked ILIAS from [Download & Releases](https://docu.ilias.de/go/pg/197851_35), but it is necessary when using plugins to rebuild the PHP autoload classmap.

```shell
apt install apache2 libapache2-mod-php php php-gd php-xsl php-imagick php-curl php-mysql php-xmlrpc php-soap php-ldap composer
apt install apache2 libapache2-mod-php php php-gd php-xsl php-imagick php-curl php-mysql php-soap php-ldap composer
```

Create a directory for the html sources (e.g. `/var/www/ilias`) which is referenced in the apache2 vhost and also a directory outside the web servers docroot (e.g. `/var/www/files`) for files stored by ILIAS.
Expand Down