Commit 55b5433a authored by Carsten  Rose's avatar Carsten Rose

Merge remote-tracking branch 'origin/develop' into develop

parents f9de05c2 98cbacdd
Pipeline #3325 failed with stages
in 2 minutes and 50 seconds
......@@ -59,11 +59,12 @@ Use with LDAP: `typeAheadLdap`
### .data-typeahead-limit
* Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value. The server default is 20.
* Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value.
The server default is 20.
### .data-typeahead-minlength
* Defines the string minlegth, typed by the user, before the first lookup is started. Default is 2.
* Defines the string minlength, typed by the user, before the first lookup is started. Default is 2.
### data-typeahead-pedantic
......@@ -72,7 +73,8 @@ Use with LDAP: `typeAheadLdap`
## Tags Form Element
The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional to the attributes for Typeahead (see above).
The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional
to the attributes for Typeahead (see above).
Mockups can be found in `mockup/typahead.php`
......
......@@ -287,7 +287,7 @@ Setup a *report* to manage all *forms*:
10 {
# All forms
sql = SELECT CONCAT('p:{{pageAlias:T}}&form=form&r=', f.id, '|A:data-reference=editForm', f.name) as _pagee
, f.id, QMORE(f.name, 50), f.title AS '_striptags', f.tableName
, f.id, f.name, QMORE(strip_tags(f.title), 50), f.tableName
, CONCAT('U:form=form&r=', f.id, '|A:data-reference=deletForm') as _paged
FROM Form AS f
ORDER BY f.name
......@@ -2080,15 +2080,17 @@ To decide which Parameter should be placed on *Form.parameter* and which on *For
.. _LDAP_Typeahead:
Typeahead (TA)
--------------
Typeahead (TA) - LDAP
---------------------
*Typeahead* offers continous searching of a LDAP directoy by using a regular *FormElement* of type *text*.
See also `input-typeahead`_
*Typeahead* offers continuous searching of a LDAP directoy by using a regular *FormElement* of type *text*.
The *FormElement.parameter*=*typeAheadLdap* will trigger LDAP searches on every user **keystroke**
(starting after *typeAheadMinLength* keystrokes) for the current *FormElement* - this is different from *dynamicUpdate*
(triggered by leaving focus of an input element). Typeahead delivers a list of elements.
* *FormElement.parameter.typeAheadLdap* - activate the mode *Typeahead* - no value is needed, the existence is suffucient.
* *FormElement.parameter.typeAheadLdap* - activate the mode *Typeahead* - no value is needed, the existence is sufficient.
* *Form.parameter* or *FormElement.parameter*:
* *ldapServer* = `directory.example.com`
......@@ -2146,7 +2148,7 @@ This situation also applies in *pedantic* mode to verify the user input after ea
PerToken
^^^^^^^^
Sometimes a LDAP server only provides attributes like 'sn' and 'givenName', but not 'displayName' or another practial
Sometimes a LDAP server only provides attributes like 'sn' and 'givenName', but not 'displayName' or another practical
combination of multiple attributes - than it is difficult to search for 'firstname' *and* (=human AND) 'lastname'.
E.g. 'John Doe', results to search like `(|(sn=*John Doe*)(givenName=*John Doe*))` which will be probably always be empty.
Instead, the user input has to be split in token and the search string has to repeated for every token.
......@@ -3228,6 +3230,12 @@ See also at specific *FormElement* definitions.
| typeAheadMinLength, | | |
| typeAheadSql, | | |
| typeAheadSqlPrefetch, | | |
| typeAheadPedantic | | |
+------------------------+--------+----------------------------------------------------------------------------------------------------------+
| typeAheadTag, | | See `type_ahead_tag`_ |
| typeAheadGlueInsert, | | |
| typeAheadGlueDelete, | | |
| typeAheadTagInsert | | |
+------------------------+--------+----------------------------------------------------------------------------------------------------------+
| wrapRow | string | If specified, skip default wrapping (`<div class='col-md-?'>`). Instead the given string is used. |
+------------------------+--------+ |
......@@ -3558,6 +3566,123 @@ LDAP
See :ref:`LDAP_Typeahead`
.. _`type_ahead_tag`:
Type Ahead Tag
""""""""""""""
Extend a TypeAhead input element to take more than one token (=tag) in the same input element.
This mode supports only *typeAheadSql* (no LDAP).
Usage: A user might choose one or more tags from a typeahead list (to minimize typos and to reuse already given tags).
The user starts typing and for each keypress *typeAheadSql* is searched for all matches. The user selects an element
by clicking on it or by using one of the *typeAheadTagDelimiter* key presses (by default tab or comma). If a tag is
selected, it will be visual separated from the input cursor. Already selected tags can not be edited but removed
(clicking on the x). Further tags can be added.
*typeAheadTag* support two different modes: a) *Tag* , b) *Glue*.
.. _`ta_mode_tag`:
Mode: Tag
;;;;;;;;;
Tags will be loaded and saved as a comma separated list. Maximum length of saved tags is limit by
the size of the column (incl. separator).
Additional arguments needed for *typeAheadTag*:
* *FormElement.parameter*:
* *typeAheadTag* = [0|1] - Default 0 (=off), existence or =1 switches the mode *typeAheadTag* on.
* *typeAheadTagDelimiter* = List of ASCII codes to separate tags during input. Default '9,44' (tab and comma).
.. _`ta_mode_glue`:
Mode: Glue
;;;;;;;;;;
For each selected tag a glue record, pointing to the tag, is created.
The *Glue* mode will be activated by setting *FormElement.parameter.typeAheadGlueInsert* with a corresponding SQL statement.
Glue records will be created or deleted, as the user select or deselect tags. Processing of those Glue records will be done
after the primary form record has been written and before any after*-action FormElements will be processed.
*FormElement.name* should **not** point to a column in the form primary table. Instead a free name should be used for the *typeAhead*
FormElement.
The maximum number of tags is not limited - but take care to size the FormElement big enough (*FormElement.maxLength*) to
show all tags.
On *Form load* (to show already assigned tags) a comma separated list has to be given in *FormElement.value*, based on
the previously saved Glue records. The string format is identically to the one used in mode *Tag*.
Extra parameter for mode = *Tag* :
* *FormElement.parameter*:
* *typeAheadTagInsert* = {{INSERT INTO Tag (....) VALUES (...)}} - Only needed with *typeAheadPedantic=0*.
* *typeAheadGlueInsert* = {{INSERT INTO glueTag (...) VALUES (...)}}
* *typeAheadGlueDelete* = {{DELETE FROM glueTag WHERE ...}}
**Example**:
Table *Person* with some records.
Table *Fruit* with a list of fruits.
Table *FruitPerson* with glue records.
Usage: assign favourite fruits to a person. The fruits are the tags, the glue records will assign the fruits to a person.
The form will be open with a person record and has only one FormElement.
* Form.name=personFavouriteFruits
* Form.title=Person Favourite Fruits
* Form.primaryTable = Person
* FormElement[1].name = myFavoriteFruits
* FormElement[1].type = Text
* FormElement[1].value = {{SELECT GROUP_CONCAT( CONCAT(f.id, ':', f.name)) FROM FruitPerson AS fp, Fruit AS f WHERE fp.pId={{id:R}} AND fp.fruitId=f.id ORDER BY f.name}}
* FormElement[1].parameter:
* typeAheadTag = 1
* typeAheadSql = SELECT f.id AS 'id', f.name AS 'value' FROM Fruit AS f WHERE f.name LIKE ?
* typeAheadMinLength = 1
* typeAheadGlueInsert = {{INSERT INTO FruitPerson (pId, fruitId) VALUES ({{id:R}}, {{tagId:V}} ) }}
* typeAheadGlueDelete = {{DELETE FROM FruitPerson WHERE pId={{id:R}} AND fruitId={{tagId:V}} }}
Explanation:
* On form load, without any assigned tags (=fruits), *FormElement.value* will be empty.
* The User will assign three fruits: Apple, Banana, Lemon.
* On form save, QFQ does:
* compares the old tag assigment (empty) with the new tag assigment (3 elements).
* for each new assigned tag:
* the *tagId* and *tagValue* will be stored in STORE_VAR (that's the one selected by the user and defined
via *typeAheadSql*)
* *typeAheadGlueInsert* will be fired (with the replaced variable *{{tagId:V}}*).
* The user loads the person favourite fruit form again (same user).
* *FormElement.value* will now be: ``1:Apple,3:Banana,10:Lemon``.
* The user removes 'Banana' and adds 'Orange'.
* On form save, QFQ does:
* compares the old tag assigment (3 elements) with the new tag assigment (also 3 elements, but different).
* for each new assigned tag:
* the *tagId* and *tagValue* will be stored in STORE_VAR.
* *typeAheadGlueInsert* will be fired (with the replaced variable *{{tagId:V}}*).
* for each removed assigned tag:
* the *tagId* and *tagValue* will be stored in STORE_VAR.
* *typeAheadGlueDelete* will be fired (with the replaced variable *{{tagId:V}}*).
.. _`input-editor`:
Type: editor
......@@ -3578,7 +3703,6 @@ Type: editor
* Top: *toolbar* - by default visible.
* Bottom: *statusbar* - by default hidden, exception: *min_height* and *max_height* are given via size parameter.
* The default setting in *FormElement.parameter* is::
editor-plugins=code link lists searchreplace table textcolor textpattern visualchars
......@@ -9226,4 +9350,4 @@ To allow it, add 'span' to the valid elements in the FormElement.parameter field
editor-extended_valid_elements = span[class|style]
The HTML span tag has to be added via 'source' view. At least in TinyMCE 4.7.13, the glyph is still not shown in the
editor but saved.
editor.
......@@ -736,7 +736,8 @@ abstract class AbstractBuildForm {
$html = '';
// The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
$skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER, FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON];
$skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT];
// get current data record
$primaryKey = $this->formSpec[F_PRIMARY_KEY];
......@@ -1318,8 +1319,23 @@ abstract class AbstractBuildForm {
$class = 'form-control';
$elementCharacterCount = '';
$typeAheadUrlParam = $this->typeAheadBuildParam($formElement);
if ($typeAheadUrlParam != '') {
if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') {
// crop string only if it's not empty (substr returns false on empty strings)
$value = mb_substr($value, 0, $formElement[FE_MAX_LENGTH]);
}
if ($formElement[FE_HIDE_ZERO] != '0' && $value == '0') {
$value = '';
}
if ($formElement[FE_DECIMAL_FORMAT] !== '') {
if ($value !== '') { // empty string causes exception in number_format()
$decimalScale = explode(',', $formElement[FE_DECIMAL_FORMAT])[1]; // scale: Nachkommastellen
$value = number_format($value, $decimalScale, '.', '');
}
}
if ('' != ($typeAheadUrlParam = $this->typeAheadBuildParam($formElement))) {
if (empty($formElement[FE_INPUT_TYPE])) {
$formElement[FE_INPUT_TYPE] = FE_TYPE_SEARCH; // typeahead behaves better with 'search' instead of 'text'
......@@ -1334,9 +1350,27 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
if (isset($formElement[FE_TYPEAHEAD_PEDANTIC]) && $formElement[FE_TYPEAHEAD_PEDANTIC] === '1') {
if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_PEDANTIC] ?? '-')) {
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_PEDANTIC, 'true');
}
// Tag
if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_TAG, 'true');
// Default: tab, comma
$kk = '[' . ($formElement[FE_TYPEAHEAD_TAG_DELIMITER] ?? '9,44') . ']';
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_TAG_DELIMITER, $kk);
$formElement[FE_INPUT_TYPE] = 'hidden';
// Client: TAG handling expects the '$value' as a JSON string.
$kk = KeyValueStringParser::parse($value, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, KVP_IF_VALUE_EMPTY_COPY_KEY);
$jj = '';
foreach ($kk as $arrKey => $arrValue) {
$jj .= ',' . json_encode(["key" => $arrKey, "value" => $arrValue]);
}
$value = '[' . substr($jj, 1) . ']';
}
}
if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
......@@ -1364,26 +1398,11 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']');
}
if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') {
// crop string only if it's not empty (substr returns false on empty strings)
$value = mb_substr($value, 0, $formElement[FE_MAX_LENGTH]);
}
// 'maxLength' needs an upper 'L': naming convention for DB tables!
if ($formElement[FE_MAX_LENGTH] > 0) {
$attribute .= Support::doAttribute('maxlength', $formElement[FE_MAX_LENGTH], false);
}
if ($formElement[FE_HIDE_ZERO] != '0' && $value == '0') {
$value = '';
}
if ($formElement[FE_DECIMAL_FORMAT] !== '') {
if ($value !== '') { // empty string causes exception in number_format()
$decimalScale = explode(',', $formElement[FE_DECIMAL_FORMAT])[1]; // scale: Nachkommastellen
$value = number_format($value, $decimalScale, '.', '');
}
}
// In case the user specifies MIN & MAX with numbers, the html tag 'type' has to be 'number', to make the range check work in the browser.
if (empty($formElement[FE_INPUT_TYPE]) && !empty($formElement[FE_MIN]) && !empty($formElement[FE_MAX]) &&
is_numeric($formElement[FE_MIN]) && is_numeric($formElement[FE_MAX])
......@@ -1424,9 +1443,14 @@ abstract class AbstractBuildForm {
$htmlTag = '<input';
if (!empty($formElement[FE_INPUT_TYPE])) {
$formElement[FE_TYPE] = $formElement[FE_INPUT_TYPE];
// TypeAhead tag elements needs to be hidden
if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
$formElement[FE_TYPE] = 'hidden';
}
}
$attribute .= HelperFormElement::getAttributeList($formElement, [FE_TYPE, 'size']);
$attribute .= Support::doAttribute('value', htmlentities($value), false);
// $attribute .= Support::doAttribute('value', htmlentities($value, ENT_QUOTES, 'UTF-8'), false);
}
$attribute .= HelperFormElement::getAttributeList($formElement, [FE_INPUT_AUTOCOMPLETE, 'autofocus', 'placeholder']);
......@@ -1443,7 +1467,6 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute(F_FE_DATA_PATTERN_ERROR, $formElement[F_FE_DATA_PATTERN_ERROR], true, ESCAPE_WITH_BACKSLASH);
}
if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
$attribute .= Support::doAttribute(F_FE_DATA_REQUIRED_ERROR, $formElement[F_FE_DATA_REQUIRED_ERROR]);
}
......@@ -1456,7 +1479,6 @@ abstract class AbstractBuildForm {
$attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
$attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
$attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE], false);
$attribute .= Support::doAttribute('class', $class);
......@@ -1503,11 +1525,10 @@ abstract class AbstractBuildForm {
if (isset($formElement[FE_TYPEAHEAD_SQL])) {
$sql = $this->checkSqlAppendLimit($formElement[FE_TYPEAHEAD_SQL], $formElement[FE_TYPEAHEAD_LIMIT]);
$formElement[FE_TYPEAHEAD_SQL_PREFETCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_SQL_PREFETCH);
$arr = [
FE_TYPEAHEAD_SQL => $sql,
FE_TYPEAHEAD_SQL_PREFETCH => $formElement[FE_TYPEAHEAD_SQL_PREFETCH]
FE_TYPEAHEAD_SQL_PREFETCH => $formElement[FE_TYPEAHEAD_SQL_PREFETCH] ?? ''
];
} elseif (isset($formElement[FE_TYPEAHEAD_LDAP])) {
$formElement[FE_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_LDAP_SERVER);
......@@ -1566,6 +1587,14 @@ abstract class AbstractBuildForm {
$sqlTest = '';
$sql = trim($sql);
// If exist, unwrap '{{!', '}}'
if (substr($sql, 0, 2) == '{{') {
$sql = trim(substr($sql, 2, strlen($sql) - 4));
if ($sql[0] ?? '' == '!') {
$sql = trim(substr($sql, 1));
}
}
if ($sql[0] == '[') {
// Remove optional existing dbIndex token.
......
......@@ -728,7 +728,8 @@ const VAR_FILENAME_EXT = 'filenameExt'; // Extension of the original filename of
const VAR_FILE_MIME_TYPE = 'mimeType';
const VAR_FILE_SIZE = 'fileSize';
const VAR_ALL_REQUIRED_GIVEN = 'allRequiredGiven'; // If all required FE are given: 1, else: 0. If there is no required FE: 1
const VAR_TAG_ID = 'tagId';
const VAR_TAG_VALUE = 'tagValue';
// PHP class Typeahead
const TYPEAHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=... - See also FE_TYPE_AHEAD_SQL
......@@ -858,6 +859,8 @@ const DATA_ENABLE_SAVE_BUTTON = 'data-enable-save-button';
const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit';
const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength';
const DATA_TYPEAHEAD_PEDANTIC = 'data-typeahead-pedantic';
const DATA_TYPEAHEAD_TAG = 'data-typeahead-tags';
const DATA_TYPEAHEAD_TAG_DELIMITER = 'data-typeahead-tag-delimiters';
const CLASS_CHARACTER_COUNT = 'qfq-character-count';
const DATA_CHARACTER_COUNT_ID = 'data-character-count-id';
......@@ -1237,6 +1240,12 @@ const FE_LDAP_USE_BIND_CREDENTIALS = F_LDAP_USE_BIND_CREDENTIALS;
const FE_TYPEAHEAD_LIMIT = F_TYPEAHEAD_LIMIT;
const FE_TYPEAHEAD_MINLENGTH = F_TYPEAHEAD_MINLENGTH;
const FE_TYPEAHEAD_PEDANTIC = F_TYPEAHEAD_PEDANTIC;
const FE_TYPEAHEAD_TAG = 'typeAheadTag';
const FE_TYPEAHEAD_TAG_DELIMITER = 'typeAheadTagDelimiter';
const FE_TYPEAHEAD_GLUE_INSERT = 'typeAheadGlueInsert';
const FE_TYPEAHEAD_GLUE_DELETE = 'typeAheadGlueDelete';
const FE_TYPEAHEAD_TAG_INSERT = 'typeAheadTagInsert';
const FE_TYPEAHEAD_SQL = 'typeAheadSql';
const FE_TYPEAHEAD_SQL_PREFETCH = 'typeAheadSqlPrefetch';
const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = F_TYPEAHEAD_LDAP_VALUE_PRINTF;
......@@ -1732,6 +1741,9 @@ const QUESTION_INDEX_FLAG_MODAL = 5;
const PARAM_DELIMITER = '|';
const PARAM_TOKEN_DELIMITER = ':';
const PARAM_LIST_DELIMITER = ',';
const PARAM_KEY_VALUE_DELIMITER = ':';
const TOKEN_URL = 'u';
const TOKEN_MAIL = 'm';
const TOKEN_PAGE = 'p';
......
......@@ -135,10 +135,11 @@ class Evaluate {
* @param string $line
* @param string $sqlMode ROW_IMPLODE | ROW_REGULAR | ... - might be overwritten in $line by '{{!...'
* @param int $recursion
*
* @param array $debugStack
* @param string $foundInStore
* @return array|mixed|null|string
*
* @return array|mixed|null|string - in case of INSERT: last_insert_id()
*
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
......
......@@ -342,12 +342,15 @@ class FormAction {
}
/**
* Process slaveId, sqlBefore, sqlInsert|sqlUpdate|sqlDelete, sqlAfter.
* flagFeAction=false: for Native Elements
* flagFeAction=true: for Action Elements
*
* Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
* `sqlInsert`
*
* @param array $fe
* @param int $recordId
*
* @param bool $flagFeAction indicates of the FE are of type 'native' or 'action'.
* @return int ACTION_ELEMENT_MODIFIED if there are potential(!) changes on the DB like INSERT / UPDATE,
* ACTION_ELEMENT_NO_CHANGE if nothing happened
......
......@@ -41,7 +41,8 @@ class KeyValueStringParser {
*
* @return string
*/
public static function unparse(array $keyValueArray, $keyValueDelimiter = ":", $listDelimiter = ",") {
public static function unparse(array $keyValueArray, $keyValueDelimiter = PARAM_KEY_VALUE_DELIMITER, $listDelimiter = PARAM_LIST_DELIMITER, $flagEscape = false) {
array_walk($keyValueArray, function (&$value) {
if (!is_string($value) || $value === "" || strlen($value) === 1) {
return;
......@@ -54,6 +55,13 @@ class KeyValueStringParser {
$newKeyValuePairImploded = array();
foreach ($keyValueArray as $key => $value) {
if ($flagEscape) {
$key = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $key);
$key = str_replace($listDelimiter, '\\' . $listDelimiter, $key);
$value = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $value);
$value = str_replace($listDelimiter, '\\' . $listDelimiter, $value);
}
$newKeyValuePairImploded[] = trim($key) . $keyValueDelimiter . $value;
}
......
......@@ -49,7 +49,7 @@ use IMATHUZH\Qfq\Core\Store\Store;
* G:Glyph
* h:
* H:Help
* i:
* i:icon (Font Awesome, t)
* I:information
* j:
* J:
......
......@@ -12,6 +12,7 @@ use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Form\FormAction;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
......@@ -288,7 +289,6 @@ class Save {
}
$newValues[$column] = $formValues[$column];
$realColumnFound = true;
}
// Only save record if real columns exist.
......@@ -315,6 +315,8 @@ class Save {
}
/**
* Process sqlBefore, sqlInsert|.... for all native FE.
*
* @param $recordId
*
* @throws \CodeException
......@@ -330,6 +332,67 @@ class Save {
$this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);
$this->formAction->doSqlBeforeSlaveAfter($fe, $recordId, false);
$this->typeAheadDoTagGlue($fe);
}
}
/**
* typeAhead: if given, process Tag or Glue.
*
* @param array $fe
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
private function typeAheadDoTagGlue(array $fe) {
// Update 'glue' records?
if (($fe[FE_TYPEAHEAD_TAG] ?? '0') == '0' || (!isset($fe[FE_TYPEAHEAD_GLUE_INSERT]) && !isset($fe[FE_TYPEAHEAD_TAG_INSERT]))) {
return;
}
if (empty($fe[FE_TYPEAHEAD_GLUE_INSERT]) || empty($fe[FE_TYPEAHEAD_GLUE_DELETE])) {
throw new \UserFormException("Missing 'typeAheadGlueInsert' or 'typeAheadGlueDelete'", ERROR_MISSING_REQUIRED_PARAMETER);
}
// Extract assigned tags: last
$tagLast = KeyValueStringParser::parse($this->evaluate->parse($fe[FE_VALUE], ROW_EXPECT_0_1));
// Extract assigned tags: new
$tagNew = KeyValueStringParser::parse($this->store->getVar($fe[FE_NAME], STORE_FORM,
($fe[FE_CHECK_TYPE] == SANITIZE_ALLOW_AUTO) ? SANITIZE_ALLOW_ALNUMX : $fe[FE_CHECK_TYPE]));
// Get all tags who are new
$result = array_diff_assoc($tagNew, $tagLast);
// Create glue records
foreach ($result as $id => $value) {
$this->store->setVar(VAR_TAG_ID, $id, STORE_VAR);
$this->store->setVar(VAR_TAG_VALUE, $value, STORE_VAR);
if ($id == 0) {
if (empty($fe[FE_TYPEAHEAD_TAG_INSERT])) {
throw new \UserFormException("Missing 'typeAheadTagInsert'", ERROR_MISSING_REQUIRED_PARAMETER);
}
// Create tag
$id = $this->evaluate->parse($fe[FE_TYPEAHEAD_TAG_INSERT]);
$this->store->setVar(VAR_TAG_ID, $id, STORE_VAR);
}
// Create glue
$this->evaluate->parse($fe[FE_TYPEAHEAD_GLUE_INSERT]);
}
// Get all tags who has been removed
$result = array_diff_assoc($tagLast, $tagNew);
// Delete Glue records
foreach ($result as $id => $value) {
$this->store->setVar(VAR_TAG_ID, $id, STORE_VAR);
$this->store->setVar(VAR_TAG_VALUE, $value, STORE_VAR);
// Delete glue
$this->evaluate->parse($fe[FE_TYPEAHEAD_GLUE_DELETE]);
}
}
......
......@@ -11,6 +11,7 @@ namespace IMATHUZH\Qfq\Core\Store;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
......@@ -173,7 +174,8 @@ class FillStoreForm {
public function process($formMode = FORM_SAVE) {
// The following will never be used during load (fe.type='upload').
$skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER, FE_VALUE, FE_FILL_STORE_VAR];
$skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
FE_VALUE, FE_FILL_STORE_VAR, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT];
$html = '';
$newValues = array();
......@@ -292,45 +294,21 @@ class FillStoreForm {
$formElement[FE_MODE] === FE_MODE_SHOW ||
(isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] != '0')) {
$val = $clientValues[$clientFieldName];
if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
// TYPEAHEAD_TAG will be delivered as JSON. Check and sanitize every key/value pair.
$arr = json_decode($clientValues[$clientFieldName], true);
$arrTmp = array();
foreach ($arr as $row) {
// Trim input
if (empty($formElement[FE_TRIM])) {
$val = trim($val);
} elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) {
$val = trim($val, $formElement[FE_TRIM]);
}
switch ($formElement[FE_TYPE]) {
case FE_TYPE_DATE:
case FE_TYPE_DATETIME:
case FE_TYPE_TIME:
if ($clientValues[$clientFieldName] !== '') { // do not check empty values
$val = $this->doDateTime($formElement, $val);
}
break;
default:
if ($formElement[FE_TYPE] == FE_TYPE_EDITOR) {
// Tiny MCE always wrap a '<p>' around the content. Remove it before saving.
$val = Support::unWrapTag('<p>', $val);
}
// Check only if there is something.
if ($val !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
$val = Sanitize::sanitize($val, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN],
$formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? '');
if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
// $val = htmlspecialchars($val, ENT_QUOTES);
$val = Support::htmlEntityEncodeDecode(MODE_ENCODE, $val);
}
}
break;
}
$arrKey = $this->doValue($formElement, $formMode, $row['key']);
$arrValue = $this->doValue($formElement, $formMode, $row['value']);
$arrTmp[$arrKey] = $arrValue;
}
$val = KeyValueStringParser::unparse($arrTmp, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, true);
if ($val !== '') {
$val = Sanitize::checkMinMax($val, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION);
} else {
// Single Value
$val = $this->doValue($formElement, $formMode, $clientValues[$clientFieldName]);
}
$newValues[$formElement[FE_NAME]] = $val;
......@@ -342,6 +320,56 @@ class FillStoreForm {
}
/**
* @param $formElement
* @param $value
* @return string
* @throws \CodeException
* @throws \UserFormException
*/
private function doValue($formElement, $formMode, $value) {
// Trim input
if (empty($formElement[FE_TRIM])) {
$value = trim($value);
} elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) {
$value = trim($value, $formElement[FE_TRIM]);
}
switch ($formElement[FE_TYPE]) {
case FE_TYPE_DATE:
case FE_TYPE_DATETIME:
case FE_TYPE_TIME:
if ($value !== '') { // do not check empty values
$value = $this->doDateTime($formElement, $value);
}
break;
default:
if ($formElement[FE_TYPE] == FE_TYPE_EDITOR) {
// Tiny MCE always wrap a '<p>' around the content. Remove it before saving.
$value = Support::unWrapTag('<p>', $value);
}
// Check only if there is something.
if ($value !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
$value = Sanitize::sanitize($value, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN],
$formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? '');
if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
// $value = htmlspecialchars($value, ENT_QUOTES);
$value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
}
}
break;
}
if ($value !== '') {
$value = Sanitize::checkMinMax($value, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION);
}
return $value;
}
/**
* Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma
* separated string (MYSQL ENUM type). If there is no element '_h_${clientFieldName}', than there are no multi
......@@ -369,7 +397,6 @@ class FillStoreForm {
return $unchecked;
// For templateGroups: all expanded FormElements will be tried to collect - this fails for not submitted fields.
// Therefore skip not existing clientvalues.
if (!isset($clientValues[$checkboxKey])) {
......
......@@ -43,13 +43,29 @@ END;
#
DROP FUNCTION IF EXISTS QBAR;