Commit faf44578 authored by Carsten  Rose's avatar Carsten Rose

Fixes #11119 - restClient via ... AS _link

parent b54975ba
Pipeline #3779 passed with stages
in 4 minutes and 9 seconds
......@@ -38,8 +38,11 @@ REST
====
Via `REST <https://en.wikipedia.org/wiki/Representational_state_transfer>`_ it's possible to access the QFQ based
application. Each REST API endpoint has to be defined as a QFQ Form. The QFQ REST api implements the
four most used REST HTTP methods:
application. Each REST API endpoint has to be defined as a QFQ Form.
This describes the server side (=QFQ is server). For client access check :ref:`rest_client`.
The QFQ REST api implements the four most used REST HTTP methods:
GET - Read
Shows a list of database records or a single record. The QFQ form holds the definition which and what to show.
......@@ -77,14 +80,14 @@ Endpoint
``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/<level1>/<id1>/<level2>/<id2>/.../?<var1>=<value1>&...``
Append level names and ids after `.../rest.php/`, each separated by '/' .
Append level names and ids after ``.../rest.php/``, each separated by '/' .
E.g.:
1. List of all persons: `<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person`
2. Data of person 123: `<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123`
3. Adresses of person 123: `<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address`
4. Adress details of address 45 from person 123: `<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address/45`
1. List of all persons: ``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person``
2. Data of person 123: ``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123``
3. Adresses of person 123: ``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address``
4. Adress details of address 45 from person 123: ``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address/45``
QFQ 'Forms' are used as a 'container' (to define all details).
......@@ -96,14 +99,14 @@ Only the last <level> of an URI will be processed. The former ones are just to f
.. note::
Each level name (=form name) is available via STORE_CLIENT and name `_formX`. E.g. in example
(1) `{{_form1:C:alnumx}}=person` and `{{_form2:C:alnumx}}=address`.
Each level name (=form name) is available via STORE_CLIENT and name ``_formX``. E.g. in example
(1) ``{{_form1:C:alnumx}}=person`` and ``{{_form2:C:alnumx}}=address``.
Each level id is available via STORE_CLIENT and name `_idX`. E.g. in example
(2) `{{_id1:C}}=123` and `{{_id2:C}}=45`.
(2) ``{{_id1:C}}=123`` and ``{{_id2:C}}=45``.
Also the `id` after the last `level` in the URI path, 123 in example (2) and 45 in example (4), is copied to
variable `r` in STORE_TYPO3, access it via `{{r:T}}`.
Also the ``id`` after the last ``level`` in the URI path, 123 in example (2) and 45 in example (4), is copied to
variable ``r`` in STORE_TYPO3, access it via ``{{r:T}}``.
GET - Read
......@@ -124,8 +127,8 @@ list
There are *no* native-FormElements necessary or loaded. Action FormElements will be processed.
To simplify access to id parameter of the URI, a mapping is possible via 'form.parameter.restParam'.
E.g. `restParam=pId,adrId` with example d) makes `{{pId:C}}=123` and `{{adrId:C}}=45`. The order of variable
names corresponds to the position in the URI. `_id1` is always mapped to the first parameter name, `_id2` to
E.g. ``restParam=pId,adrId`` with example d) makes ``{{pId:C}}=123`` and ``{{adrId:C}}=45``. The order of variable
names corresponds to the position in the URI. ``_id1`` is always mapped to the first parameter name, ``_id2`` to
the second one and so on.
GET Variables provided via URL are available via STORE_CLIENT as usual.
......@@ -156,12 +159,12 @@ GET Variables provided via URL are available via STORE_CLIENT as usual.
+-------------------+----------------------------------------------------------------------------------+
| restParam | Optional. CSV list of variable names. E.g.: ``restParam=pId,adrId`` |
+-------------------+----------------------------------------------------------------------------------+
| restToken | Optional. User defined string or dynamic token (see :ref:`restAuthorization`). |
| restToken | Optional. User defined string or dynamic token (see :ref:``restAuthorization``). |
+-------------------+----------------------------------------------------------------------------------+
.. note::
There are no :ref:`special-column-names` available in `restSqlData` or `restSqlList`. Also there are no
There are no :ref:`special-column-names` available in ``restSqlData`` or ``restSqlList``. Also there are no
SIPs possible, cause REST typically does not offer sessions/cookies (which are necessary for SIPs).
......@@ -305,7 +308,7 @@ exist. In case of multiple tokens, replace the static string against a SQL query
.. tip::
The HTML Header Authorization token is available in STORE_CLIENT via '`{{Authorization:C:alnumx}}`.
The HTML Header Authorization token is available in STORE_CLIENT via '``{{Authorization:C:alnumx}}``.
Best Practice: For example all created tokens are saved in a table 'Auth' with a column 'token'. Define::
......
......@@ -880,8 +880,8 @@ Link Examples
.. _question:
Question
^^^^^^^^
Alert: Question
^^^^^^^^^^^^^^^
**Syntax**
......@@ -1614,6 +1614,8 @@ API Call QFQ Report (e.g. AJAX)
General use API call to fire a specific QFQ tt-content record. Useful for e.g. AJAX calls. No Typo3 is involved.
*No FE-Group access control*.
This describes the client side (=QFQ is client). For server function check :ref:`restApi`.
Example QFQ record JS::
# Register SIP with given arguments.
......@@ -1640,6 +1642,67 @@ Example QFQ record called by above AJAX::
10.sql = SELECT '{{arg1:S}} {{arg2:S}} {{arg3:C}} {{arg4:C}}', NOW()
.. _rest_client:
REST Client
^^^^^^^^^^^
.. note::
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.
Example::
# Retrieve information. Received data is delivered in JSON and decoded / copied on the fly to STORE_CLIENT
10.sql = SELECT 'n:https://www.dummy.ord/rest/person/id/123' AS _restClient
20.sql = SELECT 'Status: {{http-status:C}}<br>Name: {{name:C:alnumx}}<br>Surname: {{surname:C:alnumx}}'
# Simple POST request via https. Result is printed on the page.
10.sql = SELECT 'n:https://www.dummy.ord/rest/person/id/123|method:POST|content:{"name":"John";"surname":"Doe"}' AS _restClient
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| Token | Example | Comment |
+===================+================================+============================================================================+
| n | n:https://www.dummy.ord/rest/person | |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| method | method:POST | GET or POST |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| content | content:{"name":"John";"surname":"Doe"} | Depending on the REST server JSON might be expected |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| header | *see below* | |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| timeout | timeout:5 | Default: 5 seconds. |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
| ssl | ssl:{"verify_peer":true,"allow_self_signed":false} | JSON config for SSL settings |
+-------------------+----------------------------------------------------+--------------------------------------------------------+
**Header**
* Each header must be separated by ``\r\n``.
* An explicit given header will overwrite the named default header.
* Default header:
* *content-type: application/json* - if *content* starts with a ``{``.
* *content-type: text/plain* - if *content* does not start with a ``{``.
* *connection: close* - Necessary for HTTP 1.1.
**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 the returned answer is a valid JSON string, it's automatically copied STORE_CLIENT with corresponding key names.
JSON answer example::
Answer from Server: { 'name' : 'John'; 'street': 'Milky road' }
Retrieve the values via: {{name:C:alnumx}}, {{street:C:alnumx}}
.. _special-sql-functions:
Special SQL Functions (prepared statements)
......
......@@ -11,7 +11,6 @@ namespace IMATHUZH\Qfq\Api;
require_once(__DIR__ . '/../../vendor/autoload.php');
use IMATHUZH\Qfq\Core\QuickFormQuery;
use IMATHUZH\Qfq\Core\Helper\OnString;
$restId = array();
......
......@@ -1605,6 +1605,7 @@ const COLUMN_MAILTO = "mailto";
const COLUMN_SENDMAIL = "sendmail";
const COLUMN_VERTICAL = "vertical";
const COLUMN_WEBSOCKET = "websocket";
const COLUMN_REST_CLIENT = "restClient";
const COLUMN_NO_WRAP = "noWrap";
const COLUMN_HIDE = "hide";
......@@ -1761,6 +1762,7 @@ const TOKEN_DOWNLOAD = 'd';
const TOKEN_COPY_TO_CLIPBOARD = 'y';
const TOKEN_DROPDOWN = 'z';
const TOKEN_WEBSOCKET = 'w';
const TOKEN_REST_CLIENT = 'n';
const TOKEN_TEXT = 't';
const TOKEN_ALT_TEXT = 'a';
......@@ -1810,6 +1812,12 @@ const TOKEN_L_APPEND = 'append';
const TOKEN_L_INTERVAL = 'interval';
const TOKEN_L_HTML_ID = 'htmlId';
const TOKEN_L_METHOD = 'method';
const TOKEN_L_HEADER = 'header';
const TOKEN_L_CONTENT = 'content';
const TOKEN_L_TIMEOUT = 'timeout';
const TOKEN_L_SSL = 'ssl';
const MONITOR_MODE_APPEND_0 = '0';
const MONITOR_MODE_APPEND_1 = '1';
const MONITOR_SESSION_FILE_SEEK = 'monitor-seek-file';
......@@ -2009,3 +2017,5 @@ const I_ATTRIBUTE = 'attribute';
const I_CHECKED = 'checked';
const I_UNCHECKED = 'unchecked';
const HTTP_STATUS = 'http-status';
const ERROR_MESSAGE = 'error-message';
\ No newline at end of file
......@@ -8,7 +8,6 @@
namespace IMATHUZH\Qfq\Core\Database;
use IMATHUZH\Qfq\Core\Helper\BindParam;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
......
......@@ -58,7 +58,7 @@ use IMATHUZH\Qfq\Core\Store\Store;
* L:
* m:mailto
* M:Mode
* n:
* n:GET/POST Rest Call
* N:new
* o:ToolTip
* O:Monitor
......@@ -518,6 +518,16 @@ class Link {
return $this->renderLink(KeyValueStringParser::unparse($paramArr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER));
}
/**
* @param $str
* @return string
* @throws \UserFormException
* @throws \UserReportException
*/
public function processRestClient($str) {
}
/**
* @param $str
* @return string
......@@ -599,6 +609,11 @@ class Link {
case TOKEN_WEBSOCKET:
return $this->processWebSocket($str);
break;
case TOKEN_REST_CLIENT:
$restClient = new RestClient();
return $restClient->process($str);
break;
default:
break;
}
......
......@@ -939,6 +939,7 @@ class Report {
switch ($columnName) {
case COLUMN_LINK:
case COLUMN_WEBSOCKET:
case COLUMN_REST_CLIENT:
$content .= $this->link->renderLink($columnValue);
break;
......
<?php
namespace IMATHUZH\Qfq\Core\Report;
use Exception;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Store\Store;
/**
* Class RestClient
* @package IMATHUZH\Qfq\Core\Report
*/
class RestClient {
/**
* @var Store
*/
private $store = null;
/**
* RestClient constructor.
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
public function __construct() {
$this->store = Store::getInstance();
}
/**
* @param string $str
* @return string
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
public function process($str) {
$recvBuffer = '';
$recv = array();
$param = $this->parseArgument($str);
$options = array(
'http' => array(
'header' => $param[TOKEN_L_HEADER],
'method' => strtoupper($param[TOKEN_L_METHOD]),
'timeout' => $param[TOKEN_L_TIMEOUT],
)
);
if (isset($param[TOKEN_L_SSL])) {
$options['ssl'] = json_decode($param[TOKEN_L_SSL]);
}
// Add content only if there is one.
if (!empty($param[TOKEN_L_CONTENT])) {
$options['http']['content'] = $param[TOKEN_L_CONTENT];
}
$context = stream_context_create($options);
try {
if (false === ($recvBuffer = file_get_contents($param[TOKEN_REST_CLIENT], false, $context))) {
$recv[HTTP_STATUS] = 400;
$recv[ERROR_MESSAGE] = implode(", ", $http_response_header);
} else {
// If $recBuffer is no json - don't care, $recv will be null then.
$recv = json_decode($recvBuffer, true);
$recv[HTTP_STATUS] = 200;
}
} catch (Exception $e) {
$recvBuffer = '';
$recv[HTTP_STATUS] = $e->getCode();
$recv[ERROR_MESSAGE] = $e->getMessage();
}
// Copy new values to STORE_CLIENT
$this->store::setStore($recv, STORE_CLIENT, true);
return $recvBuffer;
}
/**
* Parses $str, fill some defaults and returns an array with given arguments.
*
* @param string $str
* @return array
* @throws \UserFormException
* @throws \UserReportException
*/
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"
// Split string
$param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
if (empty($param[TOKEN_REST_CLIENT])) {
throw new \UserReportException("Missing RestClient target", ERROR_MISSING_VALUE);
}
$param[TOKEN_L_CONTENT] = trim($param[TOKEN_L_CONTENT]);
$param[TOKEN_L_HEADER] = trim($param[TOKEN_L_HEADER]);
if (empty($param[TOKEN_L_METHOD])) {
$param[TOKEN_L_METHOD] = 'GET';
}
// Default Timeout
if (empty($param[TOKEN_L_TIMEOUT])) {
$param[TOKEN_L_TIMEOUT] = 5;
}
// If 'Host' is missing in header: define - useful for Firewall/ Proxy
// CR: if a header 'host' is given, REST calls fails always.
// $header = KeyValueStringParser::parse($param[TOKEN_L_HEADER], ':', '\r\n');
// if (empty($header['host'])) {
// $urlParts = parse_url($param[TOKEN_REST_CLIENT]);
// $header['host'] = $urlParts['host'];
// }
// If 'Content-type' is missing in header: define.
if (empty($header['content-type'])) {
// Poor man guess: if no 'content-type' is explicit given and string starts with '{' >> 'application/json'
$mime = (($param[TOKEN_L_CONTENT][0] ?? '') == '{') ? 'application/json' : 'text/plain';
$header['content-type'] = $mime . '; charset: utf-8';
}
// If 'Connection' is missing in Header: define
if (empty($header['connection'])) {
$header['connection'] = 'close';
}
// Join all header arguments to one string
$param[TOKEN_L_HEADER] = KeyValueStringParser::unparse($header, ': ', '\r\n') . '\r\n';
return $param;
}
}
\ No newline at end of file
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