Commit 45fc3b76 authored by Carsten  Rose's avatar Carsten Rose
Browse files

#1923: Report: SELECT "[\*]{7} [0-9]{5}" funktioniert nicht.

Parsing of the bodytext failed for single curly braces inside.
BodyTestParser.php: After cleaning the bodytext all nesting curly braces will be replaced by complex token. stripAndRemoveComment() renamed to trimAndRemoveCommentAndEmptyLine(). process() throws an exception in case of unmatched open curly braces. New encryptNestingDelimeter() / decryptNestingDelimeter(). Remove handling for escaped curly braces in unNest() - this should not be necessary anymore.
parent c1c9cf69
......@@ -181,6 +181,7 @@ Sanitize class
* **min|max**: only usable in forms. Compares the value against an lower and upper limit (numeric or string).
* **min|max date**: only usable in forms. Compares the value against an lower and upper date or datetime.
* **pattern**: only usable in forms. Compares the value against a regexp.
* **allbut**: all characters allowed, but not [ ] { } % & \ #. The used regexp: '^[^\[\]{}%&\\#]+$',
* **all**: no sanitizing
......@@ -994,6 +995,12 @@ Syntax
See :ref:`variables` for a full list of all available variables.
Be aware of the following restrictions to {{<level>.line.count}} or {{<level>.line.total}}:
line.count / line.total have to be known when the query is fired. E.g. `10.sql = SELECT {{10.line.count}}, ... WHERE {{10.line.count}} = ...`
won't work as expected. `{{10.line.count}}` can't be replaced before the query is fired, but will be replaced during processing the result!
Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW
Only SELECT and SHOW queries will fire subqueries.
......@@ -1080,7 +1087,7 @@ Example::
Nesting of levels
^^^^^^^^^^^^^^^^^
Levels can be nested by using curly brackets::
Levels can be nested by using curly brackets. Be carefull to write nothing than whitespaces/newline behind open or closing curly braces::
10.sql = SELECT 'hello world'
......
......@@ -8,19 +8,30 @@
namespace qfq;
const NESTING_TOKEN_OPEN = '#&nesting-open-&#';
const NESTING_TOKEN_CLOSE = '#&nesting-close&#';
const NESTING_TOKEN_LENGTH = 17;
class BodytextParser {
/**
* @param $bodytext
* @return mixed
*/
public function process($bodytext) {
$bodytext = $this->stripAndRemoveComment($bodytext);
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext);
// Encrypt double curly braces to prevent false positives with nesting: form = {{form}}\n
$bodytext = Support::encryptDoubleCurlyBraces($bodytext);
$bodytext = $this->encryptNestingDelimeter($bodytext);
$bodytext = $this->joinLine($bodytext);
$bodytext = $this->unNest($bodytext);
$bodytext = $this->stripAndRemoveComment($bodytext);
$bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext);
$bodytext = Support::decryptDoubleCurlyBraces($bodytext);
if (strpos($bodytext, NESTING_TOKEN_OPEN) !== false) {
throw new \qfq\UserFormException("Missing close delimiter: $bodytext", ERROR_MISSING_CLOSE_DELIMITER);
}
return $bodytext;
}
......@@ -31,7 +42,7 @@ class BodytextParser {
* @return string
*/
private function stripAndRemoveComment($bodytext) {
private function trimAndRemoveCommentAndEmptyLine($bodytext) {
$data = array();
$src = explode(PHP_EOL, $bodytext);
......@@ -46,7 +57,22 @@ class BodytextParser {
return implode(PHP_EOL, $data);
}
//PREG_SPLIT_DELIM_CAPTURE
/**
* Encrypt '{\n' and '}\n' by more complex token.
*
* @param $bodytext
* @return mixed
*/
private function encryptNestingDelimeter($bodytext) {
// Take care that a trailing '}' will be recognised: add '\n'
if (substr($bodytext, -1) === '}') {
$bodytext .= "\n";
}
$bodytext = str_replace("{\n", NESTING_TOKEN_OPEN, $bodytext);
$bodytext = str_replace("}\n", NESTING_TOKEN_CLOSE, $bodytext);
return $bodytext;
}
/**
* Join lines, which do not begin with '<level>.<keyword>[ ]='
......@@ -88,24 +114,28 @@ class BodytextParser {
return implode(PHP_EOL, $data);
}
//PREG_SPLIT_DELIM_CAPTURE
/**
* @param $bodytext
* @return mixed|string
* @throws UserFormException
*/
private function unNest($bodytext) {
// Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
$bodytext = str_replace('\{', '#&[_#', $bodytext);
$bodytext = str_replace('\}', '#&]_#', $bodytext);
$bodytext = Support::encryptDoubleCurlyBraces($bodytext);
// $bodytext = str_replace('\{', '#&[_#', $bodytext);
// $bodytext = str_replace('\}', '#&]_#', $bodytext);
// $bodytext = Support::encryptDoubleCurlyBraces($bodytext);
$result = $bodytext;
$posFirstClose = strpos($result, '}');
$posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
while ($posFirstClose !== false) {
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), '{');
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN);
if ($posMatchOpen === false) {
$result = $this->decryptNestingDelimeter($result);
throw new \qfq\UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
}
......@@ -113,12 +143,12 @@ class BodytextParser {
if ($pre === false)
$pre = '';
$post = substr($result, $posFirstClose + 1);
$post = substr($result, $posFirstClose + NESTING_TOKEN_LENGTH);
if ($post === false)
$post = '';
// trim also removes '\n'
$match = trim(substr($result, $posMatchOpen + 1, $posFirstClose - $posMatchOpen - 1));
$match = trim(substr($result, $posMatchOpen + NESTING_TOKEN_LENGTH, $posFirstClose - $posMatchOpen - NESTING_TOKEN_LENGTH));
// "10.sql = SELECT...\n20 {\n
$levelStartPos = strrpos(trim($pre), PHP_EOL);
......@@ -138,14 +168,27 @@ class BodytextParser {
}
$result = $pre . $post;
$posFirstClose = strpos($result, '}');
$posFirstClose = strpos($result, NESTING_TOKEN_CLOSE);
}
$result = str_replace('#&[_#', '{', $result);
$result = str_replace('#&]_#', '}', $result);
$result = Support::decryptDoubleCurlyBraces($result);
// $result = str_replace('#&[_#', '{', $result);
// $result = str_replace('#&]_#', '}', $result);
// $result = Support::decryptDoubleCurlyBraces($result);
return $result;
}
/**
* Decrypt complex token by '{\n' and '}\n'
*
* @param $bodytext
* @return mixed
*/
private function decryptNestingDelimeter($bodytext) {
$bodytext = str_replace(NESTING_TOKEN_OPEN, "{\n", $bodytext);
$bodytext = str_replace(NESTING_TOKEN_CLOSE, "}\n", $bodytext);
return $bodytext;
}
}
\ No newline at end of file
......@@ -101,7 +101,6 @@ const ERROR_USER_LOGGED_IN = 1018;
const ERROR_FORM_FORBIDDEN = 1019;
const ERROR_FORM_UNKNOWN_PERMISSION_MODE = 10120;
const ERROR_MULTI_SQL_MISSING = 1021;
const ERROR_MISSING_OPEN_DELIMITER = 1022;
const ERROR_RECURSION_TOO_DEEP = 1023;
const ERROR_CHECKBOXMODE_UNKNOWN = 1024;
const ERROR_MISSING_SQL1 = 1025;
......@@ -134,6 +133,8 @@ const ERROR_REQUIRED_VALUE_EMPTY = 1055;
const ERROR_DATE_UNEXPECTED_FORMAT = 1056;
const ERROR_NOT_APPLICABLE = 1057;
const ERROR_FORMELEMENT_TYPE = 1058;
const ERROR_MISSING_OPEN_DELIMITER = 1059;
const ERROR_MISSING_CLOSE_DELIMITER = 1060;
// Store
const ERROR_STORE_VALUE_ALREADY_CODPIED = 1100;
......
......@@ -66,13 +66,13 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals($expected, $result);
// Nested expression: complex
$given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 {\n sql=SELECT 'Hello World3'\n40 { sql = SELECT 'Hello World4'\n} \n}\n}";
$given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 { \n sql=SELECT 'Hello World3'\n40 { \n sql = SELECT 'Hello World4'\n } \n } \n } ";
$expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n20.30.sql=SELECT 'Hello World3'\n20.30.40.sql = SELECT 'Hello World4'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// form=...., {{ }}
$given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 { \nsql=SELECT 'Hello World'\n}\n form=Person\n";
$given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 {\nsql=SELECT 'Hello World'\n}\n form=Person\n";
$expected = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30.sql=SELECT 'Hello World'\nform=Person";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
......@@ -83,9 +83,15 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase {
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested: unclosed open bracket
$given = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'\n";
$expected = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'";
// Single open bracket inside a string.
$given = "10.sql = SELECT 'Hello { World'";
$expected = "10.sql = SELECT 'Hello { World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Complex test
$given = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20 {\n 10 {\n 5 {\n sql = SELECT 'hello world<br>'\n }\n }\n}\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT '20.10<br>'";
$expected = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20.10.5.sql = SELECT 'hello world<br>'\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT '20.10<br>'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
......@@ -95,12 +101,24 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase {
* @expectedException \qfq\UserFormException
*
*/
public function testProcessException() {
public function testProcessExceptionClose() {
$btp = new BodytextParser();
// Nested: unclosed open bracket
// Nested: unclosed close bracket
$btp->process("10.sql = SELECT 'Hello World'\n } \n30.sql = SELECT 'Hello World'\n");
}
/**
* @expectedException \qfq\UserFormException
*
*/
public function testProcessExceptionOpen() {
$btp = new BodytextParser();
// Nested: unclosed open bracket
$btp->process("10.sql = SELECT 'Hello World'\n20 { \n30.sql = SELECT 'Hello World'\n");
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment