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';
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_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';
......
......@@ -185,6 +185,10 @@ $UPDATE_ARRAY = array(
"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 @@
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\Helper\OnArray;
use IMATHUZH\Qfq\Core\Store\Client;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Store\Store;
/**
* Class Dirty
......@@ -27,7 +26,7 @@ use IMATHUZH\Qfq\Core\Helper\OnArray;
class Dirty {
/**
* @var Database instantiated class
* @var Database[] - Array of Database instantiated class
*/
protected $dbArray = null;
......@@ -134,7 +133,7 @@ class Dirty {
switch ($this->client[API_LOCK_ACTION]) {
case API_LOCK_ACTION_LOCK:
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;
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);
......@@ -153,10 +152,13 @@ class Dirty {
* @param array $tableVars
* @param string $recordHashMd5
*
* @param $tabUniqId
* @return array
* @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];
$primaryKey = $tableVars[F_PRIMARY_KEY];
......@@ -179,10 +181,14 @@ class Dirty {
$answer = [API_STATUS => 'success', API_MESSAGE => ''];
} else {
// 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 {
$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;
......@@ -197,6 +203,8 @@ class Dirty {
*
* @return array DirtyRecord or empty array.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function getRecordDirty($tableName, $recordId) {
......@@ -219,6 +227,9 @@ class Dirty {
*
* @param $primaryKey
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) {
$status = API_ANSWER_STATUS_CONFLICT;
......@@ -263,9 +274,13 @@ class Dirty {
* @param string $feUser
* @param string $recordHashMd5
*
* @param $tabUniqId
* @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];
$primaryKey = $tableVars[F_PRIMARY_KEY];
......@@ -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.
$expire = date('Y-m-d H:i:s', strtotime("+" . $tableVars[F_RECORD_LOCK_TIMEOUT_SECONDS] - 1 . " seconds"));
// Write 'dirty' record
$this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
[$s, $tableName, $recordId, $expire, $recordHashMd5, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
$this->dbArray[$this->dbIndexQfq]->sql("INSERT INTO Dirty (`sip`, `tableName`, `recordId`, `expire`, `recordHashMd5`, `tabUniqId`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,?,?,?,? )", ROW_REGULAR,
[$s, $tableName, $recordId, $expire, $recordHashMd5, $tabUniqId, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode,
$this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => '',
......@@ -295,6 +310,9 @@ class Dirty {
* @param string $recordHashMd5 - timestamp e.g. '2017-07-27 14:06:56'
* @param $rcMd5
* @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) {
......@@ -319,6 +337,8 @@ class Dirty {
*
* @return int LOCK_NOT_FOUND | LOCK_FOUND_OWNER | LOCK_FOUND_CONFLICT,
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public function getCheckDirty($tableName, $recordId, array &$recordDirty, &$msg) {
......@@ -334,6 +354,11 @@ class Dirty {
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]) {
$msgUser = "you";
} else {
......@@ -364,6 +389,7 @@ class Dirty {
* @param bool $flagCheckModifiedFirst
* @return array
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public function checkDirtyAndRelease($formMode, $lockTimeout, $dirtyMode, $tableName, $primaryKey, $recordId, $flagCheckModifiedFirst = false) {
......@@ -446,6 +472,8 @@ class Dirty {
* @param int $recordDirtyId
*
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private function deleteDirtyRecord($recordDirtyId) {
......
......@@ -8,8 +8,8 @@
namespace IMATHUZH\Qfq\Core\Store;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
/**
* Class Client
......@@ -32,6 +32,11 @@ class Client {
// Dirty workaround to clean poisoned T3 cache
Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_TYPE);
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();
......
......@@ -30,12 +30,12 @@ CREATE TABLE IF NOT EXISTS `Form`
`forwardPage` VARCHAR(511) NOT NULL DEFAULT '',
`labelAlign` ENUM ('default', 'left', 'center', 'right') NOT NULL DEFAULT 'default',
`bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '',
`bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '',
`parameter` TEXT NOT NULL,
`parameterLanguageA` TEXT NOT NULL,
`parameter` TEXT NOT NULL,
`parameterLanguageA` TEXT NOT NULL,
`parameterLanguageB` TEXT NOT NULL,
`parameterLanguageC` TEXT NOT NULL,
`parameterLanguageD` TEXT NOT NULL,
......@@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS `Dirty`
`recordId` INT(11) NOT NULL,
`expire` DATETIME NOT NULL,
`recordHashMd5` CHAR(32) NOT NULL,
`tabUniqId` CHAR(32) NOT NULL,
`feUser` VARCHAR(255) NOT NULL,
`qfqUserSessionCookie` VARCHAR(255) NOT NULL,
`dirtyMode` ENUM ('exclusive', 'advisory', 'none') NOT NULL DEFAULT 'exclusive',
......
......@@ -739,8 +739,10 @@ var QfqNS = QfqNS || {};
};
n.QfqForm.prototype.getRecordHashMd5AsQueryParameter = function () {
return {
'recordHashMd5': this.getRecordHashMd5()
'recordHashMd5': this.getRecordHashMd5(),
'tabUniqId': this.getTabUniqId()
};
};
......@@ -1306,6 +1308,22 @@ var QfqNS = QfqNS || {};
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) {
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