Commit 700dd79d authored by Carsten  Rose's avatar Carsten Rose
Browse files

Feature #3981 / Record Locking

Manual.rst: add documentation for record locking
DatabaseUpdateData.php: Add new column 'dirtyMode'
Dirty.php, Config.php, formEditor.sql: remove dirtyMode=readonly. Rename 'timeout' to 'exclusive' and 'overwrite' to 'advisory'.
parent 1393edab
......@@ -414,7 +414,8 @@ Example: *typo3conf/config.qfq.ini*
; auto | always | never
;DB_UPDATE = auto
;DIRTY_RECORD_TIMEOUT_SECONDS_DEFAULT = 900
;RECORD_LOCK_TIMEOUT_SECONDS = 900
; Local Documentation (doc fits to installed version): typo3conf/ext/qfq/Documentation/html/Manual.html
;DOCUMENTATION_QFQ = https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html
......@@ -3307,6 +3308,29 @@ To automatically delete slave records, use a form and create `beforeDelete` Form
You might also check the form 'form' how the slave records 'FormElement' will be deleted.
.. _locking-record:
Locking Record / Form
---------------------
Support for record locking is given with mode:
* *exclusive*: user can't force a write.
* Including a timeout (default 15 mins: DIRTY_RECORD_TIMEOUT_SECONDS in `config.qfq.ini`_) for maximum lock time.
* *advisory*: user is only warned, but allowed to overwrite.
* *none*: no bookeeping about locks.
For 'new' records (r=0) there is no locking at all.
The record locking protection is based on the `tablename` and the `record id`. Different `Forms`, with the same primary table,
will be protected by record locking. On the other side, action-`FormElements` updating non primary table records are not
protected by 'record locking': the QFQ record locking is *NOT 100%*.
The 'record locking' mode will be specified per `Form`. If there are multiple Forms with different modes, and there is
already a lock for a `tablename` / `record id` pair, the most restrictive will be applied.
Best practice
-------------
......
......@@ -90,7 +90,7 @@ WKHTMLTOPDF = /opt/wkhtmltox/bin/wkhtmltopdf
; auto | always | never
;DB_UPDATE = auto
;DIRTY_RECORD_TIMEOUT_SECONDS_DEFAULT = 900
;RECORD_LOCK_TIMEOUT_SECONDS = 900
; Local Documentation (doc fits to installed version): typo3conf/ext/qfq/Documentation/html/Manual.html
;DOCUMENTATION_QFQ = https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html
......
......@@ -428,8 +428,8 @@ const SYSTEM_DB_UPDATE_ALWAYS = 'always';
const SYSTEM_DB_UPDATE_NEVER = 'never';
const SYSTEM_DB_UPDATE_AUTO = 'auto';
const SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS = 'DIRTY_RECORD_TIMEOUT_SECONDS';
const SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS_DEFAULT = 900; // 15 mins
const SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS = 'RECORD_LOCK_TIMEOUT_SECONDS';
const SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT = 900; // 15 mins
const DOCUMENTATION_QFQ = 'DOCUMENTATION_QFQ';
const DOCUMENTATION_QFQ_URL = 'https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html';
......@@ -1096,9 +1096,8 @@ const ACTION_ELEMENT_MODIFIED = 1;
const ACTION_ELEMENT_DELETED = -1;
// Dirty.php
const DIRTY_MODE_TIMEOUT = 'timeout';
const DIRTY_MODE_READONLY = 'readonly';
const DIRTY_MODE_OVERWRITE = 'overwrite';
const DIRTY_MODE_EXCLUSIVE = 'exclusive';
const DIRTY_MODE_ADVISORY = 'advisory';
const DIRTY_MODE_NONE = 'none';
const DIRTY_QFQ_USER_SESSION_COOKIE = 'qfqUserSessionCookie';
const DIRTY_FE_USER= 'feUser';
......
......@@ -270,7 +270,7 @@ class QuickFormQuery {
// Check and release dirtyRecord.
if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) {
$dirty = new Dirty();
$timeOutDurationSeconds = $this->store->getVar(STORE_SYSTEM, SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS);
$timeOutDurationSeconds = $this->store->getVar(STORE_SYSTEM, SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS);
$dirty->releaseDirty($this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $recordId, $timeOutDurationSeconds);
}
......
......@@ -52,6 +52,10 @@ $UPDATE_ARRAY = array(
"ALTER TABLE `FormElement` CHANGE `type` `type` ENUM( 'checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', 'gridJQW', 'text', 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', 'templateGroup', 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail', 'paste' ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'text';",
"ALTER TABLE `Form` CHANGE `forwardMode` `forwardMode` ENUM( 'client', 'no', 'url', 'url-skip-history', 'url-sip' ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'client';",
],
'0.19.0' => [
"ALTER TABLE `Form` ADD `dirtyMode` ENUM( 'exclusive', 'advisory', 'none' ) NOT NULL DEFAULT 'exclusive' AFTER `requiredParameter`",
],
);
......
......@@ -146,7 +146,7 @@ class Dirty {
$msg = "The record has already been tagged for editing by $msgUser at $at.";
// Mandatory lock on Record or current Form?
if ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_TIMEOUT || $currentFormDirtyMode == DIRTY_MODE_TIMEOUT) {
if ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE || $currentFormDirtyMode == DIRTY_MODE_EXCLUSIVE) {
$status = API_ANSWER_STATUS_CONFLICT;
} else {
$status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
......@@ -196,6 +196,10 @@ class Dirty {
*/
public function releaseDirty($dirtyMode, $tableName, $recordId, $timeOutDurationSeconds) {
if ($recordId == 0) {
return; // New records never have a recordDirty nor a conflict.
}
$recordDirty = $this->getRecordDirty($tableName, $recordId);
if (empty($recordDirty) && $dirtyMode == DIRTY_MODE_NONE) {
......@@ -217,7 +221,7 @@ class Dirty {
// From here: there is a foreign lock!
// Check if overwrite is allowed
if ($dirtyMode == DIRTY_MODE_OVERWRITE && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_OVERWRITE) {
if ($dirtyMode == DIRTY_MODE_ADVISORY && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_ADVISORY) {
$this->deleteDirtyRecord($recordDirty[COLUMN_ID]);
return;
}
......
......@@ -172,7 +172,7 @@ class Config {
Support::setIfNotSet($config, SYSTEM_GFX_EXTRA_BUTTON_INFO_BELOW, '<span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true"></span>');
Support::setIfNotSet($config, SYSTEM_DB_UPDATE, SYSTEM_DB_UPDATE_AUTO);
Support::setIfNotSet($config, SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS, SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS_DEFAULT);
Support::setIfNotSet($config, SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS, SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT);
Support::setIfNotSet($config, DOCUMENTATION_QFQ, DOCUMENTATION_QFQ_URL);
......
......@@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS `Form` (
`escapeTypeDefault` VARCHAR(32) NOT NULL DEFAULT 'c',
`render` ENUM('bootstrap', 'table', 'plain') NOT NULL DEFAULT 'bootstrap',
`requiredParameter` VARCHAR(255) NOT NULL DEFAULT '',
`dirtyMode` ENUM('timeout', 'readonly', 'overwrite', 'none') NOT NULL DEFAULT 'timeout',
`dirtyMode` ENUM('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive',
`showButton` SET('new', 'delete', 'close', 'save') NOT NULL DEFAULT 'new,delete,close,save',
`multiMode` ENUM('none', 'horizontal', 'vertical') NOT NULL DEFAULT 'none',
`multiSql` TEXT NOT NULL,
......@@ -195,6 +195,8 @@ VALUES
(1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'all', 'native', 220, 0, 10, '<a href="{{DOCUMENTATION_QFQ:Y}}#form-permitNewEdit">Info</a>', '', '', '', 'buttonClass=btn-default', 2, '', '', '', 'specialchar', 'no', ''),
(1, 'escapeTypeDefault', 'Escape type default', 'show', 'radio', 'all', 'native', 230, 0, 10, '<a href="{{DOCUMENTATION_QFQ:Y}}#variable-escape">Info</a>', '', '', '',
'itemList=c:config,s:single,d:double,l:ldap search,L:ldap value,m:mysql realEscapeString,-:none\nbuttonClass=btn-default', 2, '', '', '', 'specialchar', 'no', ''),
(1, 'dirtyMode', 'Record Locking', 'show', 'radio', 'all', 'native', 240, 0, 10, '<a href="{{DOCUMENTATION_QFQ:Y}}#locking-record">Info</a>', '', '', '',
'buttonClass=btn-default', 2, '', '', '', 'specialchar', 'no', ''),
(1, 'showButton', 'Show button', 'show', 'checkbox', 'all', 'native', 250, 0, 5, '<a href="{{DOCUMENTATION_QFQ:Y}}#form-showButton">Info</a>', '', '', '', 'checkBoxMode = multi\norientation=vertical', 2, '', '', '', 'specialchar', 'no', ''),
(1, 'forwardMode', 'Forward', 'show', 'radio', 'all', 'native', 300, 0, 0, '<a href="{{DOCUMENTATION_QFQ:Y}}#form-forward">Info</a>', '', '', '', 'buttonClass=btn-default', 3, '', '', '', 'specialchar', 'yes', ''),
(1, 'forwardPage', 'Forward URL / Page', 'show', 'text', 'all', 'native', 310, 0, 0, '<a href="{{DOCUMENTATION_QFQ:Y}}#form-forward">Info</a>', '', '', '', '', 3, '',
......
......@@ -292,7 +292,7 @@ EOT;
SYSTEM_GFX_EXTRA_BUTTON_INFO_BELOW => '<span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true"></span>',
SYSTEM_DB_UPDATE => SYSTEM_DB_UPDATE_AUTO,
SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS => SYSTEM_DIRTY_RECORD_TIMEOUT_SECONDS_DEFAULT,
SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS => SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT,
DOCUMENTATION_QFQ => DOCUMENTATION_QFQ_URL,
];
......
......@@ -12,14 +12,14 @@ CREATE TABLE IF NOT EXISTS `Form` (
`escapeTypeDefault` VARCHAR(32) NOT NULL DEFAULT 'c',
`render` ENUM('plain', 'table', 'bootstrap') NOT NULL DEFAULT 'plain',
`requiredParameter` VARCHAR(255) NOT NULL DEFAULT '',
`dirtyMode` ENUM('timeout', 'readonly', 'overwrite', 'none') NOT NULL DEFAULT 'timeout',
`dirtyMode` ENUM('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive',
`showButton` SET('new', 'delete') NOT NULL DEFAULT 'new,delete',
`multiMode` ENUM('none', 'horizontal', 'vertical') NOT NULL DEFAULT 'none',
`multiSql` TEXT NOT NULL,
`multiDetailForm` VARCHAR(255) NOT NULL DEFAULT '',
`multiDetailFormParameter` VARCHAR(255) NOT NULL DEFAULT '',
`forwardMode` ENUM('client', 'no', 'url', 'url-skip-history') NOT NULL DEFAULT 'client',
`forwardMode` ENUM('client', 'no', 'url', 'url-skip-history') NOT NULL DEFAULT 'client',
`forwardPage` VARCHAR(255) NOT NULL DEFAULT '',
`bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '',
......@@ -130,17 +130,17 @@ CREATE TABLE IF NOT EXISTS `FormElement` (
DROP TABLE IF EXISTS `Dirty`;
CREATE TABLE IF NOT EXISTS `Dirty` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`sip` VARCHAR(255) NOT NULL,
`tableName` VARCHAR(255) NOT NULL,
`recordId` INT(11) NOT NULL,
`recordModified` DATETIME NOT NULL,
`feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL,
`dirtyMode` ENUM('timeout', 'readonly', 'overwrite') NOT NULL,
`remoteAddress` VARCHAR(45) NOT NULL,
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created` DATETIME NOT NULL,
`id` INT(11) NOT NULL AUTO_INCREMENT,
`sip` VARCHAR(255) NOT NULL,
`tableName` VARCHAR(255) NOT NULL,
`recordId` INT(11) NOT NULL,
`recordModified` DATETIME NOT NULL,
`feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL,
`dirtyMode` ENUM('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive',
`remoteAddress` VARCHAR(45) NOT NULL,
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `sip` (`sip`),
KEY `tableName` (`tableName`),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment