Commit b592377d authored by Carsten  Rose's avatar Carsten Rose

Merge branch 'F11076_AS_websocket' into 'master'

F11076 as websocket

See merge request !279
parents 68292e01 b2d798e2
Pipeline #3760 passed with stages
in 3 minutes and 54 seconds
.. ==================================================
.. ==================================================
.. ==================================================
.. Header hierarchy
.. ==
.. --
.. ^^
.. ""
.. ;;
.. ,,
..
.. --------------------------------------------used to the update the records specified ------
.. Best Practice T3 reST: https://docs.typo3.org/m/typo3/docs-how-to-document/master/en-us/WritingReST/CheatSheet.html
.. Reference: https://docs.typo3.org/m/typo3/docs-how-to-document/master/en-us/WritingReST/Index.html
.. Italic *italic*
.. Bold **bold**
.. Code ``text``
.. External Links: `Bootstrap <http://getbootstrap.com/>`_
.. Add Images: .. image:: ../Images/a4.jpg
..
..
.. Admonitions
.. .. note:: .. important:: .. tip:: .. warning::
.. Color: (blue) (orange) (green) (red)
..
.. Definition:
.. some text becomes strong (only one line)
.. description has to indented
.. -*- coding: utf-8 -*- with BOM.
.. include:: Includes.txt
.. _`CodingGuideline`:
Coding Guideline
================
The following is not mandatory but shows some best practices:
Constants
---------
* Define constants in ``Extensions > QFQ > Custom > ...``
* Dynamic values under ``Extensions > QFQ > Dynamic > ...``
QFQ content record
------------------
* Name the record in the header field with:
* Regular content: ``[QFQ] ...``
* Content in the left column: ``[QFQ,L] ...``
* Content in englisch: ``[QFQ,E] ...``
* The first lines should be comments, explaining what the record does and list all passed variables::
#
# Shows list of Persons living in {{country:SE}}
#
# {{country:SE}}
#
* A good practice is to define all possible STORE_SIP Parameter in a SQL at the beginning and copy them to STORE_RECORD::
10 {
# Normalize variables
sql = SELECT '{{country:SE}}' AS _country
# List selected persons
20.sql = SELECT p.name FROM Person AS p WHERE p.country LIKE '{{country:R}}'
}
* Always comment the queries like shown above.
......@@ -494,9 +494,15 @@ Columns of the upper / outer level result can be accessed via variables in two w
The STORE_RECORD will always be merged with previous content. The Level Keys are unique.
Multiple columns, with the same column name, can't be accessed individually. Only the last column is available.
.. important::
Multiple columns, with the same column name, can't be accessed individually. Only the last column is available.
Retrieving the *final* value of :ref:`special-column-names` is possible via '{{&<column>:R}}. Example::
.. important::
Retrieving the *final* value of :ref:`special-column-names` is possible via '{{&<column>:R}} (there is an '&' direct behind '{{')
Example::
10.sql = SELECT 'p:home&form=Person|s|b:success|t:Edit' AS _link
10.20.sql = SELECT '{{link:R}}', '{{&link:R}}'
......@@ -599,12 +605,12 @@ One exception are columns, whose name starts with '_'. E.g.::
content will be hidden.
* The fourth column (alias name 'link') uses a QFQ special column name. Here, only in this example, it has no
further meaning.
* All columns in a row with the same special column name (e.g. ``... AS _page``) will have the same column name: 'page'.
To access individual columns a uniq column title can be added::
* All columns in a row, with the same special column name (e.g. ``... AS _page``) will have the same column name: 'page'.
To access individual columns a uniq column title is necessary and can be added ``|_column1``::
10.sql = SELECT '..1..' AS '_page|column1', '..2..' AS '_page|column2'
Those columns can be accessed via ``{{10.column1}}`` , ``{{10.column2}}`` or ``{{column1:R}}`` , ``{{column2:R}}``
Those columns can be accessed via ``{{10.column1}}`` , ``{{10.column2}}`` or (recommended) ``{{column1:R}}`` , ``{{column2:R}}``
* To skip wrapping via ``fbeg``, ``fsep``, ``fend`` for dedicated columns, add the keyword ``|_noWrap`` to the column alias.
Example::
......@@ -612,6 +618,7 @@ One exception are columns, whose name starts with '_'. E.g.::
Summary:
* Special column names always start with '_'.
* Columns starting with a '_' but not defined as as QFQ special column name are hidden(!) - in other words: they are
not **printed** as output.
......@@ -714,7 +721,9 @@ Column: _link
|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`. |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Dropdown menu |z |z||p:home|t:Home |Creates a dropdown menu. SEe :ref:`dropdownMenu`. |
| | |Dropdown menu |z |z||p:home|t:Home |Creates a dropdown menu. See :ref:`dropdownMenu`. |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |websocket |w:ws://<host>:<port>/<path> | w:ws://localhost:123/demo |Send message given in 't:...' to websocket. See :ref:`websocket`. |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Text |t:<text> |t:Firstname Lastname | |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
......@@ -1406,6 +1415,7 @@ Most of the other Link-Class attributes can be used to customize the link. ::
* Parameter are position independent.
* *<params>*: see :ref:`download-parameter-files`
* For column `_pdf` and `_zip`, the element sources `p:...`, `U:...`, `u:...`, `F:...` might repeated multiple times.
* To only render the page content without menus add the parameter `type=2`. For example: `U:id=pageToPrint&type=2&_sip=1&r=', r.id`
* Example::
10.sql = SELECT "F:fileadmin/test.pdf" as _pdf, "F:fileadmin/test.pdf" as _file, "F:fileadmin/test.pdf" as _zip
......@@ -2182,6 +2192,49 @@ Line 6: A PDF download.
Line 7: A disabled menu entry.
.. _websocket:
WebSocket
---------
Sending messages via WebSocket and receiving the answer is done via: ::
SELECT 'w:ws://<host>:<port>/<path>|t:<message>' AS _websocket
Instead of '... AS _websocket' it's also possible to use '... AS _link'.
The answer is written to output and stored in the given column (in this case 'websocket' or 'link').
.. tip::
To suppress the direct output, add '_hide' to the column name.
Example::
SELECT 'w:ws://<host>:<port>/<path>|t:<message>' AS '_websocket|_hide'
.. tip::
To define a uniq column name (to access it later via STORE_RECORD).
Example::
SELECT 'w:ws://<host>:<port>/<path>|t:<message>' AS '_websocket|myName'
.. tip::
Get the answer from STORE_RECORD by using '{{&...'. Check `access-column-values`_.
Example::
SELECT 'w:ws://<host>:<port>/<path>|t:<message>' AS '_websocket|myName'
Results:
'{{myName:R}}' >> 'w:ws://<host>:<port>/<path>|t:<message>'
'{{&myName:R}}' >> '<received socket answer>'
.. _drag_and_drop:
Drag and drop
......
......@@ -254,7 +254,7 @@ system cron run.
Type: Mail
""""""""""
At the moment there is a special sendmail notation - this will change in the future.
Currently QFQ uses a special sendmail notation - this will change in the future.
* `Mail`: ::
......@@ -263,6 +263,7 @@ At the moment there is a special sendmail notation - this will change in the fut
AutoCron will send as many mails as records are selected by the SQL query in field `Mail`. Field `Mail body` provides
the mail text.
All columns of the SQL are available in STORE_PARENT.
Type: Website
"""""""""""""
......
......@@ -86,6 +86,7 @@ This documentation is for the TYPO3 extension **qfq**.
System
ApplicationTest
GeneralTips
CodingGuideline
Release
License
Sitemap
......
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 08/09/20
* Time: 6:17 PM
*/
namespace IMATHUZH\Qfq\Api;
require_once(__DIR__ . '/../../vendor/autoload.php');
use IMATHUZH\Qfq\Core\QuickFormQuery;
/**
* Return JSON encoded answer
*
* status: success|error
* message: <message>
* redirect: client|url|no
* redirect-url: <url>
*
* Description:
*
* Save successful.
*
*/
$answer = array();
$answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO;
$answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
$answer[API_MESSAGE] = '';
$status = HTTP_400_BAD_REQUEST;
try {
try {
$qfq = new QuickFormQuery(['bodytext' => '']);
$data = $qfq->dataReport();
$status = HTTP_200_OK;
} catch (\UserReportException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (\DbException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
}
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
//header('HTTP/1.0 ' . $status);
//header("Content-Type: application/json");
//echo json_encode($answer);
echo $data;
\ No newline at end of file
......@@ -1603,6 +1603,7 @@ const COLUMN_IMG = "img";
const COLUMN_MAILTO = "mailto";
const COLUMN_SENDMAIL = "sendmail";
const COLUMN_VERTICAL = "vertical";
const COLUMN_WEBSOCKET = "websocket";
const COLUMN_NO_WRAP = "noWrap";
const COLUMN_HIDE = "hide";
......@@ -1758,6 +1759,7 @@ const TOKEN_UID = 'uid';
const TOKEN_DOWNLOAD = 'd';
const TOKEN_COPY_TO_CLIPBOARD = 'y';
const TOKEN_DROPDOWN = 'z';
const TOKEN_WEBSOCKET = 'w';
const TOKEN_TEXT = 't';
const TOKEN_ALT_TEXT = 'a';
......
......@@ -175,13 +175,13 @@ class Database {
* Else an exception. ROW_EXPECT_GE_1: Like 'ROW_REGULAR'. Throws an exception if there is an empty resultset.
* ROW_KEYS: Return 2-dimensional num(!) array. Every query row is one array row. $keys are the column names.
*
* @param $sql
* @param string $sql
* @param string $mode
* @param array $parameterArray
* @param string $specificMessage
* @param array $keys
* @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS
* @param array $skipErrno
* @param array $skipErrno Array of ERRNO numbers, which should be skipped and not throw an error.
*
* @return array|int
* SELECT | SHOW | DESCRIBE | EXPLAIN: see $mode
......@@ -349,7 +349,7 @@ class Database {
* the query.
* @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS
* @param string $specificMessage
* @param array $skipErrno
* @param array $skipErrno Array of ERRNO numbers, which should be skipped and not throw an error.
*
* @return int|mixed
* @throws \CodeException
......@@ -360,8 +360,6 @@ class Database {
$sqlLogMode = $this->isSqlModify($sql) ? SQL_LOG_MODE_MODIFY : SQL_LOG_MODE_ALL;
$errno = 0;
$result = 0;
$stat = array();
$errorMsg[ERROR_MESSAGE_TO_USER] = empty($specificMessage) ? 'SQL error' : $specificMessage;
......@@ -370,53 +368,44 @@ class Database {
$this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM);
}
// if ($specificMessage !== '') {
// $specificMessage = ' - ' . $specificMessage;
// }
// Logfile
$this->dbLog($sqlLogMode, $sql, $parameterArray);
if (false === ($this->mysqli_stmt = $this->mysqli->prepare($sql))) {
if ($skipErrno === array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$errno = $this->mysqli->errno;
if ($skipErrno === array() && false === array_search($errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error;
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $errno . ' ] ' . $this->mysqli->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_PREPARE);
} else {
$errno = $this->mysqli->errno;
}
}
if (count($parameterArray) > 0) {
if (false === $this->prepareBindParam($parameterArray)) {
if ($skipErrno !== array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$errno = $this->mysqli_stmt->errno;
if ($skipErrno === array() && false === array_search($errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $errno . ' ] ' . $this->mysqli_stmt->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_BIND);
} else {
$errno = $this->mysqli->errno;
}
}
}
if (false === $this->mysqli_stmt->execute()) {
if ($skipErrno !== array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$errno = $this->mysqli->errno;
if ($skipErrno === array() || false === array_search($errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_EXECUTE);
} else {
$errno = $this->mysqli->errno;
}
}
$msg = '';
$count = 0;
if ($errno === 0) {
$command = strtoupper(explode(' ', $sql, 2)[0]);
} else {
......
......@@ -1939,6 +1939,55 @@ class QuickFormQuery {
}
}
/**
* Process given tt-content record triggered by AJAX Call
*
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
public function dataReport() {
$uid = Store::getVar(NAME_UID, STORE_SIP . STORE_CLIENT . STORE_ZERO, SANITIZE_ALLOW_DIGIT);
return $this->getEvaluatedBodyText($uid);
}
/**
* @param $uid
*
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \DownloadException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \UserFormException
* @throws \UserReportException
*/
private function getEvaluatedBodyText($uid) {
$dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
$tt_content = $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_EXPECT_1, [$uid]);
$qfq = new QuickFormQuery([T3DATA_BODYTEXT => $tt_content[T3DATA_BODYTEXT]], false, false);
return $qfq->process();
}
/**
* Delete a record (tablename and recordid are given) or process a 'delete form'
*
......@@ -1956,7 +2005,6 @@ class QuickFormQuery {
* @throws \UserReportException
*/
public function delete() {
return $this->doForm(FORM_DELETE);
}
......
......@@ -461,7 +461,7 @@ class Download {
* @throws \UserFormException
* @throws \UserReportException
*/
private function getEvaluatedBodyText($uid, $urlParam) {
private function getEvaluatedBodyText($uid, array $urlParam) {
foreach ($urlParam as $key => $paramValue) {
$this->store->setVar($key, $paramValue, STORE_SIP);
}
......
......@@ -23,12 +23,11 @@
namespace IMATHUZH\Qfq\Core\Report;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
......@@ -77,7 +76,7 @@ use IMATHUZH\Qfq\Core\Store\Store;
* U:URL Param
* v:
* V:
* w:
* w:websocket
* W:Dimension
* x:Delete
* X:
......@@ -519,13 +518,66 @@ class Link {
return $this->renderLink(KeyValueStringParser::unparse($paramArr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER));
}
/**
* @param $str
* @return string
* @throws \UserFormException
* @throws \UserReportException
*/
public function processWebSocket($str) {
$websocket = new WebSocket();
$answer = '';
// str="w:wss://antmedia.math.uzh.ch:6334/test|t:<payload>|timeout:..."
$param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
if (empty($param[TOKEN_WEBSOCKET]) || empty($param[TOKEN_TEXT])) {
throw new \UserReportException("Missing Websocket target or text to send", ERROR_MISSING_VALUE);
}
$urlParts = parse_url($param[TOKEN_WEBSOCKET]);
$urlParts = array_merge(['scheme' => 'ws', 'host' => '', 'port' => 80, 'path' => ''], $urlParts);
if (empty($urlParts['host'])) {
throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Target URL incomplete',
ERROR_MESSAGE_TO_DEVELOPER =>
'host: ' . $urlParts['host'] . ', ' .
'port: ' . $urlParts['port'] . ', ' .
'path: ' . $urlParts['path']])
, ERROR_MISSING_VALUE);
}
// Check for wss >> ssl
if ($urlParts['scheme'] == 'wss') {
$urlParts['host'] = 'ssl://' . $urlParts['host'];
if ($urlParts['port'] == 0) {
$urlParts['port'] = 443;
}
}
// Open Socket
$errorMsg = '';
if (false === $websocket->connect($urlParts['host'], $urlParts['port'], $urlParts['path'], '', $errorMsg)) {
throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Failed connect websocket: ' . $errorMsg,
ERROR_MESSAGE_TO_DEVELOPER =>
'host: ' . $urlParts['host'] . ', ' .
'port: ' . $urlParts['port'] . ', ' .
'path: ' . $urlParts['path']])
, ERROR_MISSING_VALUE);
}
$answer = $websocket->sendData($param[TOKEN_TEXT]);
return $answer;
}
/**
* Build the whole link.
*
* @param string $str Qualifier with params. 'report'-syntax. F.e.: u:www.example.com|P:home.gif|t:Home"
*
* @return string The complete HTML encoded Link like
* <a href='http://example.com' class='external'><img src='iconf.gif' title='help text'>Description</a>
* <a href='http://example.com' class='external'><img src='icon.gif' title='help text'>Description</a>
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
......@@ -539,7 +591,18 @@ class Link {
return '';
}
// Check for dropdown menu
switch ($str[0] ?? '') {
case TOKEN_DROPDOWN:
// Check for dropdown menu
return $this->processDropdown($str);
break;
case TOKEN_WEBSOCKET:
return $this->processWebSocket($str);
break;
default:
break;
}
if (($str[0] ?? '') == TOKEN_DROPDOWN) {
return $this->processDropdown($str);
}
......@@ -673,7 +736,6 @@ class Link {
$flagArray = array();
// str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no"
// $param = explode(PARAM_DELIMITER, $str);
$param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);
$param = $this->paramPriority($param);
......@@ -807,7 +869,6 @@ class Link {
NAME_DELETE => '',
NAME_MONITOR => '0',
NAME_COPY_TO_CLIPBOARD => '',
NAME_LINK_CLASS => '', // class name
NAME_LINK_CLASS_DEFAULT => '', // Depending of 'as page' or 'as url'. Only used if class is not explicit set.
......
......@@ -675,7 +675,7 @@ class Report {
/**
* Called with an array of column names.
* Each column name can be split in multiple string by '|': [s1[|s2[|s3]]]
* Each s1|s2|s3 can be: {title}, _{special colum name}, _hide, _noWrap, _={title}, _+{tag}, _<{tag1}><{tag2}>
* Each s1|s2|s3 can be: {title}, _{special column name}, _hide, _noWrap, _={title}, _+{tag}, _<{tag1}><{tag2}>
*
* Return an Array: newKeys[idx][C_FULL|C_TITLE|C_NO_WRAP|C_HIDE]
*
......@@ -906,7 +906,7 @@ class Report {
* @param string $columnValue
* @param string $full_level
* @param string $rowIndex
* @param $flagOutput
* @param bool $flagOutput
*
* @return string rendered column
* @throws \CodeException
......@@ -938,6 +938,7 @@ class Report {
switch ($columnName) {
case COLUMN_LINK:
case COLUMN_WEBSOCKET:
$content .= $this->link->renderLink($columnValue);
break;
......
This diff is collapsed.
......@@ -50,6 +50,7 @@ class AutoCron {
/**
* AutoCron constructor.
*
* @param bool $verbose
* @param bool $phpUnit
* @throws \CodeException
......@@ -68,6 +69,7 @@ class AutoCron {
// set_error_handler("\\IMATHUZH\\Qfq\\Core\\Exception\\ErrorHandler::exception_error_handler");
$this->store = Store::getInstance();
$this->store->FillStoreSystemBySql(); // Do this after the DB-update
// Set Log Mode for AutoCron updates
$sqlLogMode = $this->store->getVar(SYSTEM_SQL_LOG_MODE_AUTOCRON, STORE_SYSTEM);
......@@ -81,7 +83,7 @@ class AutoCron {
}
/**
* Check if there are started cronJobs, older than $ageMaxMinutes
* Check if there are started cronJobs, older than $ageMaxMinutes.
*
* @param int $ageMaxMinutes
*
......@@ -102,6 +104,8 @@ class AutoCron {
}
/**
* Check if $job has expired. If yes, calculate next run and set $job[AUTOCRON_NEXT_RUN].
*
* @param array $job
*
* @return array
......@@ -199,6 +203,8 @@ class AutoCron {
}
/**
* Fill all necessary elements.
*