Commit 96e16653 authored by bbaer's avatar bbaer

Merge branch 'develop' of git.math.uzh.ch:typo3/qfq into develop

parents cab4f7ae 284a9edc
......@@ -36,6 +36,41 @@ Features
Bug Fixes
^^^^^^^^^
Version 20.4.0
--------------
Date: 05.04.2020
Notes
^^^^^
* New Feature: `typeAheadTag <https://docs.typo3.org/p/IMATHUZH/qfq/master/en-us/Manual.html#type-ahead-tag>` - extend regular input with multiple values via typeAhead. _
* MySQL StoredProcedure:
* strip_tags() - Simple strip html tags.
* QCC() - Escape colon / coma. Useful for QFQ link arguments like 'text' or 'tooltip'.
Features
^^^^^^^^
* #9686 / Download: sanitize output filename.
* #10358 / Configure path/environment via QFQ config: qpdf, gs, pdfunite.
* #9517, #10145, #10177, #10117 / typeAheadTag.
* #10152 / QCC() - Stored Procedure to escape colon / coma.
* #10115 / TypeAhead: static list.
* FabricJS: replaced glyphicons with font awesome.
* Rename config.qfq.example.php config-example.qfq.php.
Bug Fixes
^^^^^^^^^
* #6798 / Close didn't worked with r=0.
* #10199 / Form.forwardMode: missing mode 'Close' / 'Auto'.
* #10173 / Dynamic Update: Readonly element can't be activated via dynamic update.
* Fix broken default value for Form.forwardMode.
* Fix problem with reporting broken TG-FormElements.
* Add error message if primary table does not exist.
Version 20.2.0
--------------
......@@ -71,8 +106,9 @@ Features
Bug Fixes
^^^^^^^^^
* #5869 / Table names not properly escaped.
* #10010 / FE.type=sendmail will now be fired together with fe.type=after* (not after).
* #5869 / Table names not properly escaped.
* #9638 / TextArea: Autosize - broken when using clipboard,
* Fixed problem with border showing when qfq-color-white is set.
* Fix selenium tests, remove chromedriver from npm.
* Log problem that crashes qfq when calendar dependencies are missing.
......
......@@ -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`
......
......@@ -17,36 +17,35 @@ Neue Versionsnummer
3) Die aktuellen Commits anschauen und wichtige Topics uebernehmen (git log > ~/qfq.log, alles bis zum letzten TAG anschauen):
* **All commits since last tag**:
* **All commits since last tag**:
git log $(git describe --tags --abbrev=0)..HEAD --oneline | cut -c9- > /tmp/out; pluma /tmp/out
* All commits since tag 'v19.12.0'
git log v19.12.0..HEAD --oneline
* complicated:
* complicated:
git log | grep -v -e '^commit ' -e '^Author: ' -e '^Date: ' -e '^Merge: ' > /tmp/out; pluma /tmp/out
* **Anpassen**: qfq/extension/Documentation/Release.rst
* Release.rst **verteilen**: make copyReleaseNotes
* Manuell:
* Den Inhalt von Release.rst kopieren nach qfq/extension/RELEASE.txt.
* Den Inhalt von Release.rst kopieren nach CHANGELOG.md.
* Tickets
* Alle offenen Tickets im aktuellen Milestones verschieben auf den naechsten Milestone.
* Abgeschlossene Tickets schliessen.
* Abgeschlossene Tickets schliessen.
4) In folgenden Files anpassen:
**Achtung**: die Release Minor darf KEINE fuehrenden Nullen enthalten!!! Ansonsten funktioniert die Verteilung vie TER nicht.
**Auto**: ./setVersion.sh 20.2.0
**Auto**: ./setVersion.sh 20.4.0
Manuell:
* extension/Documentation/_make/conf.py: release, version-
* Documentation/Settings.cfg: version
......@@ -59,20 +58,20 @@ Neue Versionsnummer
6) **Update Version & Commit**
* Update the version number in this document (topic 6)
* Commit & Push new version changes to master branch:
New version 20.2.0
* Commit & Push new version changes to master branch:
New version 20.4.0
7) **New Tag**:
7) **New Tag**:
git tag v20.2.0
git push -u origin v20.2.0
git tag v20.4.0
git push -u origin v20.4.0
8) **Merge 'master' into 'develop'**
9) Tickets:
9) Tickets:
* Schliessen und der QFQ Version zuweisen.
* Aktuellen Milestone schliessen
* Aktuellen Milestone schliessen
10) T3 Doc aktualisieren
......
......@@ -44,30 +44,28 @@ Quick Form Query Extension
**TYPO3**
The content of this document is related to TYPO3 CMS,
a GNU/GPL CMS/Framework available from `typo3.org
<https://typo3.org/>`_ .
The content of this document is related to TYPO3 CMS,
a GNU/GPL CMS/Framework available from `typo3.org
<https://typo3.org/>`_ .
**About this manual:**
This manual is a reference. Some basic examples are at the end.
This manual is a reference. Some basic examples are at the end.
**Community documentation:**
This document is *not* official TYPO3 documentation.
This document is *not* official TYPO3 documentation.
It is maintained as part of a third party extension.
It is maintained as part of a third party extension.
If you find an error or something is missing, please report
an `issue <https://project.math.uzh.ch/projects/qfq>`_
If you find an error or something is missing, please report
an `issue <https://project.math.uzh.ch/projects/qfq>`_
**Extension Manual**
This documentation is for the TYPO3 extension **qfq**.
This documentation is for the TYPO3 extension **qfq**.
**Sitemap:**
:ref:`sitemap`
:ref:`sitemap`
.. toctree::
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
......@@ -21,8 +21,8 @@
; you can use in 'conf.py'
project = QFQ - Quick Form Query
version = 20.2
release = 20.2.0
version = 20.4
release = 20.4.0
t3author = Carsten Rose
copyright = since 2017 by the author
......
......@@ -59,6 +59,7 @@ plantuml:
bootstrap: .npmpackages .plantuml_install .virtual_env
npm update
grunt default
# take care that phpOffice is located under 'qfq/Resources/Private/vendor/phpoffice'
# cd extension/Resources/Private; composer update
cd extension; composer update
......
......@@ -736,7 +736,9 @@ 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, FE_TYPEAHEAD_INITIAL_SUGGESTION];
// get current data record
$primaryKey = $this->formSpec[F_PRIMARY_KEY];
......@@ -1318,8 +1320,24 @@ 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, '.', '');
}
}
// Check if typeAhead[Tag] needs to build
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'
......@@ -1329,14 +1347,36 @@ abstract class AbstractBuildForm {
$formElement[FE_INPUT_AUTOCOMPLETE] = 'off'; // typeahead behaves better with 'autocomplete'='off'
}
// Collect typeAhead initial suggestion
if (!empty($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION])) {
// $formElement[FE_TYPEAHEAD_MINLENGTH] = 0; // If a suggestion is defined: minLength becomes automatically 0
$arr = $this->evaluate->parse($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION]);
$arr = $this->dbArray[$this->dbIndexData]->makeArrayDict($arr, TYPEAHEAD_SQL_KEY_NAME, API_TYPEAHEAD_VALUE, API_TYPEAHEAD_KEY, API_TYPEAHEAD_VALUE);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_INITIAL_SUGGESTION, json_encode($arr));
}
$class .= ' ' . CLASS_TYPEAHEAD;
$dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
$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.
$arr = KeyValueStringParser::parse($value, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, KVP_IF_VALUE_EMPTY_COPY_KEY);
$value = OnArray::arrayToQfqJson($arr);
}
}
if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
......@@ -1364,26 +1404,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 +1449,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 +1473,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 +1485,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 +1531,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 +1593,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.
......
......@@ -644,10 +644,13 @@ const SYSTEM_ENTER_AS_SUBMIT = 'enterAsSubmit';
const SYSTEM_SHOW_ID_IN_FORM_TITLE = 'showIdInFormTitle';
const SYSTEM_CMD_WKHTMLTOPDF = 'cmdWkhtmltopdf';
// Thumbnail
const SYSTEM_CMD_INKSCAPE = 'cmdInkscape';
const SYSTEM_CMD_CONVERT = 'cmdConvert';
const SYSTEM_CMD_QPDF = 'cmdQpdf';
const SYSTEM_CMD_GS = 'cmdGs';
const SYSTEM_CMD_PDFUNITE = 'cmdPdfunite';
// Thumbnail
const SYSTEM_THUMBNAIL_DIR_SECURE = 'thumbnailDirSecure';
const SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT = 'fileadmin/protected/qfqThumbnail';
const SYSTEM_THUMBNAIL_DIR_PUBLIC = 'thumbnailDirPublic';
......@@ -728,7 +731,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 +862,9 @@ 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_INITIAL_SUGGESTION = 'data-typeahead-initial-suggestion';
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';
......@@ -867,8 +874,6 @@ const CLASS_FORM_ELEMENT_EDIT = 'qfq-form-element-edit';
const CLASS_FORM_ELEMENT_AUTO_GROW = 'qfq-auto-grow';
const ATTRIBUTE_DATA_MAX_HEIGHT = 'data-max-height';
// BuildForm
const SYMBOL_NEW = 'new';
const SYMBOL_EDIT = 'edit';
......@@ -1237,6 +1242,13 @@ 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_INITIAL_SUGGESTION = 'typeAheadInitialSuggestion';
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 +1744,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';
......
......@@ -8,13 +8,13 @@
namespace IMATHUZH\Qfq\Core\Database;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\BindParam;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Store\Store;
/**
* Class Database
......@@ -932,10 +932,12 @@ class Database {
}
/**
* $arr will be converted to a two column array with keys $keyName1 and $keyName2.
* $arr might contain one or more columns.
* Only when $keyName1 and $keyName2 exist, those will be used. Else the first column becomes $keyName1 and the
* second becomes $keyName2. If there is only one column, that column will be doubled.
* $arr = [ 0 => [ $srcColumn1 => $value0_1, $srcColumn2 => $value0_2 ], 1 => [ $srcColumn1 => $value1_1, $srcColumn2 => $value1_2 ], ...]
*
* $arr will be converted to a two column array with keys $destColumn1 and $destColumn2.
* If $destColumn1 or $destColumn2 is empty, take $srcColumn1, $srcColumn2 as names.
* $arr might contain one or more columns. Only the first two columns are used.
* If there is only one column, that column will be doubled.
*
* @param array $arr
* @param string $srcColumn1
......@@ -947,6 +949,11 @@ class Database {
*/
public function makeArrayDict(array $arr, $srcColumn1, $srcColumn2, $destColumn1 = '', $destColumn2 = '') {
if ($arr == array() || $arr === null) {
return array();
}
// Set defaults
if ($destColumn1 == '') {
$destColumn1 = $srcColumn1;
}
......@@ -955,12 +962,7 @@ class Database {
$destColumn2 = $srcColumn2;
}
$new = array();
if ($arr == array() || $arr === null) {
return array();
}
// Set final column names
$row = $arr[0];
$keys = array_keys($row);
if (count($row) < 2) {
......@@ -974,9 +976,11 @@ class Database {
$column2 = $keys[1];
}
$new = array();
$row = array_shift($arr);
while (null !== $row) {
$new[] = [$destColumn1 => $row[$column1], $destColumn2 => $row[$column2]];
// $new[] = [$destColumn1 => htmlentities($row[$column1], ENT_QUOTES), $destColumn2 => htmlentities($row[$column2], ENT_QUOTES)];
$row = array_shift($arr);
}
......@@ -1028,8 +1032,7 @@ class Database {
try {
$this->playMultiQuery($query);
}
catch (\CodeException $e) {
} catch (\CodeException $e) {
throw new \CodeException("Error playing $filename", ERROR_PLAY_SQL_FILE);
}
......
......@@ -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
......
......@@ -715,8 +715,10 @@ EOF;
case FE_MODE_SHOW_REQUIRED:
break;
case FE_MODE_REQUIRED:
$attribute .= Support::doAttribute('required', 'required');
break;
case FE_MODE_READONLY:
$attribute .= Support::doAttribute($feMode, $feMode);
$attribute .= Support::doAttribute('disabled', 'disabled');
break;
default:
throw new \UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
......
......@@ -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;
}
......
......@@ -427,4 +427,27 @@ class OnArray {
public static function getMd5(array $data) {
return md5(implode($data));
}
/**
* Converts a one dimensional array to JSON array. The 'key' and 'value' will names are hardcoded:
*
* Return: [ { 'key': $key[0], 'value': $value[0] }, { 'key': $key[1], 'value': $value[2] }, ... ]
*
* @param array $arr
* @param bool $flagHtmlEntity true|false
* @return string
*/
public static function arrayToQfqJson(array $arr, $flagHtmlEntity = false) {
$json = '';
foreach ($arr as $arrKey => $arrValue) {
if ($flagHtmlEntity) {
$arrKey = htmlentities($arrKey, ENT_QUOTES);
$arrValue = htmlentities($arrValue, ENT_QUOTES);
}
$json .= ',' . json_encode(["key" => $arrKey, "value" => $arrValue]);
}
return '[' . substr($json, 1) . ']';
}
}
\ No newline at end of file
......@@ -292,8 +292,8 @@ class Support {
*
* TinyMCE: Encoding JS Attributes (keys & values) for TinyMCE needs to be encapsulated in '&quot;' instead of '\"'.
*
* @param $str
* @param string $modeEscape
* @param string $str
* @param string $modeEscape ESCAPE_WITH_BACKSLASH | ESCAPE_WITH_HTML_QUOTE
*
* @return string
* @throws \CodeException
......
......@@ -550,9 +550,6 @@ class QuickFormQuery {
$rc = $save->process();
// Reload fresh saved record and fill STORE_RECORD with it.
$this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
$save->processAllUploads($rc);
// Action: After*, Sendmail
......@@ -572,17 +569,16 @@ class QuickFormQuery {
break;
}
$customForward = $this->setForwardModePage();
$this->setForwardModePage();
// Logic: If a) r=0 and
// b) User presses only 'save' (not save & close) and
// c) there is no forwardMode=='url...'
// b) final: (forwardMode=='auto' and User presses only 'save' (not 'save & close')) OR (forwardMode=='no')
// then the client should reload the current page with the newly created record. A new SIP is necessary!
$getJson = true;
if (0 == $this->store->getVar(SIP_RECORD_ID, STORE_SIP) &&
API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX) &&
$customForward == false
) {
if (0 == $this->store->getVar(SIP_RECORD_ID, STORE_SIP)
&& (($this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_AUTO
&& API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX)
) || $this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_NO)) {
$this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc);
$getJson = false;
}
......@@ -791,12 +787,10 @@ class QuickFormQuery {
*
* '$this->formSpec[F_FORWARD_PAGE]':
* a) url http://www.nzz.ch/index.html?a=123#bottom, website.html?a=123#bottom,
* ?[id=]<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
* ?[id=]<T3 Alias pageId>&a=123#bottom, ?id=<T3 pageId>&a=123#bottom
* b) mode no|client|url|...
* c) mode|url combination of above
*
* @return bool TRUE if F_FORWARD_MODE = 'url..', else FALSE
*
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
......@@ -805,7 +799,7 @@ class QuickFormQuery {
private function setForwardModePage() {
if (F_FORWARD_MODE_URL != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
return false;
return;
}
$forwardPageTmp = $this->evaluate->parse($this->formSpec[F_FORWARD_PAGE]);
......@@ -835,20 +829,11 @@ class QuickFormQuery {
break;
}
if ('url' == substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
if (F_FORWARD_MODE_URL == substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
if ($this->formSpec[F_FORWARD_PAGE] == '') {
$this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
$customForward = false;
} else {
$customForward = true;
}