Commit 612fc161 authored by bbaer's avatar bbaer

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

parents 5f416718 69cf59aa
Pipeline #3133 passed with stages
in 1 minute and 54 seconds
......@@ -9,7 +9,7 @@ variables:
stages:
- before
- build
- selenium
# - selenium
documentation:
stage: before
......@@ -48,29 +48,29 @@ release:
- scp qfq_${VERSION}_*.zip w16:qfq/releases/
- mv qfq_${VERSION}_*.zip build/qfq.zip
selenium:
stage: selenium
script:
- unzip -q build/qfq.zip -d qfq
- cd docker/
- ./run_qfq_docker.sh -no-deploy
- ./deploy_to_container.sh ../qfq
- ./run_selenium_tests_docker.sh
- echo "hello"
after_script:
# remove containers and move logs to persistent location
- cd docker; ./remove-containers.sh <<< "y"
- cd ..
- umask 002
- mkdir "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA"
- cp extension/Tests/selenium/selenium_logs/* "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
- echo "Selenium Logs copied to $SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
- echo "Or download result (log/screenshot) in gitlab under CI/CD > Pipelines <job> > right side 'Artifacts'"
# selenium:
# stage: selenium
# script:
# - unzip -q build/qfq.zip -d qfq
# - cd docker/
# - ./run_qfq_docker.sh -no-deploy
# - ./deploy_to_container.sh ../qfq
# - ./run_selenium_tests_docker.sh
# - echo "hello"
# after_script:
# # remove containers and move logs to persistent location
# - cd docker; ./remove-containers.sh <<< "y"
# - cd ..
# - umask 002
# - mkdir "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA"
# - cp extension/Tests/selenium/selenium_logs/* "$SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
# - echo "Selenium Logs copied to $SELENIUM_LOGS_PATH/$CI_COMMIT_SHORT_SHA/"
# - echo "Or download result (log/screenshot) in gitlab under CI/CD > Pipelines <job> > right side 'Artifacts'"
artifacts:
expire_in: 1 week
paths:
- extension/Tests/selenium/selenium_logs/
# artifacts:
# expire_in: 1 week
# paths:
# - extension/Tests/selenium/selenium_logs/
......@@ -36,10 +36,37 @@ Features
Bug Fixes
^^^^^^^^^
Date: 09.01.2020
Notes
^^^^^
* Deprecated: Form.parameter.mode. Use Form.parameter.formModeGlobal
Features
^^^^^^^^
* #9805 / Form.parameter.activateFirstRequiredTab.
* #9858 / Form.parameter: replace 'mode' by 'formModeGlobal'
* #9860 / SQL function qmore(): change text '...' to '[...]'.
* Update Developer doc for record locking.
* Mockup for error handling.
Bug Fixes
^^^^^^^^^
* #7925 / Error in split PDF file during upload. Fix the cwd error in Logger.
* #9789 / Record lock release to early on 'leave page'. QfqJS: Moved release lock to before unload.
* #9861 / Fix problem with broken sql.log filename.
* #8851 / Revert implementation: LogMode 'modify' vs. 'modifyAll'.
* #9859 / Database Update: check for 'Update specialColumnName needed' breaks new QFQ install.
* #9813 / During QFQ database update, skip errors like 'Error 1060 - Duplicate Column'.
* Manual.rst: Fix various broken table layouts.
Version 19.12.0
---------------
Date: <date>
Date: 17.12.2019
Notes
^^^^^
......@@ -72,7 +99,6 @@ Bug Fixes
* #7974 / TinyMCE: ReadOnly.
* #9424 / modeSql: skip if it starts with '#'.
* #9531 / File Upload Required.
* #9674 / Select Required Dynamic Update.
* #9678 / textarea now trigger DynamicUpdate.
* #9679 / FormModeGlobal: add STORE_USER - system wide readonly.
......@@ -83,6 +109,7 @@ Bug Fixes
* #9733 / Identiy different tabs. Record lock for same tab will always be granted.
* #9734 / Fix 'dirty lock release' - leaving a dirty form without closing, leaves a stale lock record. Added a releaselock() before window.unload. Dirty remove on goBack.
* #9735 / File Delete: no dirty trigger.
* Download / PDF merge: skip leading errors, interpret only 'Could not merge encrypted files'.
* DragAndDrop broken: after refactoring Support.php, the dragAndDrop was broken - missed init of '$store'.
......
......@@ -11,15 +11,20 @@ Neue Versionsnummer
0) Fuer jede neue Version ein Ticket erstellen. Template: #6994
1) **Laufen** die Unit Tests durch?
1) Alle offenen Branches auf **Develop** und dann auf **Master mergen**.
2) Die aktuellen Commits anschauen und wichtige Topics uebernehmen (git log > ~/qfq.log, alles bis zum letzten TAG anschauen):
# complicated: git log | grep -v -e '^commit ' -e '^Author: ' -e '^Date: ' -e '^Merge: ' > /tmp/out; pluma /tmp/out
# Zeigt **alle Commits** an, die seit dem aendern von NewVersion gemacht wurden! Das sollten alle Commits seit der letzten Version sein.
* git log --pretty=%s --after="`stat -c %y doc/NewVersion.md`"
* **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:
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
......@@ -54,12 +59,12 @@ Neue Versionsnummer
* Update the version number in this document (topic 6)
* Commit & Push new version changes to master branch:
New version 19.12.0
New version 20.1.1
6) **New Tag**:
git tag v19.12.0
git push -u origin v19.12.0
git tag v20.1.1
git push -u origin v20.1.1
7) Tickets:
* Schliessen und der QFQ Version zuweisen.
......
......@@ -484,6 +484,9 @@ Server Response
### Record lock
Request, extend or release a lock for a record, identified by the SIP. The SIP contain a SIP_FORM and a SIP_R (record id).
See `Documentation-develop/diagram` for a state diagram.
To detect record change at time of 'record lock' or 'record save', a MD5 hash is provided from the server
and needs to pass back to dirty.php as well.
......@@ -497,6 +500,7 @@ URL Parameters
: `s=<SIP>` (form, r)
: `action=lock`, `action=extend`, `action=release>`
: `recordHashMd5=<value of hidden form element 'recordHashMd5'>`
: `tabUniqId=<unique id>`
Server Response
: The response contains an [Lock Response].
......@@ -509,7 +513,7 @@ JSON Response from the server (extended [Minimal Response]) containing:
: {
: "status": "success"|"error"|"conflict"|"conflict_allow_force",
: "message": "<message>"e5
: }
: }
`status` indicates how the request has been fulfilled by the server.
On`"success"`, the Client display nothing to the user.
......@@ -520,11 +524,18 @@ On `"conflict_allow_force"` the Client opens the alert non-modal (default).
### tabUniqId
Every tab get's a uniq id (timestamp) on page load: window.name.
This 'tabUniqId' is saved in dirty record on lock acquire.
On page reload, when the 'release' comes after 'acquire' (unwished async behaviour), the locking is skipped (if same user
session) - on reload there is no variable 'tabUniqId'. On real lock acquire, the tab ID is compared and will be denied
if not matching. The 'tabUniqId' might not work in IE - doesn't matter: it's a seldom special situation.
* Client: Every browser tab get's a `tabUniqId` (timestamp) as soon as the first time a lock is requested (acquire).
This `tabUniqId` remains, until the tab is close. It is saved in window.name.
* Server: On lock request, the `tabUniqId` is saved in dirty record.
* The `tabUniqId` helps to implement 'optimistic locking': if there is no argument against 'grant lock' - grant it.
Example: In browser tab 'A' a lock is requested. For unknown reason, there is a stale lock for the
user session and the given tab id: grant the log.
* Unexpected async behaviour in client: On page reload (F5), the 'lock release' might come after 'lock acquire'.
The form goes in 'read only' mode - an additional page reload (F5) solves the situation - but this is boring.
### Drag And Drop (sort)
......
<!-- -*- markdown -*- -->
# Record locking
## Concept: Late locking
* A lock is required on first modification.
* Multiple forms might open the same record, all seems to have write access. The first one who modifies the record
get the lock, all following will switch to form=readonly on their first try to modify the record.
## Lock mode: Exclusive
* A lock can't be overwritten.
## Lock mode: Advisory
* A lock can be ignored.
* Last save win's.
## Lock mode: None
* No locking at all.
# Workarounds
* At least one Browser (FF 71, maybe other in the future too), do not allow to wrap the 'leave page' dialog anymore.
This might result in stale lock files (modified record, click on browser tab close or any link), cause the lock
logic does not know that the user leaves the page.
* Workaround: before 'do you want to leave the page' appears, the lock is released, independent if the user answers 'no'.
As soon as the users modifies the record again, a new lock is acquired. This is better than a stale record lock.
* Reload a page (F5) on a modified record, opens the form in readonly mode (record lock found).
* Reason: the lock release is fired by the browser AFTER form load - than the lock-logic reports 'record is already locked'.
* Workaround: with the above workaround, this does not happen anymore. Nevertheless, a 'tabUniqId' has been implemented.
That one is saved as record lock and a page reload origin can be identified as the same tab as where the lock has been
acquired.
= State Diagram =
See `Documentation-develop/diagram` for a state diagram.
This diff is collapsed.
......@@ -22,7 +22,7 @@
Release
=======
Version 19.x.x
Version 20.x.x
--------------
Date: <date>
......@@ -36,6 +36,47 @@ Features
Bug Fixes
^^^^^^^^^
Version 20.1.1
--------------
Date: 13.01.2020
Bug Fixes
^^^^^^^^^
* #7705 / Fix problem with wrong value after save and form update.
* #8587 / A form triggers a save only, if there are real table columns.
Version 20.1.0
--------------
Date: 09.01.2020
Notes
^^^^^
* Deprecated: Form.parameter.mode. Use Form.parameter.formModeGlobal
Features
^^^^^^^^
* #9805 / Form.parameter.activateFirstRequiredTab.
* #9858 / Form.parameter: replace 'mode' by 'formModeGlobal'
* #9860 / SQL function qmore(): change text '...' to '[...]'.
* Update Developer doc for record locking.
* Mockup for error handling.
Bug Fixes
^^^^^^^^^
* #7925 / Error in split PDF file during upload. Fix the cwd error in Logger.
* #9789 / Record lock release to early on 'leave page'. QfqJS: Moved release lock to before unload.
* #9861 / Fix problem with broken sql.log filename.
* #8851 / Revert implementation: LogMode 'modify' vs. 'modifyAll'.
* #9859 / Database Update: check for 'Update specialColumnName needed' breaks new QFQ install.
* #9813 / During QFQ database update, skip errors like 'Error 1060 - Duplicate Column'.
* Manual.rst: Fix various broken table layouts.
Version 19.12.0
---------------
......@@ -72,7 +113,6 @@ Bug Fixes
* #7974 / TinyMCE: ReadOnly.
* #9424 / modeSql: skip if it starts with '#'.
* #9531 / File Upload Required.
* #9674 / Select Required Dynamic Update.
* #9678 / textarea now trigger DynamicUpdate.
* #9679 / FormModeGlobal: add STORE_USER - system wide readonly.
......
......@@ -21,8 +21,8 @@
; you can use in 'conf.py'
project = QFQ - Quick Form Query
version = 19.12
release = 19.12.0
version = 20.1
release = 20.1.1
t3author = Carsten Rose
copyright = since 2017 by the author
......
......@@ -31,6 +31,7 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
*/
public function showAction() {
$html = '';
$origErrorReporting = '';
$flagOk = false;
......
......@@ -9,6 +9,7 @@
namespace IMATHUZH\Qfq\Core;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Form\Checkbox;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
......@@ -21,7 +22,6 @@ use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Report\Report;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Form\Checkbox;
/**
* Class AbstractBuildForm
......@@ -605,6 +605,7 @@ abstract class AbstractBuildForm {
$attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
$attribute['enctype'] = $this->getEncType();
$attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
$attribute['data-activate-first-required-tab'] = $this->formSpec[F_ACTIVATE_FIRST_REQUIRED_TAB] == '1' ? "true" : "false"; // attribute meaning is inverted
return $attribute;
}
......@@ -762,6 +763,10 @@ abstract class AbstractBuildForm {
continue; // skip this FE
}
// #7705. Dirty fix: 'form_save and fresh created tg element can't be updated, cause the HTML id is unknown: skip those.
if ($mode == FORM_SAVE && false != stripos($fe[FE_NAME], '%d')) {
continue; // skip this FE
}
$flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form.
$debugStack = array();
......@@ -2301,8 +2306,8 @@ abstract class AbstractBuildForm {
];
// Inherit current F_MODE
if ($this->formSpec[F_MODE] != '') {
$queryStringArray[F_MODE_GLOBAL] = $this->formSpec[F_MODE];
if ($this->formSpec[F_MODE_GLOBAL] != '') {
$queryStringArray[F_MODE_GLOBAL] = $this->formSpec[F_MODE_GLOBAL];
}
// In case the subrecord FE is set to 'readonly': subforms will be called with formModeGlobal=readonly
......@@ -3666,9 +3671,7 @@ EOT;
* '{{!SELECT ...' statement, that one will be fired. In case of an non-primary FE, the result array are the
* values for the copies of the specific FE.
*
* Additional the maximum count of all select rows will be determined and returned.
*
* @return int max number of records in FormElement[FE_VALUE] over all FormElements.
* @return int Count of records in FormElement[FE_VALUE] over all FormElements.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
......
......@@ -586,6 +586,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public function getFormTag() {
......
......@@ -783,7 +783,6 @@ const RANDOM_LENGTH = 32;
// SQL logging Modes
const SQL_LOG_MODE_ALL = 'all';
const SQL_LOG_MODE_MODIFY = 'modify';
const SQL_LOG_MODE_MODIFY_ALL = 'modifyAll';
const SQL_LOG_MODE_NONE = 'none';
const SQL_LOG_MODE_ERROR = 'error';
......@@ -1011,7 +1010,6 @@ const F_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch';
const F_TYPEAHEAD_LDAP_SEARCH_PREFETCH = 'typeAheadLdapSearchPrefetch';
const F_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN = 'typeAheadLdapSearchPerToken';
const F_MODE = 'mode';
const F_MODE_READONLY = 'readonly';
const F_MODE_REQUIRED_OFF = 'requiredOff';
const F_MODE_REQUIRED_OFF_BUT_MARK = 'requiredOffButMark';
......@@ -1046,6 +1044,7 @@ const F_NEW_BUTTON_CLASS = SYSTEM_NEW_BUTTON_CLASS;
const F_NEW_BUTTON_GLYPH_ICON = SYSTEM_NEW_BUTTON_GLYPH_ICON;
const F_ENTER_AS_SUBMIT = SYSTEM_ENTER_AS_SUBMIT;
const F_ACTIVATE_FIRST_REQUIRED_TAB = 'activateFirstRequiredTab';
const F_DRAG_AND_DROP_ORDER_SQL = 'dragAndDropOrderSql';
const F_ORDER_INTERVAL = 'orderInterval';
......@@ -1371,6 +1370,7 @@ const QUERY_TYPE_SELECT = 'type: select,show,describe,explain';
const QUERY_TYPE_INSERT = 'type: insert';
const QUERY_TYPE_UPDATE = 'type: update,replace,delete';
const QUERY_TYPE_CONTROL = 'type: set';
const QUERY_TYPE_FAILED = 'type: query failed';
//Regexp
//const REGEXP_DATE_INT = '^\d{4}-\d{2}-\d{2}$';
......@@ -1815,7 +1815,7 @@ const DIRTY_API_ACTION_EXTEND = 'extend';
const LOCK_NOT_FOUND = 0;
const LOCK_FOUND_OWNER = 1;
const LOCK_FOUND_CONFLICT = 2;
const TAB_UNIQ_ID = 'tabUniqId'; // Currently only only a uniq identifier: no values stored behind the identifier - might change.
const TAB_UNIQ_ID = 'tabUniqId'; // Uniq identifier per tab: no values stored behind the identifier - might change.
// AutoCron
const AUTOCRON_MAX_AGE_MINUTES = 10;
......
......@@ -50,7 +50,7 @@ class Database {
/**
* @var array
*/
private $sqlLogModePrio = [SQL_LOG_MODE_NONE => 1, SQL_LOG_MODE_ERROR => 2, SQL_LOG_MODE_MODIFY => 3, SQL_LOG_MODE_MODIFY_ALL => 4, SQL_LOG_MODE_ALL => 5];
private $sqlLogModePrio = [SQL_LOG_MODE_NONE => 1, SQL_LOG_MODE_ERROR => 2, SQL_LOG_MODE_MODIFY => 3, SQL_LOG_MODE_ALL => 4];
private $dbName = '';
......@@ -181,6 +181,7 @@ class Database {
* @param string $specificMessage
* @param array $keys
* @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS
* @param array $skipErrno
*
* @return array|int
* SELECT | SHOW | DESCRIBE | EXPLAIN: see $mode
......@@ -190,7 +191,7 @@ class Database {
* @throws \DbException
* @throws \UserFormException
*/
public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '', array &$keys = array(), array &$stat = array()) {
public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '', array &$keys = array(), array &$stat = array(), array $skipErrno = array()) {
$queryType = '';
$result = array();
$this->closeMysqliStmt();
......@@ -205,7 +206,7 @@ class Database {
$specificMessage .= " ";
}
$count = $this->prepareExecute($sql, $parameterArray, $queryType, $stat, $specificMessage);
$count = $this->prepareExecute($sql, $parameterArray, $queryType, $stat, $specificMessage, $skipErrno);
if ($count === false) {
throw new \DbException($specificMessage . "No idea why this error happens - please take some time and check the problem.", ERROR_DB_GENERIC_CHECK);
......@@ -339,23 +340,27 @@ class Database {
* Returns the number of selected rows (SELECT, SHOW, ..) or the affected rows (UPDATE, INSERT). $stat contains
* appropriate num_rows, insert_id or rows_affected.
*
* In case of an error, throw an exception.
* mysqli error code listed in $skipErrno[] do not throw an error.
*
* @param string $sql SQL statement with prepared statement variable.
* @param array $parameterArray parameter array for prepared statement execution.
* @param string $queryType returns QUERY_TYPE_SELECT | QUERY_TYPE_UPDATE | QUERY_TYPE_INSERT, depending on
* the query.
* @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS
*
* @param string $specificMessage
* @param array $skipErrno
*
* @return int|mixed
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function prepareExecute($sql, array $parameterArray, &$queryType, array &$stat, $specificMessage = '') {
private function prepareExecute($sql, array $parameterArray, &$queryType, array &$stat, $specificMessage = '', array $skipErrno = array()) {
$sqlLogMode = $this->isSqlModify($sql) ? SQL_LOG_MODE_MODIFY : SQL_LOG_MODE_ALL;
$errno = 0;
// Only log a modify type statement here if sqlLogMode is (at least) modifyAll
// If sqlLogMode is modify, log the statement after it has been executed and we know if there are affected rows.
$sqlLogMode = $this->isSqlModify($sql) ? SQL_LOG_MODE_MODIFY_ALL : SQL_LOG_MODE_ALL;
$result = 0;
$stat = array();
$errorMsg[ERROR_MESSAGE_TO_USER] = empty($specificMessage) ? 'SQL error' : $specificMessage;
......@@ -372,34 +377,52 @@ class Database {
$this->dbLog($sqlLogMode, $sql, $parameterArray);
if (false === ($this->mysqli_stmt = $this->mysqli->prepare($sql))) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_PREPARE);
if ($skipErrno === array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_PREPARE);
} else {
$errno = $this->mysqli->errno;
}
}
if (count($parameterArray) > 0) {
if (false === $this->prepareBindParam($parameterArray)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
if ($skipErrno !== array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_BIND);
throw new \DbException(json_encode($errorMsg), ERROR_DB_BIND);
} else {
$errno = $this->mysqli->errno;
}
}
}
if (false === $this->mysqli_stmt->execute()) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
if ($skipErrno !== array() && false === array_search($this->mysqli->errno, $skipErrno)) {
$this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray);
$errorMsg[ERROR_MESSAGE_TO_DEVELOPER] = $this->getSqlHint($sql, $this->mysqli->error);
$errorMsg[ERROR_MESSAGE_OS] = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
throw new \DbException(json_encode($errorMsg), ERROR_DB_EXECUTE);
throw new \DbException(json_encode($errorMsg), ERROR_DB_EXECUTE);
} else {
$errno = $this->mysqli->errno;
}
}
$msg = '';
$count = 0;
$command = strtoupper(explode(' ', $sql, 2)[0]);
if ($errno === 0) {
$command = strtoupper(explode(' ', $sql, 2)[0]);
} else {
$command = 'FAILED';
}
switch ($command) {
case 'SELECT':
case 'SHOW':
......@@ -444,6 +467,12 @@ class Database {
$count = $stat[DB_AFFECTED_ROWS];
$msg = '';
break;
case 'FAILED':
$queryType = QUERY_TYPE_FAILED;
$stat[DB_AFFECTED_ROWS] = 0;
$count = -1;
$msg = '[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error;
break;
default:
// Unknown command: treat it as a control command
......@@ -458,18 +487,7 @@ class Database {
$this->store->setVar(SYSTEM_SQL_COUNT, $count, STORE_SYSTEM);
}
// Logfile
$pageContentSqlLogMode = $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM);
if ($pageContentSqlLogMode == SQL_LOG_MODE_MODIFY && $sqlLogMode == SQL_LOG_MODE_MODIFY_ALL) {
// sqlLogMode modify: need to log query and query result (if count > 0)
if ($count > 0) {
$this->dbLog(SQL_LOG_MODE_MODIFY, $sql, $parameterArray);
$this->dbLog(SQL_LOG_MODE_MODIFY, $msg);
}
} else {
// Query result
$this->dbLog($sqlLogMode, $msg);
}
$this->dbLog($sqlLogMode, $msg);
return $count;
}
......
......@@ -137,10 +137,6 @@ class DatabaseUpdate {
$versionInfo = $this->getDatabaseVersion();
$old = $versionInfo[QFQ_VERSION_KEY] ?? false;
if (version_compare($old, '19.9.0') === -1) {
$this->updateSpecialColumns();
}
if ($dbUpdate === SYSTEM_DB_UPDATE_ALWAYS || ($dbUpdate === SYSTEM_DB_UPDATE_AUTO && $new != $old)) {
$newFunctionHash = $this->updateSqlFunctions($versionInfo[QFQ_VERSION_KEY_FUNCTION_HASH] ?? '');
......@@ -164,6 +160,11 @@ class DatabaseUpdate {
// A complete new installation get's some extra tables
$this->db->playSqlFile(__DIR__ . '/../../Sql/customTable.sql');
}
if (version_compare($old, '19.9.0') === -1) {
$this->updateSpecialColumns();
}
}
/**
......@@ -373,6 +374,9 @@ class DatabaseUpdate {
* @throws \UserFormException
*/
private function dbUpdateStatements($old, $new) {
$dummy = array();
if ($new == '' || $old === false || $old === null) {
return;
}
......@@ -391,7 +395,9 @@ class DatabaseUpdate {
if ($apply) {
// Play Statements
foreach ($sqlStatements as $sql) {
$this->db->sql($sql, ROW_REGULAR, array(), "Apply updates to QFQ database. Installed version: $old. New QFQ version: $new");
$this->db->sql($sql, ROW_REGULAR, array(),
"Apply updates to QFQ database. Installed version: $old. New QFQ version: $new",
$dummy, $dummy, [1060] /* duplicate column name */);
}
// Remember already applied updates - in case something breaks and the update has to be repeated.
$this->setDatabaseVersion($new);
......
......@@ -146,7 +146,7 @@ class Dirty {
}
/**
* Tries to get a 'DirtyRecord'. Returns an array (becomes JSON) about success or failure.
* Tries to get a lock ('dirty record'). Returns an array (becomes JSON) about success or failure.
*
* @param int $recordId
* @param array $tableVars
......
......@@ -855,8 +855,8 @@ class Support {
$formElement[FE_MODE] = $formElement[FE_MODE_SQL];
}
if (isset($formSpec[F_MODE])) {
$formElement[FE_MODE] = self::applyFormModeToFormElement($formElement[FE_MODE], $formSpec[F_MODE]);
if (isset($formSpec[F_MODE_GLOBAL])) {
$formElement[FE_MODE] = self::applyFormModeToFormElement($formElement[FE_MODE], $formSpec[F_MODE_GLOBAL]);
}
// set typeAheadPedantic
......
......@@ -448,7 +448,7 @@ class QuickFormQuery {
$recordDirty = array();
$rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg);
if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) {
$this->formSpec[F_MODE] = F_MODE_READONLY;
$this->formSpec[F_MODE_GLOBAL] = F_MODE_READONLY;
}
}
......@@ -1427,6 +1427,7 @@ class QuickFormQuery {
* @return array
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
private function initForm(array $formSpec, $recordId) {
......@@ -1434,7 +1435,6 @@ class QuickFormQuery {
Support::setIfNotSet($formSpec, F_SUBMIT_BUTTON_TEXT, '');
Support::setIfNotSet($formSpec, F_BUTTON_ON_CHANGE_CLASS, '');
Support::setIfNotSet($formSpec, F_LDAP_USE_BIND_CREDENTIALS, '');
Support::setIfNotSet($formSpec, F_MODE, '');
Support::setIfNotSet($formSpec, F_DB_INDEX, $this->store->getVar(F_DB_INDEX, STORE_SYSTEM));
Support::setIfNotSet($formSpec, F_ENTER_AS_SUBMIT, $this->store->getVar(SYSTEM_ENTER_AS_SUBMIT, STORE_SYSTEM));
Support::setIfNotSet($formSpec, F_SESSION_TIMEOUT_SECONDS, $this->store->getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM));
......@@ -1442,6 +1442,7 @@ class QuickFormQuery {
Support::setIfNotSet($formSpec, F_MULTI_MSG_NO_RECORD, F_MULTI_MSG_NO_RECORD_TEXT);
Support::setIfNotSet($formSpec, F_FE_MIN_WIDTH, F_FE_MIN_WIDTH_DEFAULT);
Support::setIfNotSet($formSpec, FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH, FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH_DEFAULT);
Support::setIfNotSet($formSpec, F_ACTIVATE_FIRST_REQUIRED_TAB, 1);
// In case there is no F_MODE defined on the form, check if there is one in STORE_SIP.
// if ($formSpec[F_MODE] == '') {
......@@ -1451,10 +1452,18 @@ class QuickFormQuery {
// }
// }
//
$formSpec[F_MODE] = Support::getFormModeGlobal($formSpec[F_MODE]);
// Check for deprecated legacy code
if (isset($formSpec['mode'])) {
throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Outdated form definition',
ERROR_MESSAGE_TO_DEVELOPER => "form.parameter.mode is deprecated. Please use form.parameter.formModeGlobal instead."
]));
}
// Unify F_MODE_GLOBAL
$formSpec[F_MODE_GLOBAL] = Support::getFormModeGlobal($formSpec[F_MODE_GLOBAL] ?? '');
if ($formSpec[F_MODE] == F_MODE_READONLY) {
if ($formSpec[F_MODE_GLOBAL] == F_MODE_READONLY) {
$formSpec[F_SHOW_BUTTON] = FORM_BUTTON_CLOSE;
$formSpec[F_SUBMIT_BUTTON_TEXT] = '';
}
......
......@@ -71,7 +71,7 @@ class Save {
$this->evaluate = new Evaluate($this->store, $this->db);
$this->formAction = new FormAction($formSpec, $this->db);
$this->