Commit faf44578 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Fixes #11119 - restClient via ... AS _link

parent b54975ba
Pipeline #3779 passed with stages
in 4 minutes and 9 seconds
...@@ -38,8 +38,11 @@ REST ...@@ -38,8 +38,11 @@ REST
==== ====
Via `REST <https://en.wikipedia.org/wiki/Representational_state_transfer>`_ it's possible to access the QFQ based 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 application. Each REST API endpoint has to be defined as a QFQ Form.
four most used REST HTTP methods:
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 GET - Read
Shows a list of database records or a single record. The QFQ form holds the definition which and what to show. 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 ...@@ -77,14 +80,14 @@ Endpoint
``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/<level1>/<id1>/<level2>/<id2>/.../?<var1>=<value1>&...`` ``<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.: E.g.:
1. List of all persons: `<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person` 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` 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` 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` 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). 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 ...@@ -96,14 +99,14 @@ Only the last <level> of an URI will be processed. The former ones are just to f
.. note:: .. note::
Each level name (=form name) is available via STORE_CLIENT and name `_formX`. E.g. in example 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`. (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 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 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}}`. variable ``r`` in STORE_TYPO3, access it via ``{{r:T}}``.
GET - Read GET - Read
...@@ -124,8 +127,8 @@ list ...@@ -124,8 +127,8 @@ list
There are *no* native-FormElements necessary or loaded. Action FormElements will be processed. 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'. 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 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 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. the second one and so on.
GET Variables provided via URL are available via STORE_CLIENT as usual. 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. ...@@ -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`` | | 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:: .. 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). 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 ...@@ -305,7 +308,7 @@ exist. In case of multiple tokens, replace the static string against a SQL query
.. tip:: .. 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:: Best Practice: For example all created tokens are saved in a table 'Auth' with a column 'token'. Define::
......
...@@ -880,8 +880,8 @@ Link Examples ...@@ -880,8 +880,8 @@ Link Examples
.. _question: .. _question:
Question Alert: Question
^^^^^^^^ ^^^^^^^^^^^^^^^
**Syntax** **Syntax**
...@@ -1614,6 +1614,8 @@ API Call QFQ Report (e.g. AJAX) ...@@ -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. 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*. *No FE-Group access control*.
This describes the client side (=QFQ is client). For server function check :ref:`restApi`.
Example QFQ record JS:: Example QFQ record JS::
# Register SIP with given arguments. # Register SIP with given arguments.
...@@ -1640,6 +1642,67 @@ Example QFQ record called by above AJAX:: ...@@ -1640,6 +1642,67 @@ Example QFQ record called by above AJAX::
10.sql = SELECT '{{arg1:S}} {{arg2:S}} {{arg3:C}} {{arg4:C}}', NOW() 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:
Special SQL Functions (prepared statements) Special SQL Functions (prepared statements)
......
...@@ -11,7 +11,6 @@ namespace IMATHUZH\Qfq\Api; ...@@ -11,7 +11,6 @@ namespace IMATHUZH\Qfq\Api;
require_once(__DIR__ . '/../../vendor/autoload.php'); require_once(__DIR__ . '/../../vendor/autoload.php');
use IMATHUZH\Qfq\Core\QuickFormQuery; use IMATHUZH\Qfq\Core\QuickFormQuery;
use IMATHUZH\Qfq\Core\Helper\OnString; use IMATHUZH\Qfq\Core\Helper\OnString;
$restId = array(); $restId = array();
......
...@@ -1605,6 +1605,7 @@ const COLUMN_MAILTO = "mailto"; ...@@ -1605,6 +1605,7 @@ const COLUMN_MAILTO = "mailto";
const COLUMN_SENDMAIL = "sendmail"; const COLUMN_SENDMAIL = "sendmail";
const COLUMN_VERTICAL = "vertical"; const COLUMN_VERTICAL = "vertical";
const COLUMN_WEBSOCKET = "websocket"; const COLUMN_WEBSOCKET = "websocket";
const COLUMN_REST_CLIENT = "restClient";
const COLUMN_NO_WRAP = "noWrap"; const COLUMN_NO_WRAP = "noWrap";
const COLUMN_HIDE = "hide"; const COLUMN_HIDE = "hide";
...@@ -1761,6 +1762,7 @@ const TOKEN_DOWNLOAD = 'd'; ...@@ -1761,6 +1762,7 @@ const TOKEN_DOWNLOAD = 'd';
const TOKEN_COPY_TO_CLIPBOARD = 'y'; const TOKEN_COPY_TO_CLIPBOARD = 'y';
const TOKEN_DROPDOWN = 'z'; const TOKEN_DROPDOWN = 'z';
const TOKEN_WEBSOCKET = 'w'; const TOKEN_WEBSOCKET = 'w';
const TOKEN_REST_CLIENT = 'n';
const TOKEN_TEXT = 't'; const TOKEN_TEXT = 't';
const TOKEN_ALT_TEXT = 'a'; const TOKEN_ALT_TEXT = 'a';
...@@ -1810,6 +1812,12 @@ const TOKEN_L_APPEND = 'append'; ...@@ -1810,6 +1812,12 @@ const TOKEN_L_APPEND = 'append';
const TOKEN_L_INTERVAL = 'interval'; const TOKEN_L_INTERVAL = 'interval';
const TOKEN_L_HTML_ID = 'htmlId'; 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_0 = '0';
const MONITOR_MODE_APPEND_1 = '1'; const MONITOR_MODE_APPEND_1 = '1';
const MONITOR_SESSION_FILE_SEEK = 'monitor-seek-file'; const MONITOR_SESSION_FILE_SEEK = 'monitor-seek-file';
...@@ -2009,3 +2017,5 @@ const I_ATTRIBUTE = 'attribute'; ...@@ -2009,3 +2017,5 @@ const I_ATTRIBUTE = 'attribute';
const I_CHECKED = 'checked'; const I_CHECKED = 'checked';
const I_UNCHECKED = 'unchecked'; const I_UNCHECKED = 'unchecked';
const HTTP_STATUS = 'http-status';
const ERROR_MESSAGE = 'error-message';
\ No newline at end of file
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
namespace IMATHUZH\Qfq\Core\Database; namespace IMATHUZH\Qfq\Core\Database;
use IMATHUZH\Qfq\Core\Helper\BindParam; use IMATHUZH\Qfq\Core\Helper\BindParam;
use IMATHUZH\Qfq\Core\Helper\HelperFile; use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement; use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
......
...@@ -58,7 +58,7 @@ use IMATHUZH\Qfq\Core\Store\Store; ...@@ -58,7 +58,7 @@ use IMATHUZH\Qfq\Core\Store\Store;
* L: * L:
* m:mailto * m:mailto
* M:Mode * M:Mode
* n: * n:GET/POST Rest Call
* N:new * N:new
* o:ToolTip * o:ToolTip
* O:Monitor * O:Monitor
...@@ -518,6 +518,16 @@ class Link { ...@@ -518,6 +518,16 @@ class Link {
return $this->renderLink(KeyValueStringParser::unparse($paramArr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER)); 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 * @param $str
* @return string * @return string
...@@ -599,6 +609,11 @@ class Link { ...@@ -599,6 +609,11 @@ class Link {
case TOKEN_WEBSOCKET: case TOKEN_WEBSOCKET:
return $this->processWebSocket($str); return $this->processWebSocket($str);
break; break;
case TOKEN_REST_CLIENT:
$restClient = new RestClient();
return $restClient->process($str);
break;
default: default:
break; break;
} }
......
...@@ -939,6 +939,7 @@ class Report { ...@@ -939,6 +939,7 @@ class Report {
switch ($columnName) { switch ($columnName) {
case COLUMN_LINK: case COLUMN_LINK:
case COLUMN_WEBSOCKET: case COLUMN_WEBSOCKET:
case COLUMN_REST_CLIENT:
$content .= $this->link->renderLink($columnValue); $content .= $this->link->renderLink($columnValue);
break; 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
Supports Markdown
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