Commit efcb25cd authored by Elias Villiger's avatar Elias Villiger
Browse files

Rewrite REST Client as php stream, add contentFile option.

parent 9d78a6fb
Pipeline #5905 passed with stages
in 3 minutes and 18 seconds
......@@ -1761,8 +1761,9 @@ REST Client
POST and GET data to external REST interfaces or other API services.
Access to external services via HTTP / HTTPS is triggered via special column name *restClient*. The received data might
be processed in subsequent calls.
Access to external services via HTTP / HTTPS is triggered via special column name *restClient*.
QFQ uses the php stream interface for the API calls. Because of that, allow_url_fopen needs to be set to 1 on the installation.
The received data can be processed in subsequent calls.
Example::
......@@ -1782,6 +1783,9 @@ Example::
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| content | content:{"name":"John";"surname":"Doe"} | Depending on the REST server JSON might be expected |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| contentFile | contentFile:fileadmin/_temp_/data.txt | Replaces content, if given. Recommended for large data |
| | | sections, such as binary data for files. |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| header | *see below* | |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| timeout | timeout:5 | Default: 5 seconds. |
......@@ -1797,13 +1801,19 @@ Example::
* *content-type: text/plain* - if *content* does not start with a ``{``.
* *connection: close* - Necessary for HTTP 1.1.
* Basic Authorization Example
Warning: Only use base64 for SSL encrypted connections::
10.sql = SELECT CONCAT('n:https://sample.com/id/1234|header:Authorization: Basic ', TO_BASE64('{{username}}:{{password}}') )
**Result received**
* After a *REST client* call is fired, QFQ will wait up to *timeout* seconds for the answer.
* By default, the whole received answer will be shown. To suppress the output: ``... AS '_restClient|_hide'``
* The variable ``{{http-status:C}}`` shows the `HTTP status code<https://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_.
A value starting with '2..' shows success.
* In case of an error, ``{{error-message:C:allbut}}`` shows some details.
* In case of an error, the HTTP status code is set to 0 and ``{{error-message:C:allbut}}`` shows some details.
* In case the returned answer is a valid JSON string, it is flattened and automatically copied to STORE_CLIENT with corresponding key names.
* NOTE: The CLIENT store is emptied beforehand!
......
......@@ -379,6 +379,7 @@ const ERROR_IMPORT_LIST_SHEET_NAMES = 2901;
const ERROR_FORM_REST = 3000;
const ERROR_REST_AUTHORIZATION = 3001;
const ERROR_REST_INVALID_ID = 3002;
const ERROR_REST_API_CALL = 3010;
// Tablesorter
const ERROR_TABLESORTER_SIP_NOT_FOUND = 3100;
......@@ -1856,6 +1857,7 @@ const TOKEN_L_HTML_ID = 'htmlId';
const TOKEN_L_METHOD = 'method';
const TOKEN_L_HEADER = 'header';
const TOKEN_L_CONTENT = 'content';
const TOKEN_L_CONTENT_FILE = 'contentFile';
const TOKEN_L_TIMEOUT = 'timeout';
const TOKEN_L_SSL = 'ssl';
......
......@@ -101,7 +101,7 @@ class ColumnScript {
*/
class ScriptFunctions {
public static function apiCall(string $method, string $url, string $data = '', array $header = [], int $timeout = 5) {
list($http_code, $answer) = RestClient::callApiCurl($method, $url, $data, $header, $timeout);
list($http_code, $answer) = RestClient::callApiStream($method, $url, $data, $header, $timeout);
return [$http_code, $answer];
}
......
......@@ -44,9 +44,26 @@ class RestClient {
// $options['ssl'] = json_decode($param[TOKEN_L_SSL], true);
// }
// If $dataFile given: overwrite $data with file contents
$data = $param[TOKEN_L_CONTENT];
$dataFile = $param[TOKEN_L_CONTENT_FILE];
if ($dataFile) {
$fileHandle = fopen($dataFile, "r");
if ($fileHandle === false) {
throw new \UserReportException("Could not read file $dataFile.", ERROR_IO_FILE_NOT_FOUND);
}
$data = stream_get_contents($fileHandle);
fclose($fileHandle);
}
// Send request
try {
list($http_status, $recvBuffer) = self::callApiCurl(strtoupper($param[TOKEN_L_METHOD]), $param[TOKEN_REST_CLIENT], $data = $param[TOKEN_L_CONTENT] ?? '', $param[TOKEN_L_HEADER], $param[TOKEN_L_TIMEOUT]);
list($http_status, $recvBuffer) = self::callApiStream(
strtoupper($param[TOKEN_L_METHOD]),
$param[TOKEN_REST_CLIENT],
$data,
$param[TOKEN_L_HEADER],
$param[TOKEN_L_TIMEOUT]);
$recv[HTTP_STATUS] = $http_status;
if ($http_status >= 300) {
$recv[ERROR_MESSAGE] = ' Api error: ' . $param[TOKEN_REST_CLIENT] . ' HTTP code: ' . $http_status . ' Message: ' . print_r(json_decode($recvBuffer ?? '', true), true);
......@@ -58,8 +75,8 @@ class RestClient {
});
}
} catch (\Exception $e) {
$recv[HTTP_STATUS] = $e->getCode();
$recv[ERROR_MESSAGE] = $e->getMessage();
$recv[HTTP_STATUS] = 0;
$recv[ERROR_MESSAGE] = $e->getMessage() . ' (QFQ Error Code: ' . $e->getCode() . ')';
}
// Copy new values to STORE_CLIENT
......@@ -77,7 +94,8 @@ class RestClient {
*/
private function parseArgument($str) {
// "n:http://antmedia-dev.math.uzh.ch/WebRTCAppEE/rest/v2/broadcasts/create|
// content:{'streamId' => "ASDKLJfdlajfhdkhH"}|method:POST|header:Content-type: application/json\r\n"
// content:{'streamId' => "ASDKLJfdlajfhdkhH"}|contentFile:fileadmin/_temp_/data.txt|
// method:POST|header:Content-type: application/json\r\n"
// Split string
$param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
......@@ -85,7 +103,8 @@ class RestClient {
throw new \UserReportException("Missing RestClient target", ERROR_MISSING_VALUE);
}
$param[TOKEN_L_CONTENT] = trim($param[TOKEN_L_CONTENT]);
$param[TOKEN_L_CONTENT] = trim($param[TOKEN_L_CONTENT]) ?? '';
$param[TOKEN_L_CONTENT_FILE] = trim($param[TOKEN_L_CONTENT_FILE]) ?? '';
$param[TOKEN_L_HEADER] = trim($param[TOKEN_L_HEADER]);
if (empty($param[TOKEN_L_METHOD])) {
......@@ -126,64 +145,58 @@ class RestClient {
* @return array
* @throws Exception
*/
public static function callApiCurl(string $method, string $url, string $data = '', array $header = [], int $timeout = 5) {
public static function callApiStream(string $method, string $url, string $data = '', array $header = [], int $timeout = 5) {
// Header: Set content-type if not set
if (strpos(join('', $header), 'content-type:') === false) {
if (stripos(join('', $header), 'content-type:') === false) {
$mime = (($data[0] ?? '') == '{') ? 'application/json' : 'text/plain';
$header[] = 'content-type: ' . $mime . '; charset=utf-8';
}
// Header: Set connection if not set
if (strpos(join('', $header), 'connection:') === false) {
if (stripos(join('', $header), 'connection:') === false) {
$header[] = 'connection: close';
}
$ch = curl_init();
$curlConfig = array(
CURLOPT_RETURNTRANSFER => true,
// CURLINFO_HEADER_OUT => $debug
);
switch ($method) {
case "POST":
$curlConfig[CURLOPT_POST] = true;
if (!empty($data)) {
$dataJson = $data;
$curlConfig[CURLOPT_POSTFIELDS] = $dataJson;
$header[] = 'Content-Length: ' . strlen($dataJson);
}
break;
case "PUT":
$curlConfig[CURLOPT_CUSTOMREQUEST] = 'PUT';
if (!empty($data)) {
$dataJson = $data;
$curlConfig[CURLOPT_POSTFIELDS] = $dataJson;
$header[] = 'Content-Length: ' . strlen($dataJson);
}
break;
case "DELETE":
$curlConfig[CURLOPT_CUSTOMREQUEST] = 'DELETE';
if (!empty($data)) {
// Prepare options
$opts = [
'http' => [
'method' => $method,
'timeout' => $timeout
]
];
if (!empty($data)) {
switch ($method) {
case "POST":
case "PUT":
// Header: Set content-length
$header[] = 'content-length: ' . strlen($data);
// Pass data
$opts['http']['content'] = $data;
break;
case "DELETE":
default:
$url = sprintf("%s?%s", $url, http_build_query(json_decode($data, true)));
}
break;
default:
if (!empty($data)) {
$url = sprintf("%s?%s", $url, http_build_query(json_decode($data, true)));
}
}
}
$curlConfig[CURLOPT_URL] = $url;
$curlConfig[CURLOPT_TIMEOUT] = $timeout;
curl_setopt_array($ch, $curlConfig);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
// send request
$output = curl_exec($ch);
if ($output === false) {
throw new Exception(curl_error($ch), curl_errno($ch));
$opts['http']['header'] = $header;
$context = stream_context_create($opts);
$stream = @fopen($url, 'r', false, $context);
$output = stream_get_contents($stream);
if ($stream === false) {
$err = error_get_last();
throw new Exception("Api call {$method} {$url} failed: " . $err['message'], ERROR_REST_API_CALL);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// get HTTP status code
$status_line = $http_response_header[0];
preg_match('{HTTP\/\S*\s(\d{3})}', $status_line, $match);
$httpCode = $match[1];
return [$httpCode, $output];
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment