Commit 606a6b2a authored by Carsten  Rose's avatar Carsten Rose
Browse files

formDirty: version not tested, definitely broken

parent feabaa01
......@@ -12,6 +12,11 @@ Alice -> dirty: action=lock
activate dirty #FFBBBB
dirty -> Alice: status=success, lock_timeout=<secs>
...
Alice -> Alice: edit (more changes)
note over Alice: Client will extend lock automatically,\nif there are changes during last timeout period
Alice -> dirty: action=extend
dirty -> Alice: status=success
...
Alice -> save: POST form
save -> dirty: lock valid?
dirty -> save: yes
......@@ -39,17 +44,50 @@ note right: User Bob can edit and save
Bob -> save: Post form
save -> dirty: lock valid?
dirty -> save: no
save -> save: save form
save -> dirty: action=release
dirty->dirty: lock for Alice can't be released by Bob
save -> Bob: status=succes,redirect=client
save -> save: store 'tokenForce=<uniqid>' in lock
save -> Bob: status=**conflict_allow_force**,\nmessage=Record locked by user Alice,\ntokenForce=<uniqid>
Bob -> Bob: UI question 'continue?'
alt Bob: force save
Bob -> save: Post form, GET Parameter 'tokenForce=<uniqid>'
save -> save: uniqId valid?
alt yes
save -> save: save form
save -> Bob: status=**succes**,redirect=client
else no
save -> Bob: status=**conflict_allow_force**,\nmessage=Record locked by user Alice,\ntokenForce=<uniqid>
save -> save: store 'tokenForce=<uniqid>' in lock
note over save: old <uniqid> will be overwritten
end
else Bob: cancel save
Bob -> Bob: none
end
note over save: no release: Bob does not own the lock
...
Alice -> save: POST form
save -> dirty: lock valid?
dirty -> save: yes
save -> save: save form
save -> dirty: action=release
deactivate dirty
save -> Alice: status=succes,redirect=client
save -> dirty: lock valid? yes!
dirty -> dirty: record modified since lock?
alt yes
save -> Alice: status=**conflict_allow_force**,\nmessage=Record has been modified,\ntokenForce=<uniqid>
...
Alice -> save: POST form, GET Parameter 'tokenForce=<uniqid>'
save -> save: uniqId valid?
alt yes
save -> save: save form
save -> dirty: action=release
save -> Alice: status=**succes**,redirect=client
else no
save -> Alice: status=**conflict_allow_force**,\nmessage=Record locked,\ntokenForce=<uniqid>
save -> save: store 'tokenForce=<uniqid>' in lock
note over save: old <uniqid> will be overwritten
end
else
save -> save: save form
save -> dirty: action=release
deactivate dirty
save -> Alice: status=succes,redirect=client
end
@enduml
\ No newline at end of file
......@@ -51,7 +51,7 @@ Alice -> save: POST form
save -> dirty: lock valid?
dirty -> save: no
save -> Alice: status=**conflict**,redirect=no
note left: Save Button becomes disabled
note over Alice: Save Button becomes disabled
...
Bob -> save: POST form
save -> dirty: lock valid?
......@@ -60,6 +60,8 @@ save -> save: save form
save -> dirty: action=release
deactivate dirty
save -> Bob: status=**succes**,redirect=client
...
note over Alice: If Alice want's 'edit & save'\nAlice has to reload page\nto enable save button again.
@enduml
\ No newline at end of file
......@@ -257,7 +257,6 @@ const ERROR_SUBSTITUTE_FOUND = 2100;
// Dirty
const ERROR_MISSING_FORM_IN_SIP = 2200;
const ERROR_DELETE_DIRTY_RECORD = 2201;
//
// Store Names: Identifier
//
......@@ -688,9 +687,10 @@ const F_FORWARD_MODE_NO = API_ANSWER_REDIRECT_NO;
const F_FORWARD_MODE_URL = API_ANSWER_REDIRECT_URL;
const F_FORWARD_MODE_URL_SKIP_HISTORY = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
const F_FORWARD_MODE_URL_SIP = 'url-sip';
// client', 'no', 'url', 'url-skip-history'
const F_RECORD_LOCK_TIMEOUT_SECONDS = 'recordLockTimeoutSeconds';
const F_FE_DATA_PATTERN_ERROR = 'data-pattern-error';
const F_FE_DATA_REQUIRED_ERROR = 'data-required-error';
const F_FE_DATA_MATCH_ERROR = 'data-match-error';
......@@ -1102,3 +1102,7 @@ const DIRTY_MODE_NONE = 'none';
const DIRTY_QFQ_USER_SESSION_COOKIE = 'qfqUserSessionCookie';
const DIRTY_FE_USER= 'feUser';
const DIRTY_REMOTE_ADDRESS = 'remoteAddress';
const DIRTY_API_ACTION = 'action'; // Name of parameter in API call of dirty.php?action=...&s=...
const DIRTY_API_ACTION_LOCK = 'lock';
const DIRTY_API_ACTION_RELEASE = 'release';
const DIRTY_API_ACTION_EXTEND = 'extend';
......@@ -756,6 +756,8 @@ class QuickFormQuery {
F_NEW_BUTTON_CLASS,
F_NEW_BUTTON_GLYPH_ICON,
F_RECORD_LOCK_TIMEOUT_SECONDS,
];
// By definition: existing vars which are empty, means: EMPTY - do not use any default!
......
......@@ -55,6 +55,7 @@ $UPDATE_ARRAY = array(
'0.19.0' => [
"ALTER TABLE `Form` ADD `dirtyMode` ENUM( 'exclusive', 'advisory', 'none' ) NOT NULL DEFAULT 'exclusive' AFTER `requiredParameter`",
"ALTER TABLE `Form` ADD `recordLockTimeoutSeconds` INT NOT NULL DEFAULT '0' AFTER `parameter`"
],
);
......
......@@ -10,6 +10,7 @@ namespace qfq;
use qfq;
require_once(__DIR__ . '/../store/Store.php');
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../database/Database.php');
......@@ -18,6 +19,11 @@ require_once(__DIR__ . '/../../qfq/store/Client.php');
class Dirty {
/**
* @var Store
*/
private $store = null;
/**
* @var Database instantiated class
*/
......@@ -41,7 +47,7 @@ class Dirty {
$this->session = Session::getInstance($phpUnit);
$this->client = Client::getParam();
$this->db = new Database();
$this->store = Store::getInstance('', $phpUnit);
}
/**
......@@ -55,20 +61,26 @@ class Dirty {
$sipClass = new Sip();
$sipVars = $sipClass->getVarsFromSip($this->client[SIP_SIP]);
$action = $this->client[DIRTY_API_ACTION];
return $this->acquireDirty($sipVars);
return $this->processDirty($sipVars, $action);
}
/**
* Tries to get a 'DirtyRecord'. Returns an array about success or failure.
* Depending of $action:
* DIRTY_API_ACTION_LOCK: tries to get a 'DirtyRecord'.
* DIRTY_API_ACTION_RELEASE: release a 'DirtyRecord'.
* DIRTY_API_ACTION_EXTEND: extend a 'DirtyRecord'
* Returns always an array about success or failure.
*
* @param array $sipVars
* @param string $action DIRTY_API_ACTION_LOCK | DIRTY_API_ACTION_RELEASE | DIRTY_API_ACTION_EXTEND
* @return array
* @throws CodeException
* @throws DbException
*/
private function acquireDirty(array $sipVars) {
private function processDirty(array $sipVars, $action) {
$answer = array();
......@@ -78,24 +90,99 @@ class Dirty {
$recordId = empty($sipVars[SIP_RECORD_ID]) ? 0 : $sipVars[SIP_RECORD_ID];
// For r=0 (new) , 'dirty' will always succeed.
// For r=0 (new) , 'dirty' will always succeed (for all $actions modes).
if ($recordId == 0) {
return [API_STATUS => 'success', API_MESSAGE => ''];
}
// Get tableName
$tableVars = $this->db->sql("SELECT tableName, dirtyMode FROM Form AS f WHERE f.name=?", ROW_EXPECT_1, [$sipVars[SIP_FORM]], "Form not found: '" . $sipVars[SIP_FORM] . "'");
// Get tableName. Take care to check timeout defaults manually.
$tableVars = $this->db->sql("SELECT tableName, dirtyMode, recordLockTimeoutSeconds FROM Form AS f WHERE f.name=?", ROW_EXPECT_1, [$sipVars[SIP_FORM]], "Form not found: '" . $sipVars[SIP_FORM] . "'");
$tableName = $tableVars[F_TABLE_NAME];
$formDirtyMode = $tableVars[F_DIRTY_MODE];
if ($tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] == '0') {
$recordLockTimeoutSeconds = $this->store->getVar(SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS, STORE_SYSTEM);
} else {
$recordLockTimeoutSeconds = $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS];
}
$feUser = $this->session->get(SESSION_FE_USER);
// Look for already existing dirty record.
$recordDirty = $this->getRecordDirty($tableName, $recordId);
switch ($action) {
case DIRTY_API_ACTION_LOCK:
$answer = $this->lockAcquire($recordDirty, $recordId, $tableName, $formDirtyMode, $feUser, $recordLockTimeoutSeconds);
break;
case DIRTY_API_ACTION_RELEASE:
$answer = $this->lockRelease($recordDirty);
break;
case DIRTY_API_ACTION_EXTEND:
$answer = $this->lockExtend($recordDirty, $recordLockTimeoutSeconds, $formDirtyMode);
break;
default:
break;
}
return $answer;
}
private function lockExtend($recordDirty, $recordLockTimeoutSeconds, $formDirtyMode) {
if (count($recordDirty) == 0) {
// No dirty record found: this is not ok
$answer = [API_STATUS => API_ANSWER_STATUS_ERROR, API_MESSAGE => 'No lock found to extend'];
} else {
if ($this->client[CLIENT_COOKIE_QFQ] == $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE]) {
// Write 'dirty' record
$expire = ($recordLockTimeoutSeconds == 0) ? "'9999-12-31 23:59:59'" : "DATE_ADD(NOW(), INTERVAL $recordLockTimeoutSeconds SECOND)";
$rc = $this->db->sql("UPDATE Dirty SET `expire`= $expire WHERE id=? LIMIT 1", [$recordDirty[COLUMN_ID]], "Can't delete Dirty record");
if ($rc != 1) {
throw new CodeException("Failed to update a record which have been seen earlier: Dirty.id=" . $recordDirty[COLUMN_ID], ERROR_FAILED_DELETE_DIRTY_RECORD);
}
$answer = [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => ''];
} else {
$answer = $this->conflict($recordDirty, $formDirtyMode);
}
}
return $answer;
}
/**
* @param array $recordDirty
* @return array
* @throws CodeException
* @throws DbException
*/
private function lockRelease(array $recordDirty) {
if (count($recordDirty) == 0) {
$answer = [API_STATUS => API_ANSWER_STATUS_ERROR, API_MESSAGE => 'No lock found to release'];
} else {
$this->deleteDirtyRecord($recordDirty[COLUMN_ID]);
$answer = [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => ''];
}
return $answer;
}
/**
* @param array $recordDirty
* @param int $recordId
* @param string $tableName
* @param string $formDirtyMode
* @param string $feUser
* @param int $recordLockTimeoutSeconds
* @return array
*/
private function lockAcquire(array $recordDirty, $recordId, $tableName, $formDirtyMode, $feUser, $recordLockTimeoutSeconds) {
if (count($recordDirty) == 0) {
// No dirty record found.
$answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableName, $formDirtyMode, $feUser);
$answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableName, $formDirtyMode, $feUser, $recordLockTimeoutSeconds);
} else {
$answer = $this->conflict($recordDirty, $formDirtyMode);
}
......@@ -103,6 +190,7 @@ class Dirty {
return $answer;
}
/**
* Load (if exist) a DirtyRecord (lock).
*
......@@ -122,6 +210,8 @@ class Dirty {
}
/**
* Checks different conflict stati. Should only be called in case of an conflict.
*
* @param array $recordDirty
* @param $currentFormDirtyMode
* @return array
......@@ -136,7 +226,7 @@ class Dirty {
$status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
} else {
// username if logged in
if (empty($recordDirty[DIRTY_FE_USER])) {
$msgUser = "another user";
} else {
......@@ -149,7 +239,8 @@ class Dirty {
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;
// If mode is '
$status = API_ANSWER_STATUS_SUCCESS;
}
}
......@@ -159,16 +250,17 @@ class Dirty {
/**
* Write a 'Dirty'-Record.
*
* @param $s
* @param $recordId
* @param $tableName
* @param $formDirtyMode
* @param $feUser
* @param string $s
* @param int $recordId
* @param string $tableName
* @param string $formDirtyMode
* @param string $feUser
* @param int $recordLockTimeoutSeconds
* @return array
* @throws CodeException
* @throws DbException
*/
private function writeDirty($s, $recordId, $tableName, $formDirtyMode, $feUser) {
private function writeDirty($s, $recordId, $tableName, $formDirtyMode, $feUser, $recordLockTimeoutSeconds) {
$record = $this->db->sql("SELECT * FROM $tableName WHERE id=?", ROW_EXPECT_1, [$recordId], "Record to tag 'dirty' not found.");
......@@ -176,8 +268,10 @@ class Dirty {
$recordModified = empty($record[COLUMN_MODIFIED]) ? 0 : $record[COLUMN_MODIFIED];
// Write 'dirty' record
$this->db->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `recordModified`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,? )", ROW_REGULAR, [$s, $tableName, $recordId, $recordModified, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode, $this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
$expire = ($recordLockTimeoutSeconds == 0) ? "9999-12-31 23:59:59" : "DATE_ADD(NOW(), INTERVAL $recordLockTimeoutSeconds SECOND)";
$this->db->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordModified`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR, [$s, $tableName, $recordId, $expire, $recordModified, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode, $this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => ''];
......
......@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS `Form` (
`bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '',
`parameter` TEXT NOT NULL,
`recordLockTimeoutSeconds` INT(11) NOT NULL DEFAULT 0,
`deleted` ENUM('yes', 'no') NOT NULL DEFAULT 'no',
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
......@@ -135,6 +136,7 @@ CREATE TABLE IF NOT EXISTS `Dirty` (
`sip` VARCHAR(255) NOT NULL,
`tableName` VARCHAR(255) NOT NULL,
`recordId` INT(11) NOT NULL,
`expire` DATETIME NOT NULL,
`recordModified` DATETIME NOT NULL,
`feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL,
......@@ -198,6 +200,7 @@ VALUES
'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, 'recordLockTimeoutSeconds', 'Lock timeout (seconds)', 'show', 'text', 'all', 'native', 245, 0, 0, '<a href="{{DOCUMENTATION_QFQ:Y}}#form-requiredParameter">Info</a>', '', '', '', '', 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, '',
......
......@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS `Form` (
`bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '',
`parameter` TEXT NOT NULL,
`recordLockTimeoutSeconds` INT(11) NOT NULL DEFAULT 0,
`deleted` ENUM('yes', 'no') NOT NULL DEFAULT 'no',
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
......@@ -134,6 +135,7 @@ CREATE TABLE IF NOT EXISTS `Dirty` (
`sip` VARCHAR(255) NOT NULL,
`tableName` VARCHAR(255) NOT NULL,
`recordId` INT(11) NOT NULL,
`expire` DATETIME NOT NULL,
`recordModified` DATETIME NOT NULL,
`feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL,
......
Supports Markdown
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