Commit 9b1ef2ce authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'F11998CustomQFQ-Function' into 'develop'

F11998 custom qfq function

See merge request !321
parents 5744133f 92963fb7
Pipeline #5060 passed with stages
in 3 minutes and 39 seconds
......@@ -320,8 +320,8 @@ In :ref:`config-qfq-php` mutliple database credentials can be prepared. Mandator
`DB_1_USER`, `DB_1_SERVER`, `DB_1_PASSWORD`, `DB_1_NAME`. The number '1' indicates the `dbIndex`. Increment the number
to specify further database credential setups.
Typically the credentials for `DB_1` also have access to the T3 database.
By convention, it's necessary that `DB_1` is the one who also have access to the Typo3 database. E.q. `QFQ Function`_ or
download links (based on QFQ functions) needs access to the T3 database to directly fetch tt-content records.
Different QFQ versions, shared database
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
......
This diff is collapsed.
......@@ -22,7 +22,7 @@ Quick Form Query Extension
en
:Copyright:
2017-2020
2017-2021
:Authors:
Carsten Rose, Benjamin Baer
......
......@@ -490,9 +490,6 @@ const SYSTEM_DB_INIT = 'init';
const SYSTEM_DB_INDEX_DATA = "indexData";
const SYSTEM_DB_INDEX_QFQ = "indexQfq";
//const SYSTEM_DB_INDEX_DATA_DEPRECATED = "DB_INDEX_DATA";
//const SYSTEM_DB_INDEX_QFQ_DEPRECATED = "DB_INDEX_QFQ";
// Automatically filled by QFQ
const SYSTEM_DB_NAME_DATA = 'dbNameData';
const SYSTEM_DB_NAME_QFQ = 'dbNameQfq';
......@@ -1010,6 +1007,7 @@ const F_PARAMETER = 'parameter'; // valid for F_ and FE_
// Form columns: via parameter field
const F_DB_INDEX = 'dbIndex';
const DB_INDEX_DEFAULT = "1";
const DB_INDEX_T3 = DB_INDEX_DEFAULT;
const PARAM_DB_INDEX_DATA = '__dbIndexData'; // Submitted via SIP to make record locking DB aware.
const F_FORM_SUBMIT_LOG_MODE = 'formSubmitLogMode';
......@@ -1529,6 +1527,7 @@ const SENDMAIL_TOKEN_ATTACHMENT_PAGE = 'p';
// Report, BodyText
const TOKEN_SQL = 'sql';
const TOKEN_FUNCTION = 'function';
const TOKEN_TWIG = 'twig';
const TOKEN_HEAD = 'head';
const TOKEN_ALT_HEAD = 'althead';
......@@ -1553,7 +1552,7 @@ const TOKEN_DB_INDEX = F_DB_INDEX;
const TOKEN_CONTENT = 'content';
const TOKEN_REPORT_FILE = 'file';
const TOKEN_VALID_LIST = 'sql|twig|head|althead|altsql|tail|shead|stail|rbeg|rend|renr|rsep|fbeg|fend|fsep|fskipwrap|rbgd|debug|form|r|debugShowBodyText|dbIndex|sqlLog|sqlLogMode|content|render';
const TOKEN_VALID_LIST = 'sql|function|twig|head|althead|altsql|tail|shead|stail|rbeg|rend|renr|rsep|fbeg|fend|fsep|fskipwrap|rbgd|debug|form|r|debugShowBodyText|dbIndex|sqlLog|sqlLogMode|content|render';
const TOKEN_COLUMN_CTRL = '_';
......@@ -1704,6 +1703,7 @@ const NAME_URL = 'url';
const NAME_MAIL = 'mail';
const NAME_PAGE = 'page';
const NAME_UID = 'uid';
const NAME_SOURCE = 'source';
const NAME_TEXT = 'text';
const NAME_DROPDOWN = 'dropdown';
const NAME_DOWNLOAD = DOWNLOAD_EXPORT_FILENAME;
......@@ -1775,6 +1775,7 @@ const TOKEN_URL = 'u';
const TOKEN_MAIL = 'm';
const TOKEN_PAGE = 'p';
const TOKEN_UID = 'uid';
const TOKEN_SOURCE = 'source';
const TOKEN_DOWNLOAD = 'd';
const TOKEN_COPY_TO_CLIPBOARD = 'y';
const TOKEN_DROPDOWN = 'z';
......@@ -2019,6 +2020,8 @@ const SETTING_TABLESORTER_MODE = 'mode';
const SETTING_TABLESORTER_MODE_DELETE = 'delete';
const SETTING_TABLESORTER_CLEAR = 'Clear';
const COLUMN_FUNCTION_OUTPUT = '_output';
// Object: Type
const T_LABEL = 't_label';
const T_INPUT = 't_input';
......
......@@ -54,7 +54,8 @@ class Database {
*/
private $sqlLogModePrio = [SQL_LOG_MODE_NONE => 1, SQL_LOG_MODE_ERROR => 2, SQL_LOG_MODE_MODIFY => 3, SQL_LOG_MODE_ALL => 4];
private $dbName = '';
private $dbName = null;
private $dbIndex = null;
/**
* Returns current data base handle from Store[System][SYSTEM_DBH].
......@@ -72,6 +73,7 @@ class Database {
if (empty($dbIndex)) {
$dbIndex = DB_INDEX_DEFAULT;
}
$this->dbIndex = $dbIndex;
$this->store = Store::getInstance();
$storeSystem = $this->store->getStore(STORE_SYSTEM);
......@@ -98,6 +100,13 @@ class Database {
}
}
/**
* @return mixed|string
*/
public function getDbIndex() {
return $this->dbIndex;
}
/**
* @return mixed|string
*/
......@@ -1076,8 +1085,7 @@ class Database {
* @throws \DbException
* @throws \UserFormException
*/
public function selectFormByName(string $formName, array $columnsToSelect = null)
{
public function selectFormByName(string $formName, array $columnsToSelect = null) {
// make sure to select column 'name'
if (is_array($columnsToSelect) && !in_array(F_NAME, $columnsToSelect)) {
$columnsToSelect[] = F_NAME;
......@@ -1102,4 +1110,25 @@ class Database {
}
}
/**
* Load tt-content record with subheader=$uid or uid=$uid (depending if $uid is numeric).
* It's important that the current DB class has access to the Typo3 DB.
*
* @param $uid
* @return array // The full T3 tt-content record.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public function getBodyText($uid) {
$column = (is_numeric($uid)) ? 'uid' : 'subheader';
$dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$sql = "SELECT * FROM `$dbT3`.`tt_content` WHERE `$column` = ?";
$arr = $this->sql($sql, ROW_EXPECT_1, [$uid], 'Function "' . $column . '=' . $uid . '" not found or more than one found.');
return ($arr);
}
}
\ No newline at end of file
......@@ -216,16 +216,16 @@ class KeyValueStringParser {
*
* E.g.: "a,b,'c,d',e" with delimiter ',' will result in [ 'a', 'b', 'c,d', 'e' ]
*
* @param $delimeter
* @param $delimiter
* @param $str
* @param int $limit
*
* @return array|bool
* @throws \CodeException
*/
public static function explodeWrapped($delimeter, $str, $limit = PHP_INT_MAX) {
public static function explodeWrapped($delimiter, $str, $limit = PHP_INT_MAX) {
if ($delimeter == '') {
if ($delimiter == '') {
return false;
}
......@@ -242,7 +242,7 @@ class KeyValueStringParser {
$onHold = '';
$cnt = 0;
$arr = explode($delimeter, $str, PHP_INT_MAX);
$arr = explode($delimiter, $str, PHP_INT_MAX);
foreach ($arr as $value) {
$trimmed = trim($value);
if ($value == '' && $startToken == '') {
......@@ -269,19 +269,19 @@ class KeyValueStringParser {
}
if ($cnt >= $limit) {
$final[$cnt - 1] .= $delimeter . $value;
$final[$cnt - 1] .= $delimiter . $value;
} else {
$final[] = $value;
$cnt++;
}
continue;
} else {
$onHold .= $delimeter . $value;
$onHold .= $delimiter . $value;
$lastChar = substr($trimmed, -1);
if ($startToken == $lastChar) {
if ($cnt >= $limit) {
$final[$cnt - 1] .= $delimeter . $onHold;
$final[$cnt - 1] .= $delimiter . $onHold;
} else {
$final[] = $onHold;
$cnt++;
......
......@@ -130,7 +130,7 @@ class OnArray {
/**
* Converts a onedimensional array by using htmlentities on all elements
* Converts a one-dimensional array by using html entities on all elements.
*
* @param array $arr
*
......
......@@ -337,4 +337,40 @@ class OnString {
public static function strContains(string $haystack, string $needle): bool {
return strpos($haystack, $needle) !== false;
}
/**
* Split a cmd "getFeUser(pId, pName, ...) : accountId, feUserUid, ..." into:
* rcFunctionName = getFeUser
* rcFunctionParam = [ 'pId', 'pName', ... ]
* rcReturnParam = [ 'accountId', 'feUserUid', ... ]
*
* @param $cmd
* @param $rcFunctionName
* @param $rcFunctionParam
* @param $rcReturnParam
*/
public static function splitFunctionCmd($cmd, &$rcFunctionName, &$rcFunctionParam, &$rcReturnParam) {
$rcFunctionName = '';
$rcFunctionParam = array();
$rcReturnParam = array();
if ($cmd == '') {
return;
}
// $cmd = "getFeUser(pId, pName) : accountId, feUserUid"
$split = explode('=>', $cmd, 2);
// $split[0] = getFeUser(pId, pName), $split[1]=accountId, feUserUid
$functionArr = explode('(', $split[0], 2);
// $functionArr[0]='getFeUser', $functionArr[1]='pId, pName) '
$args = explode(')', $functionArr[1] ?? '', 2);
// $args[0]='pId, pName', $args[1]=' '
$rcFunctionName = trim($functionArr[0]);
$rcFunctionParam = OnArray::trimArray(explode(',', $args[0] ?? ''));
$rcReturnParam = OnArray::trimArray(explode(',', $split[1] ?? ''));
}
}
......@@ -353,6 +353,35 @@ class Download {
}
}
/**
* @param string $uid
* @param array $urlParam
* @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, array $urlParam) {
$bodyTextArr = $this->db->getBodytext($uid);
// Copy $urlParam to STORE_SIP
foreach ($urlParam as $key => $paramValue) {
$this->store->setVar($key, $paramValue, STORE_SIP);
}
$qfq = new QuickFormQuery($bodyTextArr, false, false);
return $qfq->process();
}
/**
* Interprets $element and fetches corresponding content, either as a file or the content in a variable.
*
......@@ -387,7 +416,7 @@ class Download {
$token = $arr[0];
$value = $arr[1];
if ($token === TOKEN_UID) { // extract uid
if ($token === TOKEN_UID || $token === TOKEN_SOURCE) { // extract uid
$uidParamsArr = explode('&', $value, 2);
$uid = $uidParamsArr[0];
$value = $uidParamsArr[1] ?? ''; // additional params
......@@ -445,36 +474,6 @@ class Download {
return $filename;
}
/**
* @param $uid
* @param array $urlParam
*
* @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, array $urlParam) {
foreach ($urlParam as $key => $paramValue) {
$this->store->setVar($key, $paramValue, STORE_SIP);
}
$dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
$tt_content = $this->db->sql($sql, ROW_EXPECT_1, [$uid]);
$qfq = new QuickFormQuery([T3DATA_BODYTEXT => $tt_content[T3DATA_BODYTEXT]], false, false);
return $qfq->process();
}
/**
* Creates a ZIP Files of all given $files
*
......@@ -515,6 +514,63 @@ class Download {
return $zipFile;
}
/**
* Check $param for 'source:.<function name>&arg1=val1&arg2=val2,....'.
* For each found, expand it by fire the given QFQ function with the arguments.
* Be aware, the result might contain again a 'source:..' definition ... do it recursively.
*
* Returns a string withut any 'source:' definition.
*
* @param string $param
* @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 checkAndExpandSource($param) {
if ($param == '') {
return '';
}
$final = '';
// $param = 'F:file.pdf|uid:123&pId=22|p:htmlcontent&appId=1|source:myFunction&accId=33'
$elements = explode(PARAM_DELIMITER, $param);
// $elements = [ 'F:file.pdf', 'uid:123&pId=22;, 'p:htmlcontent&appId=1', 'source:myFunction&accId=33' ]
foreach ($elements as $element) {
// E.g.: $element = 'source:myFunction&accId=33' >>
$arr = explode(PARAM_TOKEN_DELIMITER, $element, 2);
// Check for 'source:...' - $arr[0] = 'source'
if (0 === strcmp($arr[0], TOKEN_SOURCE)) {
// $arr[1] = 'myFunction&accId=33&grId=44'
$args = explode('&', $arr[1], 2);
$urlParam = KeyValueStringParser::parse($args[1] ?? '', '=', '&');
// Return a list of
$element = $this->checkAndExpandSource($this->getEvaluatedBodytext($args[0], $urlParam));
if ($element == '') {
continue;
}
}
$final .= '|' . $element;
}
return substr($final, 1);
}
/**
* $vars[DOWNLOAD_EXPORT_FILENAME] - Optional. '<new filename>'
* $vars[DOWNLOAD_MODE] - Optional. file | pdf | excel | thumbnail | monitor - default is a) 'file' in case of only one or b) 'pdf' in case of multiple sources.
......@@ -563,7 +619,8 @@ class Download {
$vars[SIP_DOWNLOAD_PARAMETER] = TOKEN_FILE . ':' . $pathFilenameThumbnail;
}
$elements = explode(PARAM_DELIMITER, $vars[SIP_DOWNLOAD_PARAMETER]);
// Check and expand 'source:...'
$elements = explode(PARAM_DELIMITER, $this->checkAndExpandSource($vars[SIP_DOWNLOAD_PARAMETER]));
// Get all files / content
$tmpData = array();
......
......@@ -147,6 +147,7 @@ class Link {
TOKEN_MAIL => NAME_MAIL,
TOKEN_PAGE => NAME_PAGE,
TOKEN_UID => NAME_UID,
TOKEN_SOURCE => NAME_SOURCE,
TOKEN_DROPDOWN => NAME_DROPDOWN,
TOKEN_DOWNLOAD => NAME_DOWNLOAD,
TOKEN_DOWNLOAD_MODE => NAME_DOWNLOAD_MODE,
......@@ -814,8 +815,8 @@ class Link {
// Store value
if ((isset($rcTokenGiven[TOKEN_DOWNLOAD]) || isset($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD])) &&
($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_UID ||
$key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED)) {
($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_UID
|| $key == TOKEN_SOURCE || $key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED)) {
$vars[NAME_COLLECT_ELEMENTS][] = $key . ':' . $value;
......
......@@ -23,6 +23,7 @@
namespace IMATHUZH\Qfq\Core\Report;
use IMATHUZH\Qfq\Core\BodytextParser;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Form\FormAsFile;
......@@ -60,6 +61,11 @@ class Report {
*/
private $store = null;
/**
* @var Evaluate
*/
private $evaluate = null;
/**
* @var string
*/
......@@ -117,19 +123,19 @@ class Report {
* Report constructor.
*
* @param array $t3data
* @param Evaluate $eval
* @param Evaluate $evaluate
* @param bool $phpUnit
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public function __construct(array $t3data, Evaluate $eval, $phpUnit = false) {
public function __construct(array $t3data, Evaluate $evaluate, $phpUnit = false) {
#TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
$this->phpUnit = $phpUnit;
Support::setIfNotSet($t3data, "uid", 0);
$t3data["uid"] = $t3data["uid"] ?? 0;
$this->sip = new Sip($phpUnit);
if ($phpUnit) {
......@@ -138,6 +144,7 @@ class Report {
$_SERVER['REQUEST_URI'] = 'localhost';
}
$this->evaluate = $evaluate;
$this->store = Store::getInstance();
$this->showDebugInfoFlag = (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM)));
......@@ -167,7 +174,7 @@ class Report {
}
$this->db = new Database($this->dbIndexData);
$this->variables = new Variables($eval, $t3data["uid"]);
$this->variables = new Variables($evaluate, $t3data["uid"]);
$this->link = new Link($this->sip, $this->dbIndexData, $phpUnit);
......@@ -279,7 +286,7 @@ class Report {
// frCmd = "sql"
$frCmd = $arrKey[count($arrKey) - 1];
// Check if token is known
if (strpos('|' . strtolower(TOKEN_VALID_LIST) . '|', '|' . $frCmd . '|') === false) {
throw new \UserReportException ("Unknown token: $frCmd in Line '$ttLine''", ERROR_UNKNOWN_TOKEN);
}
......@@ -307,7 +314,7 @@ class Report {
$index = $level . "." . $frCmd;
// throw exception if this level was already defined
if(!empty($this->frArray[$index])) {
if (!empty($this->frArray[$index])) {
throw new \UserReportException ("Double definition: $index is defined more than once.", ERROR_DOUBLE_DEFINITION);
}
// store complete line reformatted in frArray
......@@ -315,9 +322,7 @@ class Report {
// per sql command
//pro sql cmd wird der Indexarray abgefüllt. Dieser wird später verwendet um auf den $frArray zuzugreifen
//if(preg_match("/^sql/i", $frCmd) == 1){
// if ($frCmd === TOKEN_FORM || $frCmd === TOKEN_SQL) {
if ($frCmd === TOKEN_SQL) {
if ($frCmd === TOKEN_SQL || $frCmd === TOKEN_FUNCTION) {
// Remember max level
$this->levelCount = max(substr_count($level, '.') + 1, $this->levelCount);
// $indexArray[10][50][5]
......@@ -417,6 +422,85 @@ class Report {
return $sortArg;
}
/**
* Split $cmd into a) function name, b) call parameter and c) return values.
* Fire function: tt-content QFQ report
* Return 'return values' in STORE_RECORD and QFQ function output in {{_output:R}}.
* BTW: the QFQ function is cached and read only once. The evaluation is not cached.
*
* @param $cmd # 'getFirstName(pId) => firstName, myLink'
* @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 doQfqFunction($cmd) {
// QFQ function cache
static $functionCache = array();
// Explode cmd
OnString::splitFunctionCmd($cmd, $rcFunctionName, $rcFunctionParam, $rcReturnParam);
// Save STORE_RECORD, STORE_TYPO3
$storeRecord = $this->store->getStore(STORE_RECORD);
$storeTypo3 = $this->store->getStore(STORE_TYPO3);
// Fill STORE_RECORD with parameter
$tmp = array();
foreach ($rcFunctionParam as $key) {
$tmp[$key] = $storeRecord[$key] ?? '';
}
$this->store->setStore($tmp, STORE_RECORD, true);
// Get tt_content record bodytext
if (isset($functionCache[$rcFunctionName])) {
// Already cached: get it from cache
$bodytextArr = $functionCache[$rcFunctionName];
} else {
// Multi DB setup: check for the correct DB
if (DB_INDEX_T3 != $this->db->getDbIndex()) {
// Current DB is wrong: get DB with DB_INDEX_T3
$db = new Database(DB_INDEX_T3);
} else {
$db = $this->db;
}
$bodytextArr = $db->getBodyText($rcFunctionName);
$btp = new BodytextParser();
$bodytextArr[T3DATA_BODYTEXT] = $btp->process($bodytextArr[T3DATA_BODYTEXT]);
$functionCache[$rcFunctionName] = $bodytextArr;
}
// Fake uid/pid for meaningful error messages
$this->store->setVar(TYPO3_PAGE_ID, $bodytextArr[T3DATA_PID], STORE_TYPO3);
$this->store->setVar(TYPO3_TT_CONTENT_UID, $bodytextArr[T3DATA_UID], STORE_TYPO3);
// Fire bodytext. output is purged
$report = new Report(array(), $this->evaluate, $this->phpUnit);
$storeRecord[COLUMN_FUNCTION_OUTPUT] = $report->process($bodytextArr[T3DATA_BODYTEXT]);
unset($report);
// Copy defined 'return values' to STORE_RECORD
$tmp = $this->store->getStore(STORE_RECORD);
foreach ($rcReturnParam as $key) {
$storeRecord[$key] = $tmp[$key] ?? '';
$storeRecord['&' . $key] = $tmp['&' . $key] ?? '';
}
// Restore old state
$this->store->setStore($storeRecord, STORE_RECORD, true);
$this->store->setStore($storeTypo3, STORE_TYPO3, true);
}
/**
* Executes the queries recursive. This Method is called for each sublevel.
*
......@@ -495,6 +579,11 @@ class Report {
unset($this->variables->resultArray[$fullLevel . ".line."][LINE_TOTAL]);
unset($this->variables->resultArray[$fullLevel . ".line."][LINE_COUNT]);
// If defined, fire QFQ function
if ($this->frArray[$fullLevel . "." . TOKEN_FUNCTION] != '') {
$this->doQfqFunction($this->frArray[$fullLevel . "." . TOKEN_FUNCTION]);
}
$sql = $this->variables->doVariables($this->frArray[$fullLevel . "." . TOKEN_SQL]);
$this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM);
......@@ -504,9 +593,14 @@ class Report {