Commit 84b1011d authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'F12636InputSingleTickEncode' into 'develop'

Refs #12636. Form: a) Single Tick encode for {{10.line.content::s}}, b) new...

See merge request !352
parents 045802ef 1de3c188
Pipeline #5326 passed with stages
in 3 minutes and 15 seconds
...@@ -1546,7 +1546,19 @@ Type: editor ...@@ -1546,7 +1546,19 @@ Type: editor
^^^^^^^^^^^^ ^^^^^^^^^^^^
* TinyMCE (https://www.tinymce.com, community edition) is used as the QFQ Rich Text Editor. * TinyMCE (https://www.tinymce.com, community edition) is used as the QFQ Rich Text Editor.
* The content will be saved as HTML inside the database. * The content will be saved as HTML code in the database.
.. important::
*FormElement.encode*: To save HTML code, incl. HTML tags (bold, table, lists, ...), the **htmspecialchar**
encoding can't be used, cause the HTML tags loose their meaning. Therefore **single tick** or **none** is necessary.
* *FormElement.checktype*
* *all*: The only useful setting for Editor. HTML tags might contain ``% ' " < >`` and so on. This is **dangerous**
due of potential inserted malicous code! But there is no other option, cause the HTML tags are required.
* All configuration and plugins will be configured via the 'parameter' field. Just prepend the word 'editor-' in front * All configuration and plugins will be configured via the 'parameter' field. Just prepend the word 'editor-' in front
of each TinyMCE keyword. Check possible options under: of each TinyMCE keyword. Check possible options under:
......
...@@ -1899,7 +1899,7 @@ Output:: ...@@ -1899,7 +1899,7 @@ Output::
31.12.2019 23:55 / - / - 31.12.2019 23:55 / - / -
.. _qlugify: .. _qslugify:
QSLUGIFY: clean a string QSLUGIFY: clean a string
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
...@@ -1915,6 +1915,71 @@ Output:: ...@@ -1915,6 +1915,71 @@ Output::
abcd-abcd-ae-a-oe-o-ue-u-z-z abcd-abcd-ae-a-oe-o-ue-u-z-z
.. _qent_squote:
QENT_SQUOTE: convert single tick to HTML entity &apos;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Convert all single ticks in a string to the HTML entity "&apos;"
Example::
10.sql = SELECT QENT_SQUOTE("John's car")
Output::
John&apos;s car
.. _qent_dquote:
QENT_DQUOTE: convert double tick to HTML entity &quot;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Convert all double ticks in a string to the HTML entity "&quot;"
Example::
10.sql = SELECT QENT_SQUOTE('A "nice" event')
Output::
A &quot;nice&quot; event
.. _qesc_squote:
QESC_SQUOTE: escape single tick
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Escape all single ticks with a backslash. Double escaped single ticks (two backslashes) will be replaced by a single
escaped single tick.
Example::
Be Music.style = "Rock'n' Roll"
10.sql = SELECT QESC_SQUOTE(style) FROM Music
Output::
Rock\'n\'n Roll
.. _qesc_dquote:
QESC_SQUOTE: escape double tick
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Escape all double ticks with a backslash. Double escaped double ticks (two backslashes) will replaced by a single
escaped double tick.
Example::
Set Comment.note = 'A "nice" event'
10.sql = SELECT QESC_DQUOTE(style) FROM Music
Output::
Rock\'n\'n Roll
.. _strip_tags: .. _strip_tags:
strip_tags: strip html tags strip_tags: strip html tags
...@@ -1962,11 +2027,12 @@ Example tt-content record for the function:: ...@@ -1962,11 +2027,12 @@ Example tt-content record for the function::
render = api render = api
100 { 100 {
  sql = SELECT p.firstName AS _firstName sql = SELECT p.firstName AS _firstName
               , NOW() AS now, CONCAT('p:{{pageAlias:T}}&form=person&r=', p.id ) AS '_pagee|_hide|myLink'             , NOW() AS now
          FROM Person AS p , CONCAT('p:{{pageAlias:T}}&form=person&r=', p.id ) AS '_pagee|_hide|myLink'
          WHERE p.id={{pId:R}} FROM Person AS p
}                        WHERE p.id={{pId:R}}
}
Example tt-content record for the calling report:: Example tt-content record for the calling report::
...@@ -1976,24 +2042,24 @@ Example tt-content record for the calling report:: ...@@ -1976,24 +2042,24 @@ Example tt-content record for the calling report::
# #
10 { 10 {
  sql = SELECT p.id AS _pId, p.name FROM Person AS p ORDER BY p.name sql = SELECT p.id AS _pId, p.name FROM Person AS p ORDER BY p.name
  head = <table class="table"><tr><th>Name</th><th>Firstname</th><th>Link (final)</th><th>Link (source)</th><th>NOW() (via Output)</th></tr> head = <table class="table"><tr><th>Name</th><th>Firstname</th><th>Link (final)</th><th>Link (source)</th><th>NOW() (via Output)</th></tr>
  tail = </table> tail = </table>
  rbeg = <tr> rbeg = <tr>
  renr = </tr> renr = </tr>
  fbeg = <td> fbeg = <td>
  fend = </td> fend = </td>
                                                                    
  20 { 20 {
    function = getFirstName(pId) => firstName, myLink     function = getFirstName(pId) => firstName, myLink
  } }
                                                                    
  30 { 30 {
    sql = SELECT '{{firstName:R}}', "{{myLink:R}}", "{{&myLink:R}}", '{{_output:R}}' sql = SELECT '{{firstName:R}}', "{{myLink:R}}", "{{&myLink:R}}", '{{_output:R}}'
    fbeg = <td> fbeg = <td>
    fend = </td> fend = </td>
  } }
}                                                                     }
Explanation: Explanation:
......
...@@ -21,6 +21,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray; ...@@ -21,6 +21,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Path; use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize; use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Report\Link; use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Report\Report; use IMATHUZH\Qfq\Core\Report\Report;
use IMATHUZH\Qfq\Core\Store\Sip; use IMATHUZH\Qfq\Core\Store\Sip;
...@@ -862,6 +863,8 @@ abstract class AbstractBuildForm { ...@@ -862,6 +863,8 @@ abstract class AbstractBuildForm {
if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
// $value = htmlspecialchars_decode($value, ENT_QUOTES); // $value = htmlspecialchars_decode($value, ENT_QUOTES);
$value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value); $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
} elseif ($formElement[FE_ENCODE] === FE_ENCODE_SINGLE_TICK) {
$value = OnString::escapeSingleTickInHtml($value);
} }
// Typically: $htmlElementNameIdZero = true // Typically: $htmlElementNameIdZero = true
......
...@@ -1332,6 +1332,7 @@ const TYPEAHEAD_PLACEHOLDER = '?'; ...@@ -1332,6 +1332,7 @@ const TYPEAHEAD_PLACEHOLDER = '?';
// Values // Values
const FE_ENCODE_SPECIALCHAR = 'specialchar'; const FE_ENCODE_SPECIALCHAR = 'specialchar';
const FE_ENCODE_SINGLE_TICK = 'single tick';
const FE_ENCODE_NONE = 'none'; const FE_ENCODE_NONE = 'none';
const FE_FILE_CAPTURE_CAMERA = 'camera'; const FE_FILE_CAPTURE_CAMERA = 'camera';
......
...@@ -204,6 +204,11 @@ $UPDATE_ARRAY = array( ...@@ -204,6 +204,11 @@ $UPDATE_ARRAY = array(
"ALTER TABLE `FormSubmitLog` ADD `formName` VARCHAR(255) NOT NULL DEFAULT '' AFTER `formId`;", "ALTER TABLE `FormSubmitLog` ADD `formName` VARCHAR(255) NOT NULL DEFAULT '' AFTER `formId`;",
], ],
'21.6.0' => [
"ALTER TABLE `FormElement` CHANGE `encode` `encode` ENUM('none','specialchar','single tick') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'specialchar';",
],
); );
......
...@@ -249,7 +249,7 @@ class Evaluate { ...@@ -249,7 +249,7 @@ class Evaluate {
* @throws \UserFormException * @throws \UserFormException
* @throws \UserReportException * @throws \UserReportException
*/ */
private function inlineLink($arrToken, $dbIndex, &$foundInStore) { private function inlineLink($arrToken, $dbIndex, &$foundInStore): string {
$token = OnString::trimQuote(trim(implode(' ', $arrToken))); $token = OnString::trimQuote(trim(implode(' ', $arrToken)));
...@@ -262,31 +262,6 @@ class Evaluate { ...@@ -262,31 +262,6 @@ class Evaluate {
return $this->link->renderLink($token); return $this->link->renderLink($token);
} }
/**
* Get the CET/CEST Timezone for a given date, or if date is '' based on the current date.
*
* @param string $dateStr
* @return string // CET, CEST or GMT+?/GMT-?
*/
public function getEuropeanTimezone($dateStr = '') {
$ts = ($dateStr == '') ? time() : strtotime($dateStr);
$offset = date("Z", $ts) / 3600;
switch ($offset) {
case 1:
$tz = "CET";
break;
case 2:
$tz = "CEST";
break;
default:
$tz = 'GMT' . sprintf("%+d", $offset);
}
return $tz;
}
/** /**
* @param $arrToken * @param $arrToken
* @param $dbIndex * @param $dbIndex
...@@ -296,7 +271,7 @@ class Evaluate { ...@@ -296,7 +271,7 @@ class Evaluate {
* @throws \UserFormException * @throws \UserFormException
* @throws \UserReportException * @throws \UserReportException
*/ */
private function inlineDataDndApi($arrToken, $dbIndex, &$foundInStore) { private function inlineDataDndApi($arrToken, $dbIndex, &$foundInStore): string {
$token = OnString::trimQuote(trim(implode(' ', $arrToken))); $token = OnString::trimQuote(trim(implode(' ', $arrToken)));
...@@ -344,7 +319,7 @@ class Evaluate { ...@@ -344,7 +319,7 @@ class Evaluate {
$token = trim($token); $token = trim($token);
$dbIndex = $this->dbIndex; $dbIndex = $this->dbIndex;
$flagWipe = false; $rcFlagWipe = false;
// Check if the $token starts with '[<int>]...' - yes: open the necessary database. // Check if the $token starts with '[<int>]...' - yes: open the necessary database.
if (strlen($token) > 2 && $token[0] === '[') { if (strlen($token) > 2 && $token[0] === '[') {
...@@ -421,64 +396,7 @@ class Evaluate { ...@@ -421,64 +396,7 @@ class Evaluate {
$value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANITIZE], $value = $this->store::getVar($arrToken[VAR_INDEX_VALUE], $arrToken[VAR_INDEX_STORE], $arrToken[VAR_INDEX_SANITIZE],
$foundInStore, $typeMessageViolate, $arrToken[VAR_INDEX_DEFAULT]); $foundInStore, $typeMessageViolate, $arrToken[VAR_INDEX_DEFAULT]);
// escape ticks $value = OnString::escape($escapeTypes, $value, $rcFlagWipe);
if (is_string($value)) {
// Process all escape requests in the given order.
for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {
$escape = $escapeTypes[$ii];
if ($escape == TOKEN_ESCAPE_CONFIG) {
$escape = $this->escapeTypeDefault;
}
switch ($escape) {
case TOKEN_ESCAPE_SINGLE_TICK:
$value = str_replace("'", "\\'", $value);
break;
case TOKEN_ESCAPE_DOUBLE_TICK:
$value = str_replace('"', '\\"', $value);
break;
case TOKEN_ESCAPE_COLON:
$value = str_replace(':', '\\:', $value);
break;
case TOKEN_ESCAPE_LDAP_FILTER:
$value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
break;
case TOKEN_ESCAPE_LDAP_DN:
$value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
break;
case TOKEN_ESCAPE_MYSQL:
$value = $this->dbArray[$dbIndex]->realEscapeString($value);
break;
case TOKEN_ESCAPE_NONE: // do nothing
break;
case TOKEN_ESCAPE_PASSWORD_T3FE:
$value = T3Handler::getHash($value);
break;
case TOKEN_ESCAPE_STOP_REPLACE:
$value = Support::encryptDoubleCurlyBraces($value);
break;
case TOKEN_ESCAPE_EXCEPTION:
// empty values will be handled later.
break;
case TOKEN_ESCAPE_WIPE:
$flagWipe = true;
break;
case TOKEN_ESCAPE_TIMEZONE:
$value = $this->getEuropeanTimezone($value);
break;
case TOKEN_ESCAPE_HTML_SPECIAL_CHAR:
$value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
break;
default:
throw new \UserFormException("Unknown escape qualifier: $escape", ERROR_UNKNOW_SANITIZE_CLASS);
break;
}
}
} else {
// In case the value is not found and the escape class forces a full stop
if (strpos($escapeTypes, TOKEN_ESCAPE_EXCEPTION) !== false) {
throw new \UserFormException($arrToken[VAR_INDEX_MESSAGE] ?? '', ERROR_QUIT_QFQ_REGULAR);
}
}
// Not found and a default is given: take the default. // Not found and a default is given: take the default.
if ($foundInStore == '' && $arrToken[VAR_INDEX_DEFAULT] != '') { if ($foundInStore == '' && $arrToken[VAR_INDEX_DEFAULT] != '') {
...@@ -486,7 +404,7 @@ class Evaluate { ...@@ -486,7 +404,7 @@ class Evaluate {
$value = str_replace('\\:', ':', $arrToken[VAR_INDEX_DEFAULT]); $value = str_replace('\\:', ':', $arrToken[VAR_INDEX_DEFAULT]);
} }
if ($flagWipe) { if ($rcFlagWipe) {
switch ($foundInStore) { switch ($foundInStore) {
case STORE_SIP: case STORE_SIP:
$this->store::unsetVar($arrToken[VAR_INDEX_VALUE], STORE_SIP); $this->store::unsetVar($arrToken[VAR_INDEX_VALUE], STORE_SIP);
......
...@@ -9,12 +9,26 @@ ...@@ -9,12 +9,26 @@
namespace IMATHUZH\Qfq\Core\Helper; namespace IMATHUZH\Qfq\Core\Helper;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;
/** /**
* Class OnString * Class OnString
* @package qfq * @package qfq
*/ */
class OnString { class OnString {
/**
* @var Store
*/
private static $store = null;
/**
* @var Database
*/
private static $db = null;
/** /**
* Returns part of haystack string starting from and including the last occurrence of needle to the end of haystack. * Returns part of haystack string starting from and including the last occurrence of needle to the end of haystack.
* *
...@@ -372,5 +386,264 @@ class OnString { ...@@ -372,5 +386,264 @@ class OnString {
$rcFunctionParam = OnArray::trimArray(explode(',', $args[0] ?? '')); $rcFunctionParam = OnArray::trimArray(explode(',', $args[0] ?? ''));
$rcReturnParam = OnArray::trimArray(explode(',', $split[1] ?? '')); $rcReturnParam = OnArray::trimArray(explode(',', $split[1] ?? ''));
} }
/**
* Escape $value by list of $escapeTypes.
*
* @param $escapeTypes
* @param $value
* @param $rcFlagWipe
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public static function escape($escapeTypes, $value, &$rcFlagWipe) {
// escape ticks
if (is_string($value)) {
// Process all escape requests in the given order.
for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {
$escape = $escapeTypes[$ii];
if ($escape == TOKEN_ESCAPE_CONFIG) {
$escape = self::getEscapeTypeDefault();
}
switch ($escape) {
case TOKEN_ESCAPE_SINGLE_TICK:
$value = str_replace("'", "\\'", $value);
break;
case TOKEN_ESCAPE_DOUBLE_TICK:
$value = str_replace('"', '\\"', $value);
break;
case TOKEN_ESCAPE_COLON:
$value = str_replace(':', '\\:', $value);
break;
case TOKEN_ESCAPE_LDAP_FILTER:
$value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
break;
case TOKEN_ESCAPE_LDAP_DN:
$value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
break;
case TOKEN_ESCAPE_MYSQL:
if (self::$db === null) {
self::$db = new Database();
}
$value = self::$db->realEscapeString($value);
break;
case TOKEN_ESCAPE_NONE: // do nothing
break;
case TOKEN_ESCAPE_PASSWORD_T3FE:
$value = T3Handler::getHash($value);
break;
case TOKEN_ESCAPE_STOP_REPLACE:
$value = Support::encryptDoubleCurlyBraces($value);
break;
case TOKEN_ESCAPE_EXCEPTION:
// empty values will be handled in 'else'.
break;
case TOKEN_ESCAPE_WIPE:
$rcFlagWipe = true;
break;
case TOKEN_ESCAPE_TIMEZONE:
$value = self::getEuropeanTimezone($value);
break;
case TOKEN_ESCAPE_HTML_SPECIAL_CHAR:
$value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
break;
default:
throw new \UserFormException("Unknown escape qualifier: $escape", ERROR_UNKNOW_SANITIZE_CLASS);
break;
}
}
} else {
// In case the value is not found and the escape class forces a full stop
if (strpos($escapeTypes, TOKEN_ESCAPE_EXCEPTION) !== false) {
throw new \UserFormException($arrToken[VAR_INDEX_MESSAGE] ?? '', ERROR_QUIT_QFQ_REGULAR);
}
}
return $value;
}
/**
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
private static function getEscapeTypeDefault() {
static $escapeTypeDefault = null;
if ($escapeTypeDefault === null) {
self::$store = Store::getInstance();
$escapeTypeDefault = self::$store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
}
return $escapeTypeDefault;
}
/**
* Get the CET/CEST Timezone for a given date, or if date is '' based on the current date.
*
* @param string $dateStr
* @return string
*/
public static function getEuropeanTimezone($dateStr = '') {
$ts = ($dateStr == '') ? time() : strtotime($dateStr);
$offset = date("Z", $ts) / 3600;
switch ($offset) {
case 1:
$tz = "CET";
break;
case 2:
$tz = "CEST";
break;
default:
$tz = 'GMT' . sprintf("%+d", $offset);
}
return $tz;
}
/**
* Replaces single tick in a HTML code by html entity
* or if a attribute is quoted with single tick convert the attribute to double quoted.
* If there is a double quote in single tick quoted attribute, it's replaced by '&quot;'.
*
* Content (outside of an HTML tag): single ticks will be replaced by '&apos;':
* <b>John's</b> >> <b>John&apos;s</b>
*
* HTML tag attribute: A single tick quoted attribute will be converted to a double tick quoted attribute.
* <img title='Moon'> >> <img title="Moon">
*
* HTML tag attribute: A single ticks in a double tick quoted string will be replaced by '&apos;'
* <img title='echo "hello"'> >> <img title="echo &quot;hello&quot;'>
*
* @param $line
* @return string
*/
public static function escapeSingleTickInHtml($line): string {
$flagTag = false;
$flagAttribute = false;
$flagAttributeStartSingleTick = false;
$flagAttributeStartDoubleTick = false;
$flagAttributeStarted = false;
$posStartSingleTick = null;
$new = '';
$length = strlen($line);
for ($ii = 0; $ii < $length; $ii++) {
$c = $line[$ii];
switch ($c) {
case '>':
// HTML tag ends here: close all open flags
$flagTag = false;
$flagAttribute = false;
$flagAttributeStartSingleTick = false;
$flagAttributeStartDoubleTick = false;
$flagAttributeStarted = false;
$posStartSingleTick = null;
break;
case '<':
// HTML tag starts here
$flagTag = true;
break;
case '=':
if ($flagTag && !$flagAttribute) {
// Inside a tag and outside a running attribute: here starts a new attribute
$flagAttribute = true;
}
break;
case ' ':