diff --git a/Documentation/Form.rst b/Documentation/Form.rst index 5d7a35e207462a95dd910ccb8558ac9358c5fb0e..a475eb5c4990b0d0a2262f385c3f134291c6d696 100644 --- a/Documentation/Form.rst +++ b/Documentation/Form.rst @@ -2027,9 +2027,7 @@ FormElement.parameter * If `downloadButton` is empty, just shows the regular download glyph. * To just show the filename: `downloadButton = t:{{filenameOnly:V}}` - * Additional attributes might be given like `downloadButton = t:Download|o:check file`. Please check :ref:`download`. - - * The following attributes are hard coded (can't be changed): `s|M:file|d|F` + * Additional attributes might be given like `downloadButton = t:Download|o:check file|G:0`. Please check :ref:`download`. * *fileUnzip* - If the file is a ZIP file (only then) it will be unzipped. If no directory is given via ``fileUnzip``, the basedir of ``fileDestination`` is taken, appended by ``unpack``. diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php index 43c104acb077464c8846c364d70ecb958b68a6a6..8b032674bea11135326c55d9673c74dec2d5c1b4 100644 --- a/extension/Classes/Core/AbstractBuildForm.php +++ b/extension/Classes/Core/AbstractBuildForm.php @@ -2728,7 +2728,7 @@ abstract class AbstractBuildForm { if (!empty($value) && Support::isEnabled($formElement, FE_FILE_DOWNLOAD_BUTTON)) { if (is_readable($value)) { $link = new Link($this->sip, $this->dbIndexData); - $value = $link->renderLink('s|M:file|d|F:' . $value . '|' . $this->evaluate->parse($formElement[FE_FILE_DOWNLOAD_BUTTON])); + $value = $link->renderLink($this->evaluate->parse($formElement[FE_FILE_DOWNLOAD_BUTTON]), 's|M:file|d|F:' . $value); } else { $msg = "Already uploaded file not found."; diff --git a/extension/Classes/Core/Helper/KeyValueStringParser.php b/extension/Classes/Core/Helper/KeyValueStringParser.php index 86b7626fc3be34209a7760650a884844ccbc3ca6..ae817874c3eb4deb777984b2bc53b16b87b397ba 100644 --- a/extension/Classes/Core/Helper/KeyValueStringParser.php +++ b/extension/Classes/Core/Helper/KeyValueStringParser.php @@ -10,7 +10,6 @@ namespace IMATHUZH\Qfq\Core\Helper; - /** * Class KeyValueStringParser @@ -82,7 +81,7 @@ class KeyValueStringParser { } /** - * Parse key/value pairs string and returns them as an assoc array + * Parse key/value pairs string and returns them as an assoc array. Respects escape '\'. * * Hint $keyValueString: "a:1,b:2,c:,d", "," (empty key AND empty value) * @@ -112,7 +111,7 @@ class KeyValueStringParser { // Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape(). if (strpos($keyValueString, '\\' . $listDelimiter) !== false) { - $keyValuePairs = self::explodeEscape($listDelimiter, $keyValueString, 2); + $keyValuePairs = self::explodeEscape($listDelimiter, $keyValueString); } else { $keyValuePairs = explode($listDelimiter, $keyValueString); } @@ -178,12 +177,18 @@ class KeyValueStringParser { // 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)); + $remain[$max - 1] .= $delimiter . implode($delimiter, array_slice($items, $max)); + # Take care, that the 'escape' in last element is not cleaned! + for ($ii = 0; $ii <= $max - 2; $ii++) { + $remain[$ii] = str_replace('\\' . $delimiter, $delimiter, $remain[$ii]); + } $items = $remain; + } else { + // Escaped tokens: the escape character needs to be replaced. + $items = OnArray::arrayValueReplace($items, '\\' . $delimiter, $delimiter); } - // Escaped tokens: the escape character needs to be replaced. - return OnArray::arrayValueReplace($items, '\\' . $delimiter, $delimiter); + return $items; } /** @@ -289,4 +294,26 @@ class KeyValueStringParser { return $final; } + + /** + * Parse kvp string. Key has to be uniq otherwise only the last will be taken. Escaping not supported. + * + * p:{{pageAlias:T}}|q:Delete?:yes:no|download:file.pdf + * @param $str + * @param string $keyValueDelimiter + * @param string $listDelimiter + */ + public static function explodeKvpSimple($str, $keyValueDelimiter = ":", $listDelimiter = ",") { + $result = array(); + + $items = explode($listDelimiter, $str); + foreach ($items as $item) { + if ($item == '') { + continue; + } + $arg = explode($keyValueDelimiter, $item, 2); + $result[$arg[0]] = $arg[1] ?? ''; + } + return $result; + } } diff --git a/extension/Classes/Core/Helper/Token.php b/extension/Classes/Core/Helper/Token.php index 87abadb46bcc04c5c1dfcc09572e5fefde2d260a..e9b0f13c4a171b4169164dd6ed8fd02c8f363fcd 100644 --- a/extension/Classes/Core/Helper/Token.php +++ b/extension/Classes/Core/Helper/Token.php @@ -42,14 +42,8 @@ class Token { $value = 'r'; break; case TOKEN_ENCRYPTION: - $value = '1'; - break; case TOKEN_SIP: - $value = '1'; - break; case TOKEN_BOOTSTRAP_BUTTON: - $value = '1'; - break; case TOKEN_MONITOR: $value = '1'; break; diff --git a/extension/Classes/Core/Report/Link.php b/extension/Classes/Core/Report/Link.php index b8ee821ebb84f9e5a0bb7992fed8f0bffdb9fafc..ed78c3ac6da551d7d52078b63261dd280c0f9115 100644 --- a/extension/Classes/Core/Report/Link.php +++ b/extension/Classes/Core/Report/Link.php @@ -585,14 +585,16 @@ class Link { * Build the whole link. * * @param string $str Qualifier with params. 'report'-syntax. F.e.: u:www.example.com|P:home.gif|t:Home" + * @param string $strDefault Same as $str, but might give some defaults if corresponding values in $str are missing. * * @return string The complete HTML encoded Link like * Description * @throws \CodeException + * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ - public function renderLink($str) { + public function renderLink($str, $strDefault = '') { $tokenGiven = array(); $link = ""; @@ -618,7 +620,7 @@ class Link { break; } - $vars = $this->fillParameter($str, $tokenGiven); + $vars = $this->fillParameter($str, $tokenGiven, $strDefault); $vars = $this->processParameter($vars, $tokenGiven); $mode = $this->getModeRender($vars, $tokenGiven); @@ -697,35 +699,61 @@ class Link { } /** - * Order $param. Parameter with priority are hardcoded at the moment. + * $str and $strDefault are standard QFQ link format strings like 'p:{{pageAlias:T}}|t:linktext|b|s|...' + * Parameter missing in $str and given in $strDefault will used. * - * @param array $param + * Split Parameter string in num Array (assoc is not possible cause for 'download', multiple sources with same key are possible). + * Reorder param to bring prio token (currently only 'd') to top. * + * @param $str + * @param $strDefault * @return array */ - private function paramPriority(array $param) { + private function paramPreparation($str, $strDefault = '') { $prio = array(); $regular = array(); + // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no" + // Return a numbered array with strings like [ 0 => 'p:..', 1 => 't:...' , ...] + $param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str); + + // Return an assoc array like [ 'd' => 'file.pdf', 'p' => 'content', ... ] + $assocDefault = KeyValueStringParser::explodeKvpSimple($strDefault, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER); + foreach ($param as $value) { if ($value == '') { continue; } - $key = substr($value, 0, 2); + $arr = explode(PARAM_TOKEN_DELIMITER, $value, 2); + $key = $arr[0]; + $value = $arr[1] ?? ''; - if (strlen($key) == 1) { - $key .= PARAM_TOKEN_DELIMITER; + if ($key == TOKEN_DOWNLOAD) { + $prio[] = [$key => $value ?? '']; + } else { + $regular[] = [$key => $value ?? '']; } - if ($key == TOKEN_DOWNLOAD . PARAM_TOKEN_DELIMITER) { - $prio[] = $value; - } else { - $regular[] = $value; + // Explicit given arg: remove from default + if (isset($assocDefault[$key])) { + unset ($assocDefault[$key]); } } + // Apply defaults, if not already given + + // First check if there is a prio item - currently only TOKEN_DOWNLOAD is of this type. + if (isset($assocDefault[TOKEN_DOWNLOAD])) { + $prio[] = [TOKEN_DOWNLOAD => $assocDefault[TOKEN_DOWNLOAD]]; + unset ($assocDefault[TOKEN_DOWNLOAD]); + } + // Append all remaining defaults to regular + foreach ($assocDefault as $key => $value) { + $regular[] = [$key => $value]; + } + return array_merge($prio, $regular); } @@ -735,39 +763,38 @@ class Link { * @param string $str * @param array $rcTokenGiven - return an array with found token. * + * @param string $strDefault * @return array * @throws \CodeException + * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ - public function fillParameter($str, array &$rcTokenGiven) { + public function fillParameter($str, array &$rcTokenGiven, $strDefault = '') { + $rcTokenGiven = array(); // Define all possible vars: no more isset(). $vars = $this->initVars(); $flagArray = array(); - // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no" - $param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str); - - $param = $this->paramPriority($param); + $items = $this->paramPreparation($str, $strDefault); // Parse all parameter, fill variables. - foreach ($param as $item) { + foreach ($items as $item) { + + $value = reset($item); + $key = key($item); // Skip empty entries - if ($item === '') { + if (empty($key)) { continue; } - // u:www.example.com - $arr = explode(":", $item, 2); - $key = isset($arr[0]) ? $arr[0] : ''; - $value = isset($arr[1]) ? $arr[1] : ''; - // Bookkeeping defined parameter. if (isset($rcTokenGiven[$key])) { throw new \UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION); } + $rcTokenGiven[$key] = true; if (!isset($this->tableVarName[$key])) { diff --git a/extension/Tests/Unit/Core/Helper/KeyValueStringParserTest.php b/extension/Tests/Unit/Core/Helper/KeyValueStringParserTest.php index e1d425f48ca653338f8d6f4c7f630da39cb474d8..5175e8852c07c57c4c1a0716a0cedaa3d7b8badd 100644 --- a/extension/Tests/Unit/Core/Helper/KeyValueStringParserTest.php +++ b/extension/Tests/Unit/Core/Helper/KeyValueStringParserTest.php @@ -5,7 +5,7 @@ namespace IMATHUZH\Qfq\Tests\Unit\Core\Helper; - + use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser; use PHPUnit\Framework\TestCase; @@ -170,6 +170,42 @@ class KeyValueStringParserTest extends TestCase { $expected = KeyValueStringParser::parse("key1=value1,key2=value2", ":", ","); $this->assertEquals($expected, $actual); $this->assertCount(2, $actual); + + $actual = KeyValueStringParser::parse("key1:value1,key2:value2"); + $expected = KeyValueStringParser::parse("key1:value1,key2:value2", ":", ","); + $this->assertEquals($expected, $actual); + $this->assertCount(2, $actual); + } + + public function testParseEscapeMax() { + $actual = KeyValueStringParser::parse("a:hello A,b:hello B,c:hello C"); + $this->assertEquals(['a' => 'hello A', 'b' => 'hello B', 'c' => 'hello C'], $actual); + + $actual = KeyValueStringParser::parse("a\:new:hello A,b:hello B,c:hello C"); + $this->assertEquals(['a:new' => 'hello A', 'b' => 'hello B', 'c' => 'hello C'], $actual); + + $actual = KeyValueStringParser::parse("a\:new:hello A:A:A,b:hello B,c:hello C"); + $this->assertEquals(['a:new' => 'hello A:A:A', 'b' => 'hello B', 'c' => 'hello C'], $actual); + + // Escape char will be removed + $actual = KeyValueStringParser::parse("a\:new:hello A\:A\:A,b:hello B,c:hello C"); + $this->assertEquals(['a:new' => 'hello A:A:A', 'b' => 'hello B', 'c' => 'hello C'], $actual); + + // Escape list delimiter + $actual = KeyValueStringParser::parse("a\,x:A\,A,b\,x:B\,B"); + $this->assertEquals(['a,x' => 'A,A', 'b,x' => 'B,B'], $actual); + + // Escape list delimiter & key/value delimiter + $actual = KeyValueStringParser::parse("a\,\:x:A\,\:A,b\,\:x:B\,\:B"); + $this->assertEquals(['a,:x' => 'A,:A', 'b,:x' => 'B,:B'], $actual); + + // Escape char in value is untouched + $actual = KeyValueStringParser::parse("a\,x\,y:h\,e\,l\,l\,o A,b:hello B,c:C,d:D"); + $this->assertEquals(['a,x,y' => 'h,e,l,l,o A', 'b' => 'hello B', 'c' => 'C', 'd' => 'D'], $actual); + + // Escape char in value is untouched + $actual = KeyValueStringParser::parse("a\,x\,y:h\,e\,l\,l\,o A,b:hello B,c\,x\,y:h\,e\,l\,lo C"); + $this->assertEquals(['a,x,y' => 'h,e,l,l,o A', 'b' => 'hello B', 'c,x,y' => 'h,e,l,lo C'], $actual); } public function testExplodeContent() { @@ -351,11 +387,13 @@ class KeyValueStringParserTest extends TestCase { $this->assertEquals(['a', 'b', 'c'], $actual); $actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 2); - $this->assertEquals(['a', 'bc'], $actual); + $this->assertEquals(['a', 'b,c'], $actual); $actual = KeyValueStringParser::explodeEscape(',', 'a,b,c', 1); - $this->assertEquals(['abc'], $actual); + $this->assertEquals(['a,b,c'], $actual); + $actual = KeyValueStringParser::explodeEscape(',', 'a\,b,c', 2); + $this->assertEquals(['a,b', 'c'], $actual); } diff --git a/extension/Tests/Unit/Core/Report/LinkTest.php b/extension/Tests/Unit/Core/Report/LinkTest.php index 945a1cab80ba3d48f4b119dd368e33292a9a3af4..523ec6fa321755b1a2c5b35658e9f0076ba54afe 100644 --- a/extension/Tests/Unit/Core/Report/LinkTest.php +++ b/extension/Tests/Unit/Core/Report/LinkTest.php @@ -8,7 +8,7 @@ namespace IMATHUZH\Qfq\Tests\Unit\Core\Report; - + use IMATHUZH\Qfq\Core\Report\Link; use IMATHUZH\Qfq\Core\Store\Session; use IMATHUZH\Qfq\Core\Store\Sip; @@ -57,6 +57,103 @@ class LinkTest extends TestCase { $link->renderLink('abc:hello world'); } + /** + * @throws \CodeException + * @throws \DbException + * @throws \UserFormException + * @throws \UserReportException + */ + public function testfillParameter() { + + $args = ['mail' => '', + 'url' => '', + 'page' => '', + 'text' => '', + 'altText' => '', + 'bootstrapButton' => '', + 'image' => '', + 'imageTitle' => '', + 'glyph' => '', + 'glyphTitle' => '', + 'question' => '', + 'target' => '', + 'toolTip' => '', + 'toolTipJs' => '', + 'param' => '', + 'extraContentWrap' => '', + 'mode' => '', + 'downloadElements' => array(), + 'copyToClipBoard' => '', + 'attribute' => '', + 'render' => '0', + 'picturePositionRight' => 'l', + 'sip' => '0', + 'encryption' => '0', + 'delete' => '', + 'monitor' => '0', + 'linkClass' => '', + 'linkClassDefault' => '', + 'actionDelete' => '', + 'finalHref' => '', + 'finalContent' => '', + 'finalSymbol' => '', + 'finalToolTip' => '', + 'finalClass' => '', + 'finalQuestion' => '', + 'finalThumbnail' => '', + ]; + + $link = new Link($this->sip, DB_INDEX_DEFAULT, true); + + $rcTokenGiven = array(); + + // Empty definition + $expect = $args; + $result = $link->fillParameter('', $rcTokenGiven); + $this->assertEquals($expect, $result); + + // 1 Parameter + $expect = $args; + $expect['page'] = '?id=page1'; + $result = $link->fillParameter('p:page1', $rcTokenGiven); + $this->assertEquals($expect, $result); + + // 1 Parameter, 1 Default + $expect = $args; + $expect['page'] = '?id=page1'; + $expect['text'] = 'comment'; + $result = $link->fillParameter('p:page1', $rcTokenGiven, 't:comment'); + $this->assertEquals($expect, $result); + + // 1 Parameter, 1 Default + $expect = $args; + $expect['page'] = '?id=page1'; + $expect['text'] = 'comment'; + $expect['sip'] = '1'; + $result = $link->fillParameter('p:page1|t:comment', $rcTokenGiven, 'p:page1|t:comment|s'); + $this->assertEquals($expect, $result); + + $expect = $args; + $expect['url'] = 'typo3conf/ext/qfq/Classes/Api/download.php'; + $expect['text'] = 'text'; + $expect['bootstrapButton'] = '0'; + $expect['glyph'] = 'glyphicon-file'; + $expect['glyphTitle'] = 'Download'; + $expect['extraContentWrap'] = ''; + $expect['downloadElements'] = ['p:page1']; + $expect['sip'] = '1'; + $expect['linkClassDefault'] = 'no_class'; + $expect['_exportFilename'] = 'download.pdf'; + $result = $link->fillParameter('p:page1|t:text', $rcTokenGiven, 'd:download.pdf'); + $this->assertEquals($expect, $result); + + $expect['mode'] = 'file'; + $expect['downloadElements'] = ['p:page1', 'F:file.pdf']; + $result = $link->fillParameter('p:page1|t:text|d:download.pdf', $rcTokenGiven, 's|M:file|d|F:file.pdf'); + $this->assertEquals($expect, $result); + + } + /** * @throws \CodeException * @throws \UserFormException @@ -92,6 +189,7 @@ class LinkTest extends TestCase { * @throws \CodeException * @throws \UserFormException * @throws \UserReportException + * @throws \DbException */ public function testLinkUrlBasicExceptionDouble() { $link = new Link($this->sip, DB_INDEX_DEFAULT, true);