Commit de576531 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Fixes #9733: add JS code to name browser tabs individually. New GET variable...

Fixes #9733: add JS code to name browser tabs individually. New GET variable 'tabUniqId' on record acquire is now saved in dirty record. On page reload, when the 'release' comes after 'acquire' (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.
parent e0b1788b
Pipeline #2896 failed with stages
in 1 minute and 52 seconds
...@@ -702,6 +702,7 @@ const SIP_URLPARAM = 'urlparam'; ...@@ -702,6 +702,7 @@ const SIP_URLPARAM = 'urlparam';
const SIP_SIP_URL = 'sipUrl'; const SIP_SIP_URL = 'sipUrl';
const SIP_MAKE_URLPARAM_UNIQ = '_makeUrlParamUniq'; // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter const SIP_MAKE_URLPARAM_UNIQ = '_makeUrlParamUniq'; // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter
const SIP_DOWNLOAD_PARAMETER = '_b64_download'; // Parameter name, filled in SIP, to hold all download element parameter. const SIP_DOWNLOAD_PARAMETER = '_b64_download'; // Parameter name, filled in SIP, to hold all download element parameter.
const TAB_UNIQ_ID = 'tabUniqId'; // Currently only only a uniq identifier: no values stored behind the identifier - might change.
const SIP_PREFIX_BASE64 = '_b64'; const SIP_PREFIX_BASE64 = '_b64';
......
...@@ -185,6 +185,10 @@ $UPDATE_ARRAY = array( ...@@ -185,6 +185,10 @@ $UPDATE_ARRAY = array(
"ALTER TABLE `Setting` CHANGE `created` `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; ", "ALTER TABLE `Setting` CHANGE `created` `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; ",
], ],
'19.12.0' => [
"ALTER TABLE `Dirty` ADD `tabUniqId` VARCHAR(32) NOT NULL AFTER `recordHashMd5`;",
],
); );
......
...@@ -8,13 +8,12 @@ ...@@ -8,13 +8,12 @@
namespace IMATHUZH\Qfq\Core\Form; namespace IMATHUZH\Qfq\Core\Form;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Database\Database; use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Store\Client; use IMATHUZH\Qfq\Core\Store\Client;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip; use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Helper\OnArray; use IMATHUZH\Qfq\Core\Store\Store;
/** /**
* Class Dirty * Class Dirty
...@@ -27,7 +26,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray; ...@@ -27,7 +26,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray;
class Dirty { class Dirty {
/** /**
* @var Database instantiated class * @var Database[] - Array of Database instantiated class
*/ */
protected $dbArray = null; protected $dbArray = null;
...@@ -134,7 +133,7 @@ class Dirty { ...@@ -134,7 +133,7 @@ class Dirty {
switch ($this->client[API_LOCK_ACTION]) { switch ($this->client[API_LOCK_ACTION]) {
case API_LOCK_ACTION_LOCK: case API_LOCK_ACTION_LOCK:
case API_LOCK_ACTION_EXTEND: case API_LOCK_ACTION_EXTEND:
$answer = $this->acquireDirty($recordId, $tableVars, $this->client[DIRTY_RECORD_HASH_MD5]); $answer = $this->acquireDirty($recordId, $tableVars, $this->client[DIRTY_RECORD_HASH_MD5], $this->client[TAB_UNIQ_ID]);
break; break;
case API_LOCK_ACTION_RELEASE: case API_LOCK_ACTION_RELEASE:
$answer = $this->checkDirtyAndRelease(FORM_SAVE, $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS], $tableVars[F_DIRTY_MODE], $tableVars[F_TABLE_NAME], $tableVars[F_PRIMARY_KEY], $recordId); $answer = $this->checkDirtyAndRelease(FORM_SAVE, $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS], $tableVars[F_DIRTY_MODE], $tableVars[F_TABLE_NAME], $tableVars[F_PRIMARY_KEY], $recordId);
...@@ -153,10 +152,13 @@ class Dirty { ...@@ -153,10 +152,13 @@ class Dirty {
* @param array $tableVars * @param array $tableVars
* @param string $recordHashMd5 * @param string $recordHashMd5
* *
* @param $tabUniqId
* @return array * @return array
* @throws \CodeException * @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function acquireDirty($recordId, array $tableVars, $recordHashMd5) { private function acquireDirty($recordId, array $tableVars, $recordHashMd5, $tabUniqId) {
$tableName = $tableVars[F_TABLE_NAME]; $tableName = $tableVars[F_TABLE_NAME];
$primaryKey = $tableVars[F_PRIMARY_KEY]; $primaryKey = $tableVars[F_PRIMARY_KEY];
...@@ -179,10 +181,14 @@ class Dirty { ...@@ -179,10 +181,14 @@ class Dirty {
$answer = [API_STATUS => 'success', API_MESSAGE => '']; $answer = [API_STATUS => 'success', API_MESSAGE => ''];
} else { } else {
// No dirty record found. // No dirty record found.
$answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5); $answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5, $tabUniqId);
} }
} else { } else {
$answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey); if ($tabUniqId == $recordDirty[TAB_UNIQ_ID]) {
$answer = [API_STATUS => 'success', API_MESSAGE => ''];
} else {
$answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey);
}
} }
return $answer; return $answer;
...@@ -197,6 +203,8 @@ class Dirty { ...@@ -197,6 +203,8 @@ class Dirty {
* *
* @return array DirtyRecord or empty array. * @return array DirtyRecord or empty array.
* @throws \CodeException * @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function getRecordDirty($tableName, $recordId) { private function getRecordDirty($tableName, $recordId) {
...@@ -219,6 +227,9 @@ class Dirty { ...@@ -219,6 +227,9 @@ class Dirty {
* *
* @param $primaryKey * @param $primaryKey
* @return array * @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) { private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) {
$status = API_ANSWER_STATUS_CONFLICT; $status = API_ANSWER_STATUS_CONFLICT;
...@@ -263,9 +274,13 @@ class Dirty { ...@@ -263,9 +274,13 @@ class Dirty {
* @param string $feUser * @param string $feUser
* @param string $recordHashMd5 * @param string $recordHashMd5
* *
* @param $tabUniqId
* @return array * @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function writeDirty($s, $recordId, array $tableVars, $feUser, $recordHashMd5) { private function writeDirty($s, $recordId, array $tableVars, $feUser, $recordHashMd5, $tabUniqId) {
$tableName = $tableVars[F_TABLE_NAME]; $tableName = $tableVars[F_TABLE_NAME];
$primaryKey = $tableVars[F_PRIMARY_KEY]; $primaryKey = $tableVars[F_PRIMARY_KEY];
...@@ -276,9 +291,9 @@ class Dirty { ...@@ -276,9 +291,9 @@ class Dirty {
# Dirty workaround: setting the 'expired timestamp' minus 1 second guarantees that the client ask for relock always if the timeout is expired. # Dirty workaround: setting the 'expired timestamp' minus 1 second guarantees that the client ask for relock always if the timeout is expired.
$expire = date('Y-m-d H:i:s', strtotime("+" . $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] - 1 . " seconds")); $expire = date('Y-m-d H:i:s', strtotime("+" . $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] - 1 . " seconds"));
// Write 'dirty' record // Write 'dirty' record
$this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " . $this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `tabUniqId`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR, "VALUES ( ?,?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
[$s, $tableName, $recordId, $expire, $recordHashMd5, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode, [$s, $tableName, $recordId, $expire, $recordHashMd5, $tabUniqId, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
$this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]); $this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => '', return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => '',
...@@ -295,6 +310,9 @@ class Dirty { ...@@ -295,6 +310,9 @@ class Dirty {
* @param string $recordHashMd5 - timestamp e.g. '2017-07-27 14:06:56' * @param string $recordHashMd5 - timestamp e.g. '2017-07-27 14:06:56'
* @param $rcMd5 * @param $rcMd5
* @return bool true if $recordHashMd5 is different from current record md5 hash. * @return bool true if $recordHashMd5 is different from current record md5 hash.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function isRecordModified($tableName, $primaryKey, $recordId, $recordHashMd5, &$rcMd5) { private function isRecordModified($tableName, $primaryKey, $recordId, $recordHashMd5, &$rcMd5) {
...@@ -319,6 +337,8 @@ class Dirty { ...@@ -319,6 +337,8 @@ class Dirty {
* *
* @return int LOCK_NOT_FOUND | LOCK_FOUND_OWNER | LOCK_FOUND_CONFLICT, * @return int LOCK_NOT_FOUND | LOCK_FOUND_OWNER | LOCK_FOUND_CONFLICT,
* @throws \CodeException * @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
public function getCheckDirty($tableName, $recordId, array &$recordDirty, &$msg) { public function getCheckDirty($tableName, $recordId, array &$recordDirty, &$msg) {
...@@ -334,6 +354,11 @@ class Dirty { ...@@ -334,6 +354,11 @@ class Dirty {
return LOCK_NOT_FOUND; return LOCK_NOT_FOUND;
} }
// 'Reload Tab' don't send a tab ID - Possible lock conflict will be skipped here and a) pops up later, or b) is not a conflict if it's the same tab.
if (!isset($this->client[TAB_UNIQ_ID]) && $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
return LOCK_NOT_FOUND;
}
if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) { if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) {
$msgUser = "you"; $msgUser = "you";
} else { } else {
...@@ -364,6 +389,7 @@ class Dirty { ...@@ -364,6 +389,7 @@ class Dirty {
* @param bool $flagCheckModifiedFirst * @param bool $flagCheckModifiedFirst
* @return array * @return array
* @throws \CodeException * @throws \CodeException
* @throws \DbException
* @throws \UserFormException * @throws \UserFormException
*/ */
public function checkDirtyAndRelease($formMode, $lockTimeout, $dirtyMode, $tableName, $primaryKey, $recordId, $flagCheckModifiedFirst = false) { public function checkDirtyAndRelease($formMode, $lockTimeout, $dirtyMode, $tableName, $primaryKey, $recordId, $flagCheckModifiedFirst = false) {
...@@ -446,6 +472,8 @@ class Dirty { ...@@ -446,6 +472,8 @@ class Dirty {
* @param int $recordDirtyId * @param int $recordDirtyId
* *
* @throws \CodeException * @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/ */
private function deleteDirtyRecord($recordDirtyId) { private function deleteDirtyRecord($recordDirtyId) {
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
namespace IMATHUZH\Qfq\Core\Store; namespace IMATHUZH\Qfq\Core\Store;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\OnString; use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
/** /**
* Class Client * Class Client
...@@ -32,6 +32,11 @@ class Client { ...@@ -32,6 +32,11 @@ class Client {
// Dirty workaround to clean poisoned T3 cache // Dirty workaround to clean poisoned T3 cache
Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_TYPE); Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_TYPE);
Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_LANGUAGE); Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_LANGUAGE);
Sanitize::digitCheckAndCleanGet(TAB_UNIQ_ID);
if (empty($_GET[TAB_UNIQ_ID])) {
$_GET[TAB_UNIQ_ID] = 1; // Maybe some browser do not support 'window.name' to be misused as tabId variable: fake with '1'.
}
$header = self::getHeader(); $header = self::getHeader();
......
...@@ -30,12 +30,12 @@ CREATE TABLE IF NOT EXISTS `Form` ...@@ -30,12 +30,12 @@ CREATE TABLE IF NOT EXISTS `Form`
`forwardPage` VARCHAR(511) NOT NULL DEFAULT '', `forwardPage` VARCHAR(511) NOT NULL DEFAULT '',
`labelAlign` ENUM ('default', 'left', 'center', 'right') NOT NULL DEFAULT 'default', `labelAlign` ENUM ('default', 'left', 'center', 'right') NOT NULL DEFAULT 'default',
`bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '', `bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '', `bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '', `bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '',
`parameter` TEXT NOT NULL, `parameter` TEXT NOT NULL,
`parameterLanguageA` TEXT NOT NULL, `parameterLanguageA` TEXT NOT NULL,
`parameterLanguageB` TEXT NOT NULL, `parameterLanguageB` TEXT NOT NULL,
`parameterLanguageC` TEXT NOT NULL, `parameterLanguageC` TEXT NOT NULL,
`parameterLanguageD` TEXT NOT NULL, `parameterLanguageD` TEXT NOT NULL,
...@@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS `Dirty` ...@@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS `Dirty`
`recordId` INT(11) NOT NULL, `recordId` INT(11) NOT NULL,
`expire` DATETIME NOT NULL, `expire` DATETIME NOT NULL,
`recordHashMd5` CHAR(32) NOT NULL, `recordHashMd5` CHAR(32) NOT NULL,
`tabUniqId` CHAR(32) NOT NULL,
`feUser` VARCHAR(255) NOT NULL, `feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL, `qfqUserSessionCookie` VARCHAR(255) NOT NULL,
`dirtyMode` ENUM ('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive', `dirtyMode` ENUM ('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive',
......
...@@ -739,8 +739,10 @@ var QfqNS = QfqNS || {}; ...@@ -739,8 +739,10 @@ var QfqNS = QfqNS || {};
}; };
n.QfqForm.prototype.getRecordHashMd5AsQueryParameter = function () { n.QfqForm.prototype.getRecordHashMd5AsQueryParameter = function () {
return { return {
'recordHashMd5': this.getRecordHashMd5() 'recordHashMd5': this.getRecordHashMd5(),
'tabUniqId': this.getTabUniqId()
}; };
}; };
...@@ -1306,6 +1308,22 @@ var QfqNS = QfqNS || {}; ...@@ -1306,6 +1308,22 @@ var QfqNS = QfqNS || {};
return this.getValueOfHiddenInputField('recordHashMd5'); return this.getValueOfHiddenInputField('recordHashMd5');
}; };
/**
* Misuse the window.name attribute to set/get a tab uniq identifier.
* Use millisecond timestamp as identifier: hopefully there are never more than one tab opened per millisecond in a single browser session.
*
* @returns {string} tab identifier
*/
n.QfqForm.prototype.getTabUniqId = function () {
if (!window.name.toString()) {
// Misuse window.name as tab uniq identifier. Set window.name if it is empty.
window.name = Date.now().toString();
}
return window.name;
};
n.QfqForm.prototype.getValueOfHiddenInputField = function (fieldName) { n.QfqForm.prototype.getValueOfHiddenInputField = function (fieldName) {
return $('#' + this.formId + ' input[name=' + fieldName + ']').val(); return $('#' + this.formId + ' input[name=' + fieldName + ']').val();
}; };
......
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