diff --git a/Documentation/REST.rst b/Documentation/REST.rst index 7605a52cfa8f08a340439ba8cda0a46f22b2893a..23711fdff68f2799a828ec02a8cc92340fe126b6 100644 --- a/Documentation/REST.rst +++ b/Documentation/REST.rst @@ -38,8 +38,11 @@ REST ==== Via `REST `_ 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 ``/typo3conf/ext/qfq/Classes/Api/rest.php/////.../?=&...`` -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: `/typo3conf/ext/qfq/Classes/Api/rest.php/person` -2. Data of person 123: `/typo3conf/ext/qfq/Classes/Api/rest.php/person/123` -3. Adresses of person 123: `/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address` -4. Adress details of address 45 from person 123: `/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address/45` +1. List of all persons: ``/typo3conf/ext/qfq/Classes/Api/rest.php/person`` +2. Data of person 123: ``/typo3conf/ext/qfq/Classes/Api/rest.php/person/123`` +3. Adresses of person 123: ``/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address`` +4. Adress details of address 45 from person 123: ``/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 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:: diff --git a/Documentation/Report.rst b/Documentation/Report.rst index 405f9e26779680d6ca230c7bfc07c0ba2acbbe74..5fdd76ea64db59b3f7ab4346c8f252c2f7f07c98 100644 --- a/Documentation/Report.rst +++ b/Documentation/Report.rst @@ -716,7 +716,7 @@ Column: _link +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |x | |Page |p: |p:impressum |Prepend '?' or '?id=', no hostname qualifier (automatically set by browser) | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|x | |Download |d:[] |d:complete.pdf |Link points to `api/download.php`. Additional parameter are encoded into a SIP. 'Download' needs an enabled SIP. See :ref:`download`. | +|x | |Download |d:[] |d:complete.pdf |Link points to `.../typo3conf/ext/qfq/Api/download.php`. Additional parameter SIP encoded. 'Download' needs SIP. See :ref:`download`. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |x | |Copy to |y:[some content] |y:this will be copied |Click on it copies the value of 'y:' to the clipboard. Optional a file ('F:...') might be specified as source. | | | |clipboard | | |See :ref:`copyToClipboard`. | @@ -816,6 +816,8 @@ render mode might dynamically control the rendered link. +------------+---------------------+--------------------+------------------+---------------------------------------------------------------------------+ |7 | pure url |pure url | |no link, pure url | +------------+---------------------+--------------------+------------------+---------------------------------------------------------------------------+ +|8 | pure sip |pure sip | |no link, no html, only the 13 digit sip code. | ++------------+---------------------+--------------------+------------------+---------------------------------------------------------------------------+ Example:: @@ -878,8 +880,8 @@ Link Examples .. _question: -Question -^^^^^^^^ +Alert: Question +^^^^^^^^^^^^^^^ **Syntax** @@ -1600,6 +1602,107 @@ Example:: FROM Person AS p   +.. _api_call_qfq_report: + +API Call QFQ Report (e.g. AJAX) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + + QFQ Report functionality protected by SIP offered to simple API calls: ``typo3conf/ext/qfq/Api/dataReport.php?s=....`` + +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. + 10.sql = SELECT 'U:uid=12345&arg1=Hello&arg2=World|s|r:8' AS '_link|col1' + # Build JS + 10.tail = + +Example QFQ record called by above AJAX:: + + # Create a dedicated tt-content record (on any T3 page, might be on the same page as the JS code). + # The example above assumes that this record has the tt_content.uid=12345. + render = api + 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}}
Name: {{name:C:alnumx}}
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`_. + 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) diff --git a/Documentation/Store.rst b/Documentation/Store.rst index ddba85ae878aaae180aa282f00cf536c83ec326d..b43831b8eae9aaadd9853e32e6663f0caa20014a 100644 --- a/Documentation/Store.rst +++ b/Documentation/Store.rst @@ -255,7 +255,10 @@ Store: *VARS* - V +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Explanation | +=========================+============================================================================================================================================+ -| random | Random string with length of 32 alphanum chars (lower & upper case). This is variable is always filled. | +| random | Random string with length of 32 alphanum chars (lower & upper case). This variable is always filled. Each access gives a new value. | ++-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ +| randomUniq | Like {{random:V}} but it's guaranteed that it's uniq. Optional: as *default* define an expiration time. Example: ``{{randomUniq:V}}``, | +| | ``{{randomUniq:V:::2020-12-15}}``, ``{{randomUniq:V:::+ 10 sec}}`` (sec, min, day, ... - MySQL notation DATE_ADD() ). | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | slaveId | see :ref:`slave-id` | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/Documentation/Variable.rst b/Documentation/Variable.rst index c73f899f1b4885e9fd0edf755b575e0d06b0e43b..c4f4fb86be1c004ea86eddb2b499a4a66cfe633e 100644 --- a/Documentation/Variable.rst +++ b/Documentation/Variable.rst @@ -86,7 +86,7 @@ Store variables .. note:: - Syntax: {{ variable name : :ref:`store` : :ref:`sanitize-class` : :ref:`variable-escape` : :ref:`variable-default` : :ref:`variable-type-message-violate` }} + {{ *variable name* : :ref:`store` : :ref:`sanitize-class` : :ref:`variable-escape` : :ref:`variable-default` : :ref:`variable-type-message-violate` }} Example:: @@ -105,7 +105,7 @@ Example:: * If no store is specified, the default for the searched stores are: **FSRVD** (=FORM > SIP > RECORD > VARS > DEFAULT). * If the VarName is not found in one store, the next store is searched, up to the last specified store. * If the VarName is not found and a default value is given, the default is returned. -* If no value is found, nothing is replaced - the string '{{}}' remains. +* If no value is found, nothing is replaced - the string ``{{}}`` remains. * If anywhere along the line an empty string is found, this **is** a value: therefore, the search will stop. .. _`sanitize-class`: @@ -117,7 +117,7 @@ Values in STORE_CLIENT *C* (Client=Browser) and STORE_FORM *F* (Form, HTTP 'post sanitize class. Values from other stores are *not* checked against any sanitize class, even if a sanitize class is specified. * Variables get by default the sanitize class defined in the corresponding `FormElement`. If not defined, - the default class is 'digit'. + the default class is ``digit``. * A default sanitize class can be overwritten by individual definition: *{{a:C:alnumx}}* * If a value violates the specific sanitize class, see :ref:`variable-type-message-violate` for default or customized message. By default the value becomes `!!!!`. E.g. `!!digit!!`. @@ -133,7 +133,7 @@ For QFQ variables and FormElements: +------------------+------+-------+-----------------------------------------------------------------------------------------+ | **numerical** | Form | Query | [0-9.-+] | +------------------+------+-------+-----------------------------------------------------------------------------------------+ -| **allbut** | Form | Query | All characters allowed, but not [ ] { } % \ #. The used regexp: '^[^\[\]{}%\\#]+$', | +| **allbut** | Form | Query | All characters allowed, but not [ ] { } % \ #. The used regexp: ``^[^\[\]{}%\\#]+$',`` | +------------------+------+-------+-----------------------------------------------------------------------------------------+ | **all** | Form | Query | no sanitizing | +------------------+------+-------+-----------------------------------------------------------------------------------------+ @@ -158,8 +158,8 @@ Rules for CheckType Auto (by priority): * integer type: **digit** * floating point number: **numerical** * FE Type - * 'password', 'note': **all** - * 'editor', 'text' and encode = 'specialchar': **all** + * ``password``, ``note``: **all** + * ``editor``, ``text`` and encode = ``specialchar``: **all** * None of the above: **alnumx** @@ -216,9 +216,9 @@ The following `escape` & `action` types are available: Escape ^^^^^^ -To 'escape' a character typically means: a character, which have a special meaning/function, should not treated as a special +To *escape* a character typically means: a character, which have a special meaning/function, should not treated as a special character. -E.g. a string is surrounded by single ticks '. If such a string should contain the same single tick inside, +E.g. a string is surrounded by single ticks ``'``. If such a string should contain the same single tick inside, the inside single tick has to be escaped - if not, the string end's at the second tick, not the third. This is typically done by a backlash: \\ @@ -229,21 +229,21 @@ Especially variables used in SQL statements might cause trouble when using: NUL Action ^^^^^^ -* *password* - 'p': transforms the value of the variable into a Typo3 salted password hash. The hash function is the one +* *password* - ``p``: transforms the value of the variable into a Typo3 salted password hash. The hash function is the one used by Typo3 to encrypt and salt a password. This is useful to manipulate FE user passwords via QFQ. See :ref:`setFeUserPassword` -* *stop replace* - 'S': typically QFQ will replace nested variables as long as there are variables to replace. This options +* *stop replace* - ``S``: typically QFQ will replace nested variables as long as there are variables to replace. This options stops this -* *exception* - 'X': If a variable is not found in any given store, it's replace by a default value or an error message. +* *exception* - ``X``: If a variable is not found in any given store, it's replace by a default value or an error message. In special situation it might be useful to do a full stop on all current actions (no further procession). A custom message can be defined via: :ref:`variable-type-message-violate`. .. _`variable-escape-wipe-key`: -* *wipe* - 'w': In special cases it might be useful to get a value via SIP only one time and after retrieving the value - it will be deleted in STORE SIP . Further access to the variable will return 'variable undefined'. At time of writing - only the STORE SIP supports the feature 'wipe'. This is useful to suppress any repeating events by using the browser history. +* *wipe* - ``w``: In special cases it might be useful to get a value via SIP only one time and after retrieving the value + it will be deleted in STORE SIP . Further access to the variable will return *variable undefined*. At time of writing + only the STORE SIP supports the feature *wipe*. This is useful to suppress any repeating events by using the browser history. The following example will send a mail only the first when it is called with a given SIP:: 10.sql = SELECT '...' AS _sendmail FROM Person AS p WHERE '{{action:S::w}}'='send' AND p.id={{pId:S}} @@ -258,7 +258,8 @@ Default * Any string can be given to define a default value. * If a default value is given, it makes no sense to define more than one store: with a default value given, only the first store is considered. -* If the default value contains a ':', that one needs to be escaped by \\ +* If the default value contains a ``:``, that one needs to be escaped by ``\`` +* For dedicated variables this value has a special meaning. E.g. ``{{randomUniq:V}}`` uses this as ``expire`` argument. .. _`variable-type-message-violate`: @@ -267,11 +268,11 @@ Type message violate If a value violates the sanitize class, the following actions are possible: -* 'c' - The violated class will be set as content, surrounded by '!!'. E.g. '!!digit!!'. This is the default. -* 'e' - Instead of the value an empty string will be set as content. -* '0' - Instead of the value the string '0' will be set as content. -* 'custom text ...' - Instead of the value, the custom text will be set as content. If the text contains a ':', that one - needs to be escaped by \\ . Check :ref:`variable-escape` qualifier 'C' to let QFQ do the colon escaping. +* ``c`` - The violated class will be set as content, surrounded by *!!*. E.g. *!!digit!!*. This is the default. +* ``e`` - Instead of the value an empty string will be set as content. +* ``0`` - Instead of the value the string *0* will be set as content. +* *custom text ...* - Instead of the value, the custom text will be set as content. If the text contains a ``:``, that one + needs to be escaped by \\ . Check :ref:`variable-escape` qualifier ``C`` to let QFQ do the colon escaping. .. _`sql-variables`: @@ -300,7 +301,7 @@ Result: row """"""""""" A few functions needs more than a returned string, instead separate columns are necessary. To indicate an array -result, specify those with an '!': :: +result, specify those with an ``!``:: {{!SELECT ...}} @@ -329,7 +330,7 @@ Example :: {{SELECT id, name FROM Person}} - {{SELECT id, name, IF({{feUser:T0}}=0,'Yes','No') FROM Person WHERE id={{r:S}} }} + {{SELECT id, name, IF({{feUser:T0}}=0, 'Yes', 'No') FROM Person WHERE id={{r:S}} }} {{SELECT id, city FROM Address AS adr WHERE adr.accId={{SELECT id FROM Account AS acc WHERE acc.name={{feUser:T0}} }} }} {{!SELECT id, name FROM Person}} {{[2]SELECT id, name FROM Form}} @@ -344,8 +345,8 @@ Syntax: *{{.}}* Only used in report to access outer columns. See :ref:`access-column-values` and :ref:`syntax-of-report`. -There might be name conflicts between VarName / SQL keywords and . QFQ checks first for '', -than for 'SQL keywords' and than for 'VarNames' in stores. +There might be name conflicts between VarName / SQL keywords and . QFQ checks first for **, +than for *SQL keywords* and than for *VarNames* in stores. All types might be nested with each other. There is no limit of nesting variables. @@ -363,7 +364,7 @@ Link column variables ^^^^^^^^^^^^^^^^^^^^^ These variables return a link, completely rendered in HTML. The syntax and all features of :ref:`column-link` are available. -The following code will render a 'new person' button:: +The following code will render a *new person* button:: {{p:form&form=Person|s|N|t:new person AS link}} diff --git a/extension/Classes/Api/rest.php b/extension/Classes/Api/rest.php index 38be130bd6ecf13d38ebd2e3f5b19c4cdd69cfa7..cf4fd9391665d715ab361c11417d08bd85c82766 100644 --- a/extension/Classes/Api/rest.php +++ b/extension/Classes/Api/rest.php @@ -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(); diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index 119a213173a169d1b15b13e71c009acbe21963ab..0642db1820b3df61c201665e26e0e13acac577c5 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -724,6 +724,7 @@ const SIP_EXCLUDE_XDEBUG_SESSION_START = 'XDEBUG_SESSION_START'; const ACTION_KEYWORD_SLAVE_ID = 'slaveId'; const VAR_RANDOM = 'random'; +const VAR_RANDOM_UNIQ = 'randomUniq'; const VAR_FILE_DESTINATION = 'fileDestination'; const VAR_SLAVE_ID = ACTION_KEYWORD_SLAVE_ID; const VAR_FILENAME = 'filename'; // Original filename of an uploaded file. @@ -1604,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"; @@ -1760,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'; @@ -1809,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'; @@ -2008,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 diff --git a/extension/Classes/Core/Database/Database.php b/extension/Classes/Core/Database/Database.php index dfc42d36b3706808785ea5d36acc96f129d93d15..79430583b6e38c13b63db9480e150e58b661b6c0 100644 --- a/extension/Classes/Core/Database/Database.php +++ b/extension/Classes/Core/Database/Database.php @@ -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; diff --git a/extension/Classes/Core/Database/DatabaseUpdateData.php b/extension/Classes/Core/Database/DatabaseUpdateData.php index d6da9c2a918206cc640e03c0baaf6b9337fa0290..d4fe2661bed54fb50890fbf75aedb7b7e9355896 100644 --- a/extension/Classes/Core/Database/DatabaseUpdateData.php +++ b/extension/Classes/Core/Database/DatabaseUpdateData.php @@ -194,7 +194,9 @@ $UPDATE_ARRAY = array( "ALTER TABLE `FormElement` CHANGE `label` `label` VARCHAR(1023) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '';", ], - + '20.9.0' => [ + "CREATE TABLE `Uniq` (`id` int(11) NOT NULL AUTO_INCREMENT, `random` char(32) NOT NULL, `expire` datetime NOT NULL, `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `random` (`random`) USING BTREE, KEY `expire` (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;);", + ], ); diff --git a/extension/Classes/Core/Evaluate.php b/extension/Classes/Core/Evaluate.php index 55521c573afd2b734360c4e3ed56ceb4989a9077..4804b76fe4e174c1d5c91b698a812c99f43d0a77 100644 --- a/extension/Classes/Core/Evaluate.php +++ b/extension/Classes/Core/Evaluate.php @@ -392,7 +392,8 @@ class Evaluate { $typeMessageViolate = ($arrToken[VAR_INDEX_MESSAGE] === null || $arrToken[VAR_INDEX_MESSAGE] === '') ? SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS : $arrToken[VAR_INDEX_MESSAGE]; // search for value in stores - $value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANATIZE], $foundInStore, $typeMessageViolate); + $value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANATIZE], + $foundInStore, $typeMessageViolate, $arrToken[VAR_INDEX_DEFAULT]); // escape ticks if (is_string($value)) { diff --git a/extension/Classes/Core/Report/Link.php b/extension/Classes/Core/Report/Link.php index 21bfbcfc5c94e5b19e81b9e23310a7fb1fc16f27..d5eabae8a68faf49277431237135da570750fe04 100644 --- a/extension/Classes/Core/Report/Link.php +++ b/extension/Classes/Core/Report/Link.php @@ -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; } diff --git a/extension/Classes/Core/Report/Report.php b/extension/Classes/Core/Report/Report.php index 31a97726b3c5ac418c717b64711d96805fd316f8..31665c12ba2293420fcc55081185775bd40301c3 100644 --- a/extension/Classes/Core/Report/Report.php +++ b/extension/Classes/Core/Report/Report.php @@ -939,6 +939,7 @@ class Report { switch ($columnName) { case COLUMN_LINK: case COLUMN_WEBSOCKET: + case COLUMN_REST_CLIENT: $content .= $this->link->renderLink($columnValue); break; diff --git a/extension/Classes/Core/Report/RestClient.php b/extension/Classes/Core/Report/RestClient.php new file mode 100644 index 0000000000000000000000000000000000000000..1872cd83f40c80ea5f5847ff2f8d6a7790e39727 --- /dev/null +++ b/extension/Classes/Core/Report/RestClient.php @@ -0,0 +1,137 @@ +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 diff --git a/extension/Classes/Core/Store/Store.php b/extension/Classes/Core/Store/Store.php index 60dec91a3140c2d079c54434b5c2ef482547204b..8db9b8957bafde2e709f77e19ec848064121fd7f 100644 --- a/extension/Classes/Core/Store/Store.php +++ b/extension/Classes/Core/Store/Store.php @@ -9,12 +9,12 @@ namespace IMATHUZH\Qfq\Core\Store; use IMATHUZH\Qfq\Core\Database\Database; +use IMATHUZH\Qfq\Core\Helper\HelperFile; use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser; use IMATHUZH\Qfq\Core\Helper\Logger; use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Helper\Sanitize; use IMATHUZH\Qfq\Core\Helper\Support; -use IMATHUZH\Qfq\Core\Helper\HelperFile; /* * Stores: @@ -296,7 +296,7 @@ class Store { $config[SYSTEM_SEND_E_MAIL] = $config[SYSTEM_EXT_PATH] . '/Classes/External/sendEmail'; // Make path absolute - foreach ([SYSTEM_MAIL_LOG, SYSTEM_QFQ_LOG, SYSTEM_SQL_LOG] AS $key) { + foreach ([SYSTEM_MAIL_LOG, SYSTEM_QFQ_LOG, SYSTEM_SQL_LOG] as $key) { if (!empty($config[$key]) && $config[$key][0] != '/') { $config[$key] = HelperFile::joinPathFilename($config[SYSTEM_SITE_PATH], $config[$key]); } @@ -677,9 +677,11 @@ class Store { * @return string|array a) if found: value, b) false. STORE_EXTRA returns an array for the given key. * @throws \CodeException * @throws \UserFormException SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO | SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY | SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS + * @throws \UserReportException + * @throws \DbException */ public static function getVar($key, $useStores = STORE_USE_DEFAULT, $sanitizeClass = '', &$foundInStore = '', - $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS) { + $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS, $default = '') { // no store specified? if ($useStores === "" || $useStores === null) { @@ -713,6 +715,8 @@ class Store { case STORE_VAR: if ($finalKey === VAR_RANDOM) { return Support::randomAlphaNum(RANDOM_LENGTH); + } elseif ($finalKey === VAR_RANDOM_UNIQ) { + return StoreVar::randomUniq($default); } else { continue 2; // no value provided, continue with while loop } diff --git a/extension/Classes/Core/Store/StoreVar.php b/extension/Classes/Core/Store/StoreVar.php new file mode 100644 index 0000000000000000000000000000000000000000..8c88343b9c4d8edd08578e2501d3a380ce330ed6 --- /dev/null +++ b/extension/Classes/Core/Store/StoreVar.php @@ -0,0 +1,94 @@ +getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM)); + } + + $expire = trim($expire); + if ($expire == '') { + $expire = '9999-12-31'; + } + + if ($expire[0] == '+') { + // $expire: '+1 day' >> '+ INTERVAL 1 day' + $expire = "NOW() + INTERVAL " . SUBSTR($expire, 1); + } else { + $expire = "CONVERT('$expire', DATETIME)"; + } + + // Purge expired records + if (!self::$purged) { + self::$db->sql("DELETE FROM Uniq WHERE expiresql("INSERT INTO Uniq (`random`,`expire`) VALUES ( '$random', $expire)"); + } catch (\DbException $e) { + $message = $e->getMessage(); + + // In case there was an error: check for duplicate key + // message: {"toUser":"SQL error","support":"","os":"[ mysqli: 1062 ] Duplicate entry '...' for key 'random'"} + if (strpos($message, '1062') !== false && strpos($message, "for key 'random'") !== false) { + continue; // Key already exist, new try. + } else { + throw new \DbException($message, $e->getCode()); // Something else happened: report. + } + } + return $random; + } + + throw new \DbException('Too much iterations to find {{randomUniq:V}}'); + } +} \ No newline at end of file diff --git a/extension/Classes/Sql/formEditor.sql b/extension/Classes/Sql/formEditor.sql index 59861b767f90209d899a0d2e7e897dd346fa9502..c45078421cce39581a7a9c66279e6620a3a10163 100644 --- a/extension/Classes/Sql/formEditor.sql +++ b/extension/Classes/Sql/formEditor.sql @@ -57,44 +57,44 @@ CREATE TABLE IF NOT EXISTS `Form` CREATE TABLE IF NOT EXISTS `FormElement` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `formId` INT(11) NOT NULL, - `feIdContainer` INT(11) NOT NULL DEFAULT '0', - `dynamicUpdate` ENUM ('yes', 'no') NOT NULL DEFAULT 'no', + `id` INT(11) NOT NULL AUTO_INCREMENT, + `formId` INT(11) NOT NULL, + `feIdContainer` INT(11) NOT NULL DEFAULT '0', + `dynamicUpdate` ENUM ('yes', 'no') NOT NULL DEFAULT 'no', - `enabled` ENUM ('yes', 'no') NOT NULL DEFAULT 'yes', + `enabled` ENUM ('yes', 'no') NOT NULL DEFAULT 'yes', - `name` VARCHAR(255) NOT NULL DEFAULT '', - `label` VARCHAR(1023) NOT NULL DEFAULT '', + `name` VARCHAR(255) NOT NULL DEFAULT '', + `label` VARCHAR(1023) NOT NULL DEFAULT '', - `mode` ENUM ('show', 'required', 'readonly', 'hidden') NOT NULL DEFAULT 'show', - `modeSql` TEXT NOT NULL, - `class` ENUM ('native', 'action', 'container') NOT NULL DEFAULT 'native', - `type` ENUM ('checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', + `mode` ENUM ('show', 'required', 'readonly', 'hidden') NOT NULL DEFAULT 'show', + `modeSql` TEXT NOT NULL, + `class` ENUM ('native', 'action', 'container') NOT NULL DEFAULT 'native', + `type` ENUM ('checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', 'gridJQW', 'text', 'editor', 'annotate', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'imageCut', 'fieldset', 'pill', 'templateGroup', 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', - 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail', 'paste') NOT NULL DEFAULT 'text', - `subrecordOption` SET ('edit', 'delete', 'new') NOT NULL DEFAULT '', - `encode` ENUM ('none', 'specialchar') NOT NULL DEFAULT 'specialchar', - `checkType` ENUM ('auto', 'alnumx', 'digit', 'numerical', 'email', 'pattern', 'allbut', - 'all') NOT NULL DEFAULT 'auto', - `checkPattern` VARCHAR(255) NOT NULL DEFAULT '', - - `onChange` VARCHAR(255) NOT NULL DEFAULT '', - - `ord` INT(11) NOT NULL DEFAULT '0', - `tabindex` INT(11) NOT NULL DEFAULT '0', - - `size` VARCHAR(255) NOT NULL DEFAULT '', - `maxLength` VARCHAR(255) NOT NULL DEFAULT '', - `labelAlign` ENUM ('default', 'left', 'center', 'right') NOT NULL DEFAULT 'default', - `bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '', - `bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '', - `bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '', - `rowLabelInputNote` SET ('row', 'label', '/label', 'input', '/input', 'note', '/note', '/row') NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row', - `note` TEXT NOT NULL, - `adminNote` TEXT NOT NULL, + 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail', 'paste') NOT NULL DEFAULT 'text', + `subrecordOption` SET ('edit', 'delete', 'new') NOT NULL DEFAULT '', + `encode` ENUM ('none', 'specialchar') NOT NULL DEFAULT 'specialchar', + `checkType` ENUM ('auto', 'alnumx', 'digit', 'numerical', 'email', 'pattern', 'allbut', + 'all') NOT NULL DEFAULT 'auto', + `checkPattern` VARCHAR(255) NOT NULL DEFAULT '', + + `onChange` VARCHAR(255) NOT NULL DEFAULT '', + + `ord` INT(11) NOT NULL DEFAULT '0', + `tabindex` INT(11) NOT NULL DEFAULT '0', + + `size` VARCHAR(255) NOT NULL DEFAULT '', + `maxLength` VARCHAR(255) NOT NULL DEFAULT '', + `labelAlign` ENUM ('default', 'left', 'center', 'right') NOT NULL DEFAULT 'default', + `bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '', + `bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '', + `bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '', + `rowLabelInputNote` SET ('row', 'label', '/label', 'input', '/input', 'note', '/note', '/row') NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row', + `note` TEXT NOT NULL, + `adminNote` TEXT NOT NULL, `tooltip` VARCHAR(255) NOT NULL DEFAULT '', `placeholder` VARCHAR(2048) NOT NULL DEFAULT '', @@ -679,4 +679,18 @@ CREATE TABLE IF NOT EXISTS `Setting` KEY `name` (`name`), KEY `typeFeUserUidTableIdPublic` (`type`, `feUser`, `tableId`, `public`) USING BTREE ) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4; \ No newline at end of file + DEFAULT CHARSET = utf8mb4; + +CREATE TABLE `Uniq` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `random` char(32) NOT NULL, + `expire` datetime NOT NULL, + `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `random` (`random`) USING BTREE, + KEY `expire` (`id`) + +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; diff --git a/extension/Tests/Unit/Core/Database/DatabaseUpdateTest.php b/extension/Tests/Unit/Core/Database/DatabaseUpdateTest.php index 1b2a94bb3caf17fe8e64c29a88fc90ce962f537a..df8d2b9c24340ea0587ac83cd6739a739adb9da5 100644 --- a/extension/Tests/Unit/Core/Database/DatabaseUpdateTest.php +++ b/extension/Tests/Unit/Core/Database/DatabaseUpdateTest.php @@ -35,7 +35,7 @@ class DatabaseUpdateTest extends AbstractDatabaseTest { public function testCheckNupdate() { // $countQfqTables = 9; - $countQfqTables = 10; + $countQfqTables = 11; $store = Store::getInstance();