Commit a11212df authored by Carsten  Rose's avatar Carsten Rose
Browse files

Bug #4274 / ItemList: escape ',' ':'

Manual.rst: Update Manual on how to use escape.
KeyValueStringParser.php: Detect if the exepensive preg_split is necessary. Implement explodeEscape().
parent 049a5b99
...@@ -2436,6 +2436,7 @@ Checkboxes can be rendered in mode: ...@@ -2436,6 +2436,7 @@ Checkboxes can be rendered in mode:
* ``itemList=red,blue,orange`` * ``itemList=red,blue,orange``
* ``itemList=1:red,2:blue,3:orange`` * ``itemList=1:red,2:blue,3:orange``
* If ':' or ',' are part of key or value, it needs to escaped by '\'. E.g.: `itemList=1:red\:,2:blue\,,3:orange``
* *FormElement.sql1* = ``{{!SELECT id, value FROM someTable}}`` * *FormElement.sql1* = ``{{!SELECT id, value FROM someTable}}``
* *FormElement.maxlength* - vertical or horizontal alignment: * *FormElement.maxlength* - vertical or horizontal alignment:
...@@ -2629,7 +2630,8 @@ Type: radio ...@@ -2629,7 +2630,8 @@ Type: radio
2. *FormElement.parameter*: 2. *FormElement.parameter*:
* *itemList* = `<attribute>` E.g.: *itemList=red,blue,orange* or *itemList=1:red,2:blue:3:orange* * *itemList* = `<attribute>` E.g.: *itemList=red,blue,orange* or *itemList=1:red,2:blue,3:orange*
* If ':' or ',' are part of key or value, it needs to escaped by '\'. E.g.: `itemList=1:red\:,2:blue\,,3:orange``
3. Definition of the *enum* or *set* field (only labels, ids are not possible). 3. Definition of the *enum* or *set* field (only labels, ids are not possible).
...@@ -2688,6 +2690,7 @@ Type: select ...@@ -2688,6 +2690,7 @@ Type: select
* *FormElement.parameter*: * *FormElement.parameter*:
* *itemList* = `<attribute>` - E.g.: *itemList=red,blue,orange* or *itemList=1:red,2:blue:3:orange* * *itemList* = `<attribute>` - E.g.: *itemList=red,blue,orange* or *itemList=1:red,2:blue:3:orange*
* If ':' or ',' are part of key or value, it needs to escaped by '\'. E.g.: `itemList=1:red\:,2:blue\,,3:orange``
* Definition of the *enum* or *set* field (only labels, ids are not possible). * Definition of the *enum* or *set* field (only labels, ids are not possible).
......
...@@ -98,7 +98,12 @@ class KeyValueStringParser { ...@@ -98,7 +98,12 @@ class KeyValueStringParser {
// Clean any "\r\n" to "\n" // Clean any "\r\n" to "\n"
$keyValueString = str_replace("\r\n", "\n", $keyValueString); $keyValueString = str_replace("\r\n", "\n", $keyValueString);
// Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
if (strpos($keyValueString, '\\' . $listDelimiter) !== false) {
$keyValuePairs = self::explodeEscape($listDelimiter, $keyValueString, 2);
} else {
$keyValuePairs = explode($listDelimiter, $keyValueString); $keyValuePairs = explode($listDelimiter, $keyValueString);
}
$returnValue = array(); $returnValue = array();
foreach ($keyValuePairs as $keyValuePairString) { foreach ($keyValuePairs as $keyValuePairString) {
...@@ -107,7 +112,12 @@ class KeyValueStringParser { ...@@ -107,7 +112,12 @@ class KeyValueStringParser {
continue; continue;
} }
// Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
if (strpos($keyValueString, '\\' . $keyValueDelimiter) !== false) {
$keyValueArray = self::explodeEscape($keyValueDelimiter, $keyValuePairString, 2);
} else {
$keyValueArray = explode($keyValueDelimiter, $keyValuePairString, 2); $keyValueArray = explode($keyValueDelimiter, $keyValuePairString, 2);
}
$key = trim($keyValueArray[0]); $key = trim($keyValueArray[0]);
...@@ -133,6 +143,33 @@ class KeyValueStringParser { ...@@ -133,6 +143,33 @@ class KeyValueStringParser {
return $returnValue; return $returnValue;
} }
/**
* Explode string by delimiter and respects escaped delimiter. The result is replaced with escaped delimiter.
* E.g. 'a,b\,c,d' becomes [ 'a', 'b,c', 'd']
*
* @param string $delimiter
* @param string $data
* @param int $max
* @return array
*/
public static function explodeEscape($delimiter, $data, $max = 0) {
// If the delimiter is a reserved regex char, it has to be escaped.
$delimiterEscaped = preg_quote($delimiter);
$items = preg_split('#(?<!\\\)' . $delimiterEscaped . '#', $data);
// In case there is an upper limit of array elements
if ($max > 0 && count($items) > $max) {
$remain = array_slice($items, 0, $max);
$remain[$max - 1] .= implode('', array_slice($items, $max));
$items = $remain;
}
// Escaped tokens: the escape character needs to be replaced.
return OnArray::arrayValueReplace($items, '\\' . $delimiter, $delimiter);
}
/** /**
* @param $string * @param $string
* @return string * @return string
...@@ -161,42 +198,42 @@ class KeyValueStringParser { ...@@ -161,42 +198,42 @@ class KeyValueStringParser {
* @return array|bool * @return array|bool
* @throws CodeException * @throws CodeException
*/ */
public static function explodeWrapped($delimeter, $str, $limit=PHP_INT_MAX ) { public static function explodeWrapped($delimeter, $str, $limit = PHP_INT_MAX) {
if($delimeter=='') { if ($delimeter == '') {
return false; return false;
} }
if($limit<0) { if ($limit < 0) {
throw new CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED); throw new CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED);
} }
if($limit==0) { if ($limit == 0) {
$limit = 1; $limit = 1;
} }
$final = array(); $final = array();
$startToken=''; $startToken = '';
$onHold = ''; $onHold = '';
$cnt = 0; $cnt = 0;
$arr = explode($delimeter, $str, PHP_INT_MAX); $arr = explode($delimeter, $str, PHP_INT_MAX);
foreach($arr as $value) { foreach ($arr as $value) {
$trimmed=trim($value); $trimmed = trim($value);
if($value=='' && $startToken=='') { if ($value == '' && $startToken == '') {
if($cnt<$limit) { if ($cnt < $limit) {
$final[] = ''; $final[] = '';
$cnt++; $cnt++;
} }
continue; continue;
} }
if($startToken=='') { if ($startToken == '') {
switch ($trimmed[0]) { switch ($trimmed[0]) {
case SINGLE_TICK: case SINGLE_TICK:
case DOUBLE_TICK: case DOUBLE_TICK:
if($trimmed[0] == substr($trimmed, -1)) { if ($trimmed[0] == substr($trimmed, -1)) {
break; // In case start and end token is in one exploded item break; // In case start and end token is in one exploded item
} }
$startToken = $trimmed[0]; $startToken = $trimmed[0];
...@@ -206,8 +243,8 @@ class KeyValueStringParser { ...@@ -206,8 +243,8 @@ class KeyValueStringParser {
break; break;
} }
if($cnt>=$limit) { if ($cnt >= $limit) {
$final[$cnt-1] .= $delimeter . $value; $final[$cnt - 1] .= $delimeter . $value;
} else { } else {
$final[] = $value; $final[] = $value;
$cnt++; $cnt++;
...@@ -215,11 +252,11 @@ class KeyValueStringParser { ...@@ -215,11 +252,11 @@ class KeyValueStringParser {
continue; continue;
} else { } else {
$onHold .= $delimeter . $value; $onHold .= $delimeter . $value;
$lastChar = substr($trimmed,-1); $lastChar = substr($trimmed, -1);
if($startToken == $lastChar) { if ($startToken == $lastChar) {
if($cnt>=$limit) { if ($cnt >= $limit) {
$final[$cnt-1] .= $delimeter . $onHold; $final[$cnt - 1] .= $delimeter . $onHold;
} else { } else {
$final[] = $onHold; $final[] = $onHold;
$cnt++; $cnt++;
......
...@@ -14,6 +14,7 @@ require_once(__DIR__ . '/../../qfq/helper/KeyValueStringParser.php'); ...@@ -14,6 +14,7 @@ require_once(__DIR__ . '/../../qfq/helper/KeyValueStringParser.php');
class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase { class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase {
public function testSingleKeyValuePair() { public function testSingleKeyValuePair() {
$actual = keyValueStringParser::parse("key:value"); $actual = keyValueStringParser::parse("key:value");
...@@ -181,73 +182,180 @@ class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase { ...@@ -181,73 +182,180 @@ class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals(['a,b,c'], $actual); $this->assertEquals(['a,b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c'); $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c');
$this->assertEquals(['a' , 'b' ,'c'], $actual); $this->assertEquals(['a', 'b', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', ' a,b,c'); $actual = keyValueStringParser::explodeWrapped(',', ' a,b,c');
$this->assertEquals([' a' , 'b' ,'c'], $actual); $this->assertEquals([' a', 'b', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a ,b,c'); $actual = keyValueStringParser::explodeWrapped(',', 'a ,b,c');
$this->assertEquals(['a ' , 'b' ,'c'], $actual); $this->assertEquals(['a ', 'b', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', ' a ,b,c'); $actual = keyValueStringParser::explodeWrapped(',', ' a ,b,c');
$this->assertEquals([' a ' , 'b' ,'c'], $actual); $this->assertEquals([' a ', 'b', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a, b,c'); $actual = keyValueStringParser::explodeWrapped(',', 'a, b,c');
$this->assertEquals(['a' , ' b' ,'c'], $actual); $this->assertEquals(['a', ' b', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b ,c'); $actual = keyValueStringParser::explodeWrapped(',', 'a,b ,c');
$this->assertEquals(['a' , 'b ' ,'c'], $actual); $this->assertEquals(['a', 'b ', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a, b ,c'); $actual = keyValueStringParser::explodeWrapped(',', 'a, b ,c');
$this->assertEquals(['a' , ' b ' ,'c'], $actual); $this->assertEquals(['a', ' b ', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b, c'); $actual = keyValueStringParser::explodeWrapped(',', 'a,b, c');
$this->assertEquals(['a' , 'b' ,' c'], $actual); $this->assertEquals(['a', 'b', ' c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c '); $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c ');
$this->assertEquals(['a' , 'b' ,'c '], $actual); $this->assertEquals(['a', 'b', 'c '], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b, c '); $actual = keyValueStringParser::explodeWrapped(',', 'a,b, c ');
$this->assertEquals(['a' , 'b' ,' c '], $actual); $this->assertEquals(['a', 'b', ' c '], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,"b",c'); $actual = keyValueStringParser::explodeWrapped(',', 'a,"b",c');
$this->assertEquals(['a' , '"b"','c'], $actual); $this->assertEquals(['a', '"b"', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,"b,b",c'); $actual = keyValueStringParser::explodeWrapped(',', 'a,"b,b",c');
$this->assertEquals(['a' , '"b,b"','c'], $actual); $this->assertEquals(['a', '"b,b"', 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b',c"); $actual = keyValueStringParser::explodeWrapped(',', "a,'b',c");
$this->assertEquals(['a' , "'b'",'c'], $actual); $this->assertEquals(['a', "'b'", 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c"); $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c");
$this->assertEquals(['a' , "'b,b'",'c'], $actual); $this->assertEquals(['a', "'b,b'", 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b,b',c"); $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b,b',c");
$this->assertEquals(['a' , "'b,b,b'",'c'], $actual); $this->assertEquals(['a', "'b,b,b'", 'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,a,a,a','b','c,c,c,c,c'"); $actual = keyValueStringParser::explodeWrapped(',', "'a,a,a,a','b','c,c,c,c,c'");
$this->assertEquals(["'a,a,a,a'" , "'b'","'c,c,c,c,c'"], $actual); $this->assertEquals(["'a,a,a,a'", "'b'", "'c,c,c,c,c'"], $actual);
$actual = keyValueStringParser::explodeWrapped(',', " 'a,a,a' , 'b' , 'c,c' "); $actual = keyValueStringParser::explodeWrapped(',', " 'a,a,a' , 'b' , 'c,c' ");
$this->assertEquals([" 'a,a,a' " , " 'b' "," 'c,c' "], $actual); $this->assertEquals([" 'a,a,a' ", " 'b' ", " 'c,c' "], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c', 2); $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c', 2);
$this->assertEquals(['a' , 'b,c'], $actual); $this->assertEquals(['a', 'b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,a',b,c", 2); $actual = keyValueStringParser::explodeWrapped(',', "'a,a',b,c", 2);
$this->assertEquals(["'a,a'" , 'b,c'], $actual); $this->assertEquals(["'a,a'", 'b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c", 2); $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c", 2);
$this->assertEquals(['a' , "'b,b',c" ], $actual); $this->assertEquals(['a', "'b,b',c"], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,'c,c'", 2); $actual = keyValueStringParser::explodeWrapped(',', "a,b,'c,c'", 2);
$this->assertEquals(['a' , "b,'c,c'" ], $actual); $this->assertEquals(['a', "b,'c,c'"], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 0); $actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 0);
$this->assertEquals(['a,b,c' ], $actual); $this->assertEquals(['a,b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 1); $actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 1);
$this->assertEquals(['a,b,c' ], $actual); $this->assertEquals(['a,b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,b',c", 1); $actual = keyValueStringParser::explodeWrapped(',', "'a,b',c", 1);
$this->assertEquals(["'a,b',c" ], $actual); $this->assertEquals(["'a,b',c"], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,c'", 1); $actual = keyValueStringParser::explodeWrapped(',', "a,'b,c'", 1);
$this->assertEquals(["a,'b,c'" ], $actual); $this->assertEquals(["a,'b,c'"], $actual);
}
public function testExplodeEscapeComma() {
$actual = KeyValueStringParser::explodeEscape(',', '');
$this->assertEquals([''], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a');
$this->assertEquals(['a'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c,d');
$this->assertEquals(['a', 'b', 'c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', ',');
$this->assertEquals(['', ''], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b');
$this->assertEquals(['a', 'b'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b\,c,d');
$this->assertEquals(['a', 'b,c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a\,b,c,d');
$this->assertEquals(['a,b', 'c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c\,d');
$this->assertEquals(['a', 'b', 'c,d'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', '\,a,b');
$this->assertEquals([',a', 'b'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b\,');
$this->assertEquals(['a', 'b,'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,\,b');
$this->assertEquals(['a', ',b'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'pre,a-b:$!@#$%^&*()_-+={}[]|":;/?.><,post');
$this->assertEquals(['pre', 'a-b:$!@#$%^&*()_-+={}[]|":;/?.><', 'post'], $actual);
}
public function testExplodeEscapeColon() {
$actual = KeyValueStringParser::explodeEscape(':', '');
$this->assertEquals([''], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a');
$this->assertEquals(['a'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:b:c:d');
$this->assertEquals(['a', 'b', 'c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', ':');
$this->assertEquals(['', ''], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:b');
$this->assertEquals(['a', 'b'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:b\:c:d');
$this->assertEquals(['a', 'b:c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a\:b:c:d');
$this->assertEquals(['a:b', 'c', 'd'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:b:c\:d');
$this->assertEquals(['a', 'b', 'c:d'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', '\:a:b');
$this->assertEquals([':a', 'b'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:b\:');
$this->assertEquals(['a', 'b:'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'a:\:b');
$this->assertEquals(['a', ':b'], $actual);
$actual = KeyValueStringParser::explodeEscape(':', 'pre:a-b$!@#$%^&*()_-+={}[]|";/?,><:post');
$this->assertEquals(['pre', 'a-b$!@#$%^&*()_-+={}[]|";/?,><', 'post'], $actual);
} }
public function testExplodeEscapeMax() {
$actual = KeyValueStringParser::explodeEscape(',', '', 0);
$this->assertEquals([''], $actual);
$actual = KeyValueStringParser::explodeEscape(',', '', 1);
$this->assertEquals([''], $actual);
$actual = KeyValueStringParser::explodeEscape(',', '', 2);
$this->assertEquals([''], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 4);
$this->assertEquals(['a', 'b', 'c'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 3);
$this->assertEquals(['a', 'b', 'c'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 2);
$this->assertEquals(['a', 'bc'], $actual);
$actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 1);
$this->assertEquals(['abc'], $actual);
}
} }
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