Index: class.ux_t3lib_sqlparser.php =================================================================== --- class.ux_t3lib_sqlparser.php (revision 25962) +++ class.ux_t3lib_sqlparser.php (working copy) @@ -45,6 +45,335 @@ */ class ux_t3lib_sqlparser extends t3lib_sqlparser { + // START: Methods originally belonging to t3lib_sqlparser but copied and fixed here as they were not used in Core + + /** + * Parsing the tablenames in the "FROM [$parseString] WHERE" part of a query into an array. + * The success of this parsing determines if that part of the query is supported by TYPO3. + * + * @param string list of tables, eg. "pages, tt_content" or "pages A, pages B". NOTICE: passed by reference! + * @param string Regular expressing to STOP parsing, eg. '^(WHERE)([[:space:]]*)' + * @return array If successful parsing, returns an array, otherwise an error string. + * @see compileFromTables() + */ + public function parseFromTables(&$parseString, $stopRegex = '') { + + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + + $stack = array(); // Contains the parsed content + $pnt = 0; // Pointer to positions in $stack + $loopExit = 0; // Recursivity brake. + + // $parseString is continously shortend by the process and we keep parsing it till it is zero: + while (strlen($parseString)) { + // Looking for the table: + if ($stack[$pnt]['table'] = $this->nextPart($parseString,'^([[:alnum:]_]+)(,|[[:space:]]+)')) { + // Looking for stop-keywords before fetching potential table alias: + if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { + $this->lastStopKeyWord = strtoupper(str_replace(array(' ',"\t","\r","\n"), '', $this->lastStopKeyWord)); + return $stack; + } + if (!preg_match('/^(LEFT|JOIN)[[:space:]]+/i',$parseString)) { + $stack[$pnt]['as_keyword'] = $this->nextPart($parseString,'^(AS[[:space:]]+)'); + $stack[$pnt]['as'] = $this->nextPart($parseString,'^([[:alnum:]_]+)[[:space:]]*'); + } + } else return $this->parseError('No table name found as expected in parseFromTables()!', $parseString); + + // Looking for JOIN + $joinCnt = 0; + while ($join = $this->nextPart($parseString,'^(LEFT[[:space:]]+JOIN|LEFT[[:space:]]+OUTER[[:space:]]+JOIN|INNER[[:space:]]+JOIN|JOIN)[[:space:]]+')) { + $stack[$pnt]['JOIN'][$joinCnt]['type'] = $join; + if ($stack[$pnt]['JOIN'][$joinCnt]['withTable'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+', 1)) { + if (!preg_match('/^ON[[:space:]]+/i', $parseString)) { + $stack[$pnt]['JOIN'][$joinCnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)'); + $stack[$pnt]['JOIN'][$joinCnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + } + if (!$this->nextPart($parseString, '^(ON[[:space:]]+)')) { + return $this->parseError('No join condition found in parseFromTables()!', $parseString); + } + $field1 = $this->nextPart($parseString,'^([[:alnum:]_.]+)[[:space:]]*=[[:space:]]*', 1); + $field2 = $this->nextPart($parseString,'^([[:alnum:]_.]+)[[:space:]]+'); + if ($field1 && $field2) { + + // Explode fields into field and table: + $tableField = explode('.', $field1, 2); + $field1 = array(); + if (count($tableField) != 2) { + $field1['table'] = ''; + $field1['field'] = $tableField[0]; + } else { + $field1['table'] = $tableField[0]; + $field1['field'] = $tableField[1]; + } + $tableField = explode('.', $field2, 2); + $field2 = array(); + if (count($tableField) != 2) { + $field2['table'] = ''; + $field2['field'] = $tableField[0]; + } else { + $field2['table'] = $tableField[0]; + $field2['field'] = $tableField[1]; + } + $stack[$pnt]['JOIN'][$joinCnt]['ON'] = array($field1, $field2); + $joinCnt++; + } else return $this->parseError('No join fields found in parseFromTables()!', $parseString); + } else return $this->parseError('No join table found in parseFromTables()!', $parseString); + } + + // Looking for stop-keywords: + if ($stopRegex && $this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex)) { + $this->lastStopKeyWord = strtoupper(str_replace(array(' ',"\t","\r","\n"), '', $this->lastStopKeyWord)); + return $stack; + } + + // Looking for comma: + if (strlen($parseString) && !$this->nextPart($parseString, '^(,)')) { + return $this->parseError('No comma found as expected in parseFromTables()', $parseString); + } + + // Increasing pointer: + $pnt++; + + // Check recursivity brake: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops, exiting prematurely in parseFromTables()...', $parseString); + } + } + + // Return result array: + return $stack; + } + + /** + * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array. + * The success of this parsing determines if that part of the query is supported by TYPO3. + * + * @param string WHERE clause to parse. NOTICE: passed by reference! + * @param string Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)' + * @return mixed If successful parsing, returns an array, otherwise an error string. + */ + public function parseWhereClause(&$parseString, $stopRegex = '') { + + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + + $stack = array(0 => array()); // Contains the parsed content + $pnt = array(0 => 0); // Pointer to positions in $stack + $level = 0; // Determines parenthesis level + $loopExit = 0; // Recursivity brake. + + // $parseString is continously shortend by the process and we keep parsing it till it is zero: + while (strlen($parseString)) { + + // Look for next parenthesis level: + $newLevel = $this->nextPart($parseString,'^([(])'); + if ($newLevel == '(') { // If new level is started, manage stack/pointers: + $level++; // Increase level + $pnt[$level] = 0; // Reset pointer for this level + $stack[$level] = array(); // Reset stack for this level + } else { // If no new level is started, just parse the current level: + + // Find "modifyer", eg. "NOT or !" + $stack[$level][$pnt[$level]]['modifier'] = trim($this->nextPart($parseString, '^(!|NOT[[:space:]]+)')); + + // Support calculated value only for: + // - "&" (boolean AND) + // - "+" (addition) + // - "-" (substraction) + // - "*" (multiplication) + // - "/" (division) + // - "%" (modulo) + $calcOperators = '&|\+|-|\*|\/|%'; + + // Fieldname: + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)([[:space:]]+|' . $calcOperators . '|<=|>=|<|>|=|!=|IS)')) { + + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) == 2) { + $stack[$level][$pnt[$level]]['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['table'] = ''; + $stack[$level][$pnt[$level]]['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); + } + + // See if the value is calculated: + $stack[$level][$pnt[$level]]['calc'] = $this->nextPart($parseString, '^(' . $calcOperators . ')'); + if (strlen($stack[$level][$pnt[$level]]['calc'])) { + // Finding value for calculation: + $stack[$level][$pnt[$level]]['calc_value'] = $this->getValue($parseString); + } + + // Find "comparator": + $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=|NOT[[:space:]]+IN|IN|NOT[[:space:]]+LIKE|LIKE|IS[[:space:]]+NOT|IS)'); + if (strlen($stack[$level][$pnt[$level]]['comparator'])) { + // Finding value for comparator: + $stack[$level][$pnt[$level]]['value'] = $this->getValue($parseString, $stack[$level][$pnt[$level]]['comparator']); + if ($this->parse_error) { + return $this->parse_error; + } + } + + // Finished, increase pointer: + $pnt[$level]++; + + // Checking if the current level is ended, in that case do stack management: + while ($this->nextPart($parseString,'^([)])')) { + $level--; // Decrease level: + $stack[$level][$pnt[$level]]['sub'] = $stack[$level+1]; // Copy stack + $pnt[$level]++; // Increase pointer of the new level + + // Make recursivity check: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops (in search for exit parenthesis), exiting prematurely in parseWhereClause()...', $parseString); + } + } + + // Detecting the operator for the next level: + $op = $this->nextPart($parseString, '^(AND[[:space:]]+NOT|&&[[:space:]]+NOT|OR[[:space:]]+NOT|OR[[:space:]]+NOT|\|\|[[:space:]]+NOT|AND|&&|OR|\|\|)(\(|[[:space:]]+)'); + if ($op) { + // Normalize boolean operator + $op = str_replace(array('&&', '||'), array('AND', 'OR'), $op); + $stack[$level][$pnt[$level]]['operator'] = $op; + } elseif (strlen($parseString)) { + + // Looking for stop-keywords: + if ($stopRegex && $this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex)) { + $this->lastStopKeyWord = strtoupper(str_replace(array(' ',"\t","\r","\n"), '', $this->lastStopKeyWord)); + return $stack[0]; + } else { + return $this->parseError('No operator, but parsing not finished in parseWhereClause().', $parseString); + } + } + } + + // Make recursivity check: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops, exiting prematurely in parseWhereClause()...', $parseString); + } + } + + // Return the stacks lowest level: + return $stack[0]; + } + + /** + * Compiles a "FROM [output] WHERE..:" table list based on input array (made with ->parseFromTables()) + * + * @param array Array of table names, (made with ->parseFromTables()) + * @return string Table name string + * @see parseFromTables() + */ + public function compileFromTables($tablesArray) { + + // Prepare buffer variable: + $outputParts = array(); + + // Traverse the table names: + if (is_array($tablesArray)) { + foreach ($tablesArray as $k => $v) { + + // Set table name: + $outputParts[$k] = $v['table']; + + // Add alias AS if there: + if ($v['as']) { + $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; + } + + if (is_array($v['JOIN'])) { + foreach ($v['JOIN'] as $join) { + $outputParts[$k] .= ' ' . $join['type'] . ' ' . $join['withTable']; + // Add alias AS if there: + if (isset($join['as']) && $join['as']) { + $outputParts[$k] .= ' ' . $join['as_keyword'] . ' ' . $join['as']; + } + $outputParts[$k] .= ' ON '; + $outputParts[$k] .= ($join['ON'][0]['table']) ? $join['ON'][0]['table'] . '.' : ''; + $outputParts[$k] .= $join['ON'][0]['field']; + $outputParts[$k] .= '='; + $outputParts[$k] .= ($join['ON'][1]['table']) ? $join['ON'][1]['table'] . '.' : ''; + $outputParts[$k] .= $join['ON'][1]['field']; + } + } + } + } + + // Return imploded buffer: + return implode(', ', $outputParts); + } + + /** + * Implodes an array of WHERE clause configuration into a WHERE clause. + * NOTICE: MIGHT BY A TEMPORARY FUNCTION. Use for debugging only! + * BUT IT IS NEEDED FOR DBAL - MAKE IT PERMANENT?!?! + * + * @param array WHERE clause configuration + * @return string WHERE clause as string. + * @see explodeWhereClause() + */ + protected function nativeCompileWhereClause($clauseArray) { + + // Prepare buffer variable: + $output = ''; + + // Traverse clause array: + if (is_array($clauseArray)) { + foreach ($clauseArray as $k => $v) { + + // Set operator: + $output .= $v['operator'] ? ' ' . $v['operator'] : ''; + + // Look for sublevel: + if (is_array($v['sub'])) { + $output .= ' (' . trim($this->nativeCompileWhereClause($v['sub'])) . ')'; + } else { + + // Set field/table with modifying prefix if any: + $output .= ' ' . trim($v['modifier'] . ' ' . ($v['table'] ? $v['table'] . '.' : '') . $v['field']); + + // Set calculation, if any: + if ($v['calc']) { + $output .= $v['calc'] . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; + } + + // Set comparator: + if ($v['comparator']) { + $output .= ' ' . $v['comparator']; + + // Detecting value type; list or plain: + if (t3lib_div::inList('NOTIN,IN', strtoupper(str_replace(array(' ', "\t", "\r", "\n"), '', $v['comparator'])))) { + $valueBuffer = array(); + foreach ($v['value'] as $realValue) { + $valueBuffer[] = $realValue[1] . $this->compileAddslashes($realValue[0]) . $realValue[1]; + } + $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; + } else { + $output .= ' ' . $v['value'][1] . $this->compileAddslashes($v['value'][0]) . $v['value'][1]; + } + } + } + } + } + + // Return output buffer: + return $output; + } + + // END: Methods originally belonging to t3lib_sqlparser but copied and fixed here as they were not used in Core + /************************* * * Compiling queries @@ -338,10 +667,10 @@ * @return string WHERE clause as string. * @see t3lib_sqlparser::parseWhereClause() */ - function compileWhereClause($clauseArray, $functionMapping = TRUE) { + public function compileWhereClause($clauseArray, $functionMapping = TRUE) { switch ((string)$GLOBALS['TYPO3_DB']->handlerCfg[$GLOBALS['TYPO3_DB']->lastHandlerKey]['type']) { case 'native': - $output = parent::compileWhereClause($clauseArray); + $output = $this->nativeCompileWhereClause($clauseArray); break; case 'adodb': // Prepare buffer variable: