<?php /** * Created by PhpStorm. * User: crose * Date: 1/12/16 * Time: 4:36 PM */ namespace IMATHUZH\Qfq\Core; use IMATHUZH\Qfq\Core\Database\Database; use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt; use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser; use IMATHUZH\Qfq\Core\Helper\OnString; use IMATHUZH\Qfq\Core\Helper\Path; use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\Report\Link; use IMATHUZH\Qfq\Core\Report\Tablesorter; use IMATHUZH\Qfq\Core\Store\Sip; use IMATHUZH\Qfq\Core\Store\Store; const EVALUATE_DB_INDEX_DEFAULT = 0; /** * Class Evaluate * @package qfq */ class Evaluate { /** * @var Store */ private $store = null; /** * @var Database[] - Array of Database instantiated class */ private $dbArray = array(); /** * @var Link */ private $link = null; /** * @var Tablesorter */ private $tablesorter = null; 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 '); private $escapeTypeDefault = ''; // private $debugStack = array(); /** * @param Store $store * @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; $this->dbArray[EVALUATE_DB_INDEX_DEFAULT] = $db; $this->startDelimiter = $startDelimiter; $this->startDelimiterLength = strlen($startDelimiter); $this->endDelimiter = $endDelimiter; $this->endDelimiterLength = strlen($endDelimiter); $this->escapeTypeDefault = $this->store::getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM); if (empty($this->escapeTypeDefault) || $this->escapeTypeDefault == TOKEN_ESCAPE_CONFIG) { $this->escapeTypeDefault = $this->store::getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM); } } /** * Evaluate a whole array or an array of arrays. * * @param $tokenArray * @param array $skip Optional Array with keynames, which will not be evaluated. * @param array $debugStack * * @return array * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ 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])) { $arr = $this->parse($tokenArray[FE_FILL_STORE_VAR], ROW_REGULAR, 0, $debugStack); if (!empty($arr)) { $this->store::appendToStore($arr[0], STORE_VAR); } unset($tokenArray[FE_FILL_STORE_VAR]); } foreach ($tokenArray as $key => $value) { if (array_search($key, $skip) !== false) { $arr[$key] = $value; continue; } if (is_array($value)) { $arr[] = $this->parseArray($value, $skip); } else { $value = Support::handleEscapeSpaceComment($value); $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 '}}' * * @param string $line * @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 = '') { if ($line === '') { return ''; } $flagTokenReplaced = false; if ($recursion > 4) { throw new \UserFormException( json_encode([ERROR_MESSAGE_TO_USER => 'Recursion too deep', ERROR_MESSAGE_TO_DEVELOPER => "Level: $recursion, Line: $line"]), ERROR_RECURSION_TOO_DEEP); } $result = $line; $debugIndent = str_repeat(' ', $recursion); $debugLocal[] = $debugIndent . "Parse: $result"; $posFirstClose = strpos($result, $this->endDelimiter); $posLastClose = strrpos($result, $this->endDelimiter); // Variables like 'fillStoreVar' might contain SQL statements. Put them in store in case a DB exception is thrown. $this->store::setVar(SYSTEM_SQL_RAW, $line, STORE_SYSTEM); while ($posFirstClose !== false) { $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter); if ($posMatchOpen === false) { throw new \UserFormException( 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); $tmpSqlMode = ($posFirstClose == $posLastClose) ? $sqlMode : ROW_IMPLODE_ALL; $evaluated = $this->substitute($match, $foundInStore, $tmpSqlMode); // newline $debugLocal[] = ''; $debugLocal[] = $debugIndent . "Replace: $match"; if ($foundInStore === '') { // Encode the non replaceable part as preparation not to process again. Recode them at the end. $evaluated = Support::encryptDoubleCurlyBraces($this->startDelimiter . $match . $this->endDelimiter); $debugLocal[] = $debugIndent . "BY: <nothing found - not replaced>"; } else { $flagTokenReplaced = true; // If an array is returned, break everything and return this assoc array. if (is_array($evaluated)) { $result = $evaluated; $debugLocal[] = $debugIndent . "BY: array(" . count($result) . ")"; break; } $debugLocal[] = $debugIndent . "BY: $evaluated"; // 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); } } } $result = $pre . $evaluated . $post; $posFirstClose = strpos($result, $this->endDelimiter); } $result = Support::decryptDoubleCurlyBraces($result); if ($flagTokenReplaced === true) { if (is_array($result)) { $str = "array(" . count($result) . ")"; } else { $str = "$result"; } $debugLocal[] = $debugIndent . "FINAL: " . $str; $debugStack = $debugLocal; } return $result; } /** * @param $arrToken * @param $dbIndex * @param $foundInStore * @return string * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ private function inlineLink($arrToken, $dbIndex, &$foundInStore): string { $token = OnString::trimQuote(trim(implode(' ', $arrToken))); if ($this->link === null) { $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 */ 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) { $this->link = new Link($this->store::getSipInstance(), $dbIndex); } $foundInStore = TOKEN_FOUND_AS_COLUMN; $s = $this->link->renderLink('U:' . $token . '|s|r:8'); // Flag to add DND JS code later on. $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 . '"'; } /** * Tries to substitute $token. * 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]', '' * * The token have to be *without* Delimiter '{{' , '}}' * If neither a) or b) match, return the token itself. * * @param string $token * @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 '{{!...' * * @return array|null|string * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ public function substitute($token, &$foundInStore = '', $sqlMode = ROW_IMPLODE_ALL) { $token = trim($token); $dbIndex = $this->dbIndex; $rcFlagWipe = false; // Check if the $token starts with '[<int>]...' - yes: open the necessary database. if (strlen($token) > 2 && $token[0] === '[') { if ($token[2] !== ']') { throw new \UserFormException(json_encode( [ERROR_MESSAGE_TO_USER => "Missing token ']' on position 3", ERROR_MESSAGE_TO_DEVELOPER => "In string '$token'"]), ERROR_TOKEN_MISSING); } $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 if ($token[0] === '!') { $token = trim(substr($token, 1)); $sqlMode = ROW_REGULAR; } // 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); // Variable Type 'SQL Statement' if (in_array(strtoupper($arrToken[VAR_INDEX_VALUE] . ' '), $this->sqlKeywords)) { $foundInStore = TOKEN_FOUND_IN_STORE_QUERY; 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)) . $baseUrlAttribute; break; default: 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]); $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]; // search for value in stores $value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANITIZE], $foundInStore, $typeMessageViolate, $arrToken[VAR_INDEX_DEFAULT]); $value = OnString::escape($escapeTypes, $value, $rcFlagWipe); if (($foundInStore == '' || $value === '') && $arrToken[VAR_INDEX_DEFAULT] != '') { $foundInStore = TOKEN_FOUND_AS_DEFAULT; $value = str_replace('\\:', ':', $arrToken[VAR_INDEX_DEFAULT]); } if ($rcFlagWipe) { switch ($foundInStore) { case STORE_SIP: $this->store::unsetVar($arrToken[VAR_INDEX_VALUE], STORE_SIP); $sip = new Sip(); $sip->removeKeyFromSip($this->store::getVar(SIP_SIP, STORE_SIP), $arrToken[VAR_INDEX_VALUE]); break; case STORE_EMPTY: case STORE_ZERO: break; default: throw new \UserReportException("Wipe not implemented for store $foundInStore", ERROR_WIPE_NOT_IMPLEMENTED_FOR_STORE); } } return $value; } /** * @return string */ // public function getDebug() { // return '<pre>' . implode("\n", $this->debugStack) . '</pre>'; // } }