Newer
Older
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 1/12/16
* Time: 4:36 PM
*/
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Report\Report;
use IMATHUZH\Qfq\Core\Report\Tablesorter;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;

Carsten Rose
committed
const EVALUATE_DB_INDEX_DEFAULT = 0;
/**
* @var Store
*/

Carsten Rose
committed
* @var Database[] - Array of Database instantiated class

Carsten Rose
committed
private $dbArray = array();
/**
* @var Link
*/
private $link = null;
/**
* @var Tablesorter
*/
private $tablesorter = null;

Carsten Rose
committed
private $dbIndex = EVALUATE_DB_INDEX_DEFAULT;
private $startDelimiter = '';
private $startDelimiterLength = 0;
private $endDelimiter = '';
private $endDelimiterLength = 0;
private $sqlKeywords = array('SELECT ', 'INSERT ', 'DELETE ', 'UPDATE ', 'SHOW ', 'REPLACE ', 'TRUNCATE ', 'DESCRIBE ', 'EXPLAIN ', 'SET ', 'WITH ');
private $escapeTypeDefault = '';
private $report = null;
// private $debugStack = array();
* @param Database $db
* @param string $startDelimiter
* @param string $endDelimiter
* @throws \CodeException
* @throws \UserFormException
public function __construct(Store $store, Database $db, $startDelimiter = '{{', $endDelimiter = '}}') {
$this->store = $store;

Carsten Rose
committed
$this->dbArray[EVALUATE_DB_INDEX_DEFAULT] = $db;
$this->startDelimiter = $startDelimiter;
$this->startDelimiterLength = strlen($startDelimiter);
$this->endDelimiter = $endDelimiter;
$this->endDelimiterLength = strlen($endDelimiter);

Carsten Rose
committed
$this->escapeTypeDefault = $this->store::getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
if (empty($this->escapeTypeDefault) || $this->escapeTypeDefault == TOKEN_ESCAPE_CONFIG) {

Carsten Rose
committed
$this->escapeTypeDefault = $this->store::getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
* Evaluate a whole array or an array of arrays.

Carsten Rose
committed
* @param array $skip Optional Array with keynames, which will not be evaluated.
* @param array $debugStack

Carsten Rose
committed
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException

Carsten Rose
committed
public function parseArray($tokenArray, array $skip = array(), &$debugStack = array()) {
$arr = array();
// In case there is an Element 'fillStoreVar', process that first (if not defined to skip).
$flagSkipFillStoreVar = (array_search(FE_FILL_STORE_VAR, $skip) !== false);
if (!$flagSkipFillStoreVar && !empty($tokenArray[FE_FILL_STORE_VAR]) && is_string($tokenArray[FE_FILL_STORE_VAR])) {

Carsten Rose
committed
$arr = $this->parse($tokenArray[FE_FILL_STORE_VAR], ROW_REGULAR, 0, $debugStack);
if (!empty($arr)) {
$this->store::appendToStore($arr[0], STORE_VAR);

Carsten Rose
committed
}
unset($tokenArray[FE_FILL_STORE_VAR]);
}
foreach ($tokenArray as $key => $value) {

Carsten Rose
committed
if (array_search($key, $skip) !== false) {

Carsten Rose
committed
continue;
}
if (is_array($value)) {

Carsten Rose
committed
$arr[] = $this->parseArray($value, $skip);
} else {

Carsten Rose
committed
$value = Support::handleEscapeSpaceComment($value);

Carsten Rose
committed

Carsten Rose
committed
$arr[$key] = $this->parse($value, ROW_IMPLODE_ALL, 0, $debugStack);
}
return $arr;
}
/**
* Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them. All queries will be
* fired. In case of an 'INSERT' statement, return the last_insert_id().
*
* Token to replace have to be enclosed by '{{' and '}}'

Carsten Rose
committed
* @param string $line

Carsten Rose
committed
* @param string $sqlMode ROW_IMPLODE | ROW_REGULAR | ... - might be overwritten in $line by '{{!...'
* @param int $recursion
* @param array $debugStack
* @param string $foundInStore
*
* @return array|mixed|null|string - in case of INSERT: last_insert_id()
*
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
public function parse($line, $sqlMode = ROW_IMPLODE_ALL, $recursion = 0, &$debugStack = array(), &$foundInStore = '', $frCmd = '') {

Carsten Rose
committed
if ($line === '') {
return '';
}
json_encode([ERROR_MESSAGE_TO_USER => 'Recursion too deep', ERROR_MESSAGE_TO_DEVELOPER => "Level: $recursion, Line: $line"]),
ERROR_RECURSION_TOO_DEEP);
enured
committed
$recursion = ($recursion === null) ? 0 : $recursion;
$debugIndent = str_repeat(' ', $recursion);

Carsten Rose
committed
$debugLocal[] = $debugIndent . "Parse: $result";
$posFirstClose = strpos($result, $this->endDelimiter);

Carsten Rose
committed
$posLastClose = strrpos($result, $this->endDelimiter);

Carsten Rose
committed
// Variables like 'fillStoreVar' might contain SQL statements. Put them in store in case a DB exception is thrown.

Carsten Rose
committed
$this->store::setVar(SYSTEM_SQL_RAW, $line, STORE_SYSTEM);

Carsten Rose
committed
while ($posFirstClose !== false) {
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter);
json_encode([ERROR_MESSAGE_TO_USER => 'Missing open delimiter', ERROR_MESSAGE_TO_DEVELOPER => "Text: $result"]),
ERROR_MISSING_OPEN_DELIMITER);
$pre = substr($result, 0, $posMatchOpen);
$post = substr($result, $posFirstClose + $this->endDelimiterLength);
$match = substr($result, $posMatchOpen + $this->startDelimiterLength, $posFirstClose - $posMatchOpen - $this->startDelimiterLength);

Carsten Rose
committed
$tmpSqlMode = ($posFirstClose == $posLastClose) ? $sqlMode : ROW_IMPLODE_ALL;
$evaluated = $this->substitute($match, $foundInStore, $tmpSqlMode, $frCmd);

Carsten Rose
committed

Carsten Rose
committed
// newline
$debugLocal[] = '';

Carsten Rose
committed
$debugLocal[] = $debugIndent . "Replace: $match";

Carsten Rose
committed
if ($foundInStore === '') {
// Encode the non replaceable part as preparation not to process again. Recode them at the end.

Carsten Rose
committed
$evaluated = Support::encryptDoubleCurlyBraces($this->startDelimiter . $match . $this->endDelimiter);

Carsten Rose
committed
$debugLocal[] = $debugIndent . "BY: <nothing found - not replaced>";

Carsten Rose
committed
} else {
$flagTokenReplaced = true;

Carsten Rose
committed
// If an array is returned, break everything and return this assoc array.
if (is_array($evaluated)) {
$result = $evaluated;

Carsten Rose
committed
$debugLocal[] = $debugIndent . "BY: array(" . count($result) . ")";

Carsten Rose
committed
break;
}

Carsten Rose
committed
$debugLocal[] = $debugIndent . "BY: $evaluated";

Carsten Rose
committed
// More to substitute in the new evaluated result? Start recursion just with the new result..
if ($foundInStore === TOKEN_FOUND_STOP_REPLACE) {
$evaluated = Support::encryptDoubleCurlyBraces($evaluated);
} else {
if (strpos($evaluated, $this->endDelimiter) !== false) {
$evaluated = $this->parse($evaluated, ROW_IMPLODE_ALL, $recursion + 1, $debugLocal, $foundInStore, $frCmd);

Carsten Rose
committed
}
}
$result = $pre . $evaluated . $post;

Carsten Rose
committed
$posFirstClose = strpos($result, $this->endDelimiter);
}

Carsten Rose
committed
$result = Support::decryptDoubleCurlyBraces($result);
if ($flagTokenReplaced === true) {
if (is_array($result)) {
$str = "array(" . count($result) . ")";
} else {

Carsten Rose
committed
$str = "$result";
}
$debugLocal[] = $debugIndent . "FINAL: " . $str;

Carsten Rose
committed

Carsten Rose
committed
return $result;
/**
* @param $arrToken
* @param $dbIndex
* @param $foundInStore
* @return string
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException

Carsten Rose
committed
private function inlineLink($arrToken, $dbIndex, &$foundInStore): string {
$token = OnString::trimQuote(trim(implode(' ', $arrToken)));
if ($this->link === null) {

Carsten Rose
committed
$this->link = new Link($this->store::getSipInstance(), $dbIndex);
}
$foundInStore = TOKEN_FOUND_AS_COLUMN;
return $this->link->renderLink($token);
}
/**
* @param $arrToken
* @param $dbIndex
* @param $foundInStore
* @return string
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException

Carsten Rose
committed
private function inlineDataDndApi($arrToken, $dbIndex, &$foundInStore): string {
$token = OnString::trimQuote(trim(implode(' ', $arrToken)));
# Include current SIP store, to fetch SIP parameter later.
$token .= '&' . DND_FORM_SIP_VALUES . '=' . $this->store::getVar(SIP_SIP, STORE_SIP);
if (empty($token)) {
throw new \UserReportException('Missing form name for "data-dnd-api"', ERROR_MISSING_FORM);
}
if ($this->link === null) {

Carsten Rose
committed
$this->link = new Link($this->store::getSipInstance(), $dbIndex);
}
$foundInStore = TOKEN_FOUND_AS_COLUMN;

Carsten Rose
committed
$s = $this->link->renderLink('U:' . $token . '|s|r:8');
// Flag to add DND JS code later on.

Carsten Rose
committed
$this->store::setVar(SYSTEM_DRAG_AND_DROP_JS, 'true', STORE_SYSTEM);
// data-dnd-api="typo3conf/ext/qfq/qfq/Api/dragAndDrop.php?s={{'U:form=<form name>[¶mX=<any value>]|s|r:8' AS _link}}"
return DND_DATA_DND_API . '="' . Path::urlApi(API_DRAG_AND_DROP_PHP) . '?s=' . $s . '"';
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/** Execute qfqFunction and output value. Content is accessible in record store
*
* @param $arrToken
* @param $foundInStore
* @return string
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
* @throws \DbException
*/
private function inlineFunction($arrToken, &$foundInStore): string {
$output = '';
$token = OnString::trimQuote(trim(implode(' ', $arrToken)));
if ($this->report === null) {
$this->report = new Report(array(), $this);
}
$this->report->doQfqFunction($token);
$foundInStore = TOKEN_FOUND_AS_COLUMN;
// Check for => in qfqFunction. If not given then output content.
if (!strpos($token, '=>')) {
$output = $this->store::getVar(COLUMN_FUNCTION_OUTPUT, STORE_RECORD);
}
return $output;
}

Carsten Rose
committed
* Token might be:
* a) a SQL statement to fire
* b) fetch from a store. Syntax: '\[db index\]form:[store]:[sanitize]:[escape]:[default]:[type violate message]', ''

Carsten Rose
committed
*
* The token have to be *without* Delimiter '{{' , '}}'

Carsten Rose
committed
* If neither a) or b) match, return the token itself.
* @param string $token

Carsten Rose
committed
* @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found, return ''.
* @param string $sqlMode - ROW_IMPLODE | ROW_REGULAR | ... - might be overwritten in $line by '{{!...'

Carsten Rose
committed
* @return array|null|string
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
public function substitute($token, &$foundInStore = '', $sqlMode = ROW_IMPLODE_ALL, $frCmd = '') {

Carsten Rose
committed
$dbIndex = $this->dbIndex;

Carsten Rose
committed
$rcFlagWipe = false;

Carsten Rose
committed
// Check if the $token starts with '[<int>]...' - yes: open the necessary database.
if (strlen($token) > 2 && $token[0] === '[') {

Carsten Rose
committed
if ($token[2] !== ']') {

Carsten Rose
committed
[ERROR_MESSAGE_TO_USER => "Missing token ']' on position 3",
ERROR_MESSAGE_TO_DEVELOPER => "In string '$token'"]), ERROR_TOKEN_MISSING);

Carsten Rose
committed
}
$dbIndex = $token[1];
$token = trim(substr($token, 3));
if (empty($this->dbArray[$dbIndex])) {
$this->dbArray[$dbIndex] = new Database($dbIndex);
}
}
if ($token === '') {
return '';
}
// Get SQL column / row separated

Carsten Rose
committed
$token = trim(substr($token, 1));
// In case there is a statement starting with '(' (multiple): remove them just to detect if the following is a SQL statement.
// E.g. original: "{{[1] ! ( SELECT name FROM person ORDER BY name UNION SELECT 'new'}}"
$tokenClean = OnString::removeLeadingBrace($token);
// Extract token: check if this is a 'variable', 'SQL Statement', 'link', 'data-dnd-api'
$arrToken = explode(' ', $tokenClean);

Carsten Rose
committed
// Variable Type 'SQL Statement'
if (in_array(strtoupper($arrToken[VAR_INDEX_VALUE] . ' '), $this->sqlKeywords)) {

Carsten Rose
committed
$foundInStore = TOKEN_FOUND_IN_STORE_QUERY;

Carsten Rose
committed
return $this->dbArray[$dbIndex]->sql($token, $sqlMode);
// Variable Type '... AS _link', '... as data-dnd-api', '... AS _tablesorter-view-saver'
$countToken = count($arrToken);
if ($countToken > 2 && strtolower($arrToken[$countToken - 2]) == 'as') {
$type = OnString::stripFirstCharIf('_', $arrToken[$countToken - 1]);
array_pop($arrToken); // remove 'link' | 'data-dnd-api' | 'tablesorter-view-saver'
array_pop($arrToken); // remove 'as'
switch (strtolower($type)) {
case COLUMN_LINK:
return ($this->inlineLink($arrToken, $dbIndex, $foundInStore));
break;
case DND_DATA_DND_API:
return ($this->inlineDataDndApi($arrToken, $dbIndex, $foundInStore));
break;
case TABLESORTER_VIEW_SAVER:
if ($this->tablesorter === null) {
// Read settings always from dbIndexQfq
$dbIndexQfq = $this->store::getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
if (empty($this->dbArray[$dbIndexQfq])) {
$this->dbArray[$dbIndexQfq] = new Database($dbIndexQfq);
}
$this->tablesorter = new Tablesorter($dbIndexQfq);
// create baseUrl attribute for tablesorter api
$baseUrlAttribute = DATA_TABLESORTER_BASE_URL . "='" . $this->store->getVar(SYSTEM_BASE_URL, STORE_SYSTEM) . "'";
return ($this->tablesorter->inlineTablesorterView($arrToken[VAR_INDEX_VALUE], $foundInStore, $frCmd)) . $baseUrlAttribute;
case COLUMN_FUNCTION:
return ($this->inlineFunction($arrToken, $foundInStore));
break;
}
}
// explode for: <key>:<store priority>:<sanitize class>:<escape>:<default>:<type violate message>
$arrToken = array_merge(KeyValueStringParser::explodeEscape(':', $token, 6), [null, null, null, null, null, null]);

Carsten Rose
committed
$escapeTypes = (empty($arrToken[VAR_INDEX_ESCAPE])) ? $this->escapeTypeDefault : $arrToken[VAR_INDEX_ESCAPE];
$typeMessageViolate = ($arrToken[VAR_INDEX_MESSAGE] === null || $arrToken[VAR_INDEX_MESSAGE] === '') ? SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS : $arrToken[VAR_INDEX_MESSAGE];
$value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANITIZE],
$foundInStore, $typeMessageViolate, $arrToken[VAR_INDEX_DEFAULT]);

Carsten Rose
committed
$value = OnString::escape($escapeTypes, $value, $rcFlagWipe);

Carsten Rose
committed
if (($foundInStore == '' || $value === '') && $arrToken[VAR_INDEX_DEFAULT] != '') {
$foundInStore = TOKEN_FOUND_AS_DEFAULT;
$value = str_replace('\\:', ':', $arrToken[VAR_INDEX_DEFAULT]);

Carsten Rose
committed

Carsten Rose
committed
if ($rcFlagWipe) {

Carsten Rose
committed
switch ($foundInStore) {
case STORE_SIP:

Carsten Rose
committed
$this->store::unsetVar($arrToken[VAR_INDEX_VALUE], STORE_SIP);

Carsten Rose
committed
$sip = new Sip();

Carsten Rose
committed
$sip->removeKeyFromSip($this->store::getVar(SIP_SIP, STORE_SIP), $arrToken[VAR_INDEX_VALUE]);

Carsten Rose
committed
break;
case STORE_EMPTY:
case STORE_ZERO:
break;
default:
throw new \UserReportException("Wipe not implemented for store $foundInStore", ERROR_WIPE_NOT_IMPLEMENTED_FOR_STORE);

Carsten Rose
committed
}
}

Carsten Rose
committed
return $value;

Carsten Rose
committed
// public function getDebug() {
// return '<pre>' . implode("\n", $this->debugStack) . '</pre>';
// }