Commit 5739af82 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Feature #3981 / Record Locking

First implementation on server side: only tag as dirty, no check during save().
dirty.php, Dirty.php, Client.php: new
Store.php: refactored fillStoreClient() to use an dedicated class.
BuildFormBootstrap.php: add hook for dirty.php
parent e93b80ab
<?php
/**
* Created by PhpStorm.
* User: ep
* Date: 12/23/15
* Time: 6:17 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../qfq/form/Dirty.php');
require_once(__DIR__ . '/../qfq/Constants.php');
/**
* Return JSON encoded answer
*
*/
try {
$dirty = new \qfq\Dirty();
$answer = $dirty->process();
} catch (\Exception $e) {
// $answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
$answer = [API_STATUS => API_ANSWER_STATUS_ERROR, API_MESSAGE => "Error: " . $e->getMessage()];
}
header("Content-Type: application/json");
echo json_encode($answer);
......@@ -503,6 +503,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
tabsId: '$tabId',
formId: '$formId',
submitTo: '$apiDir/save.php',
dirtyUrl: '$apiDir/dirty.php',
deleteUrl: '$deleteUrl',
refreshUrl: '$apiDir/load.php',
fileUploadTo: '$apiDir/file.php?$actionUpload',
......
......@@ -20,6 +20,9 @@ const SESSION_FE_USER_UID = 'feUserUid';
const SESSION_FE_USER = 'feUser';
const SESSION_FE_USER_GROUP = 'feUserGroup';
const TABLE_NAME_FORM = 'Form';
const TABLE_NAME_FORM_ELEMENT = 'FormElement';
const FORM_LOAD = 'form_load';
const FORM_SAVE = 'form_save';
const FORM_UPDATE = 'form_update';
......@@ -249,6 +252,9 @@ const ERROR_DB_MULTI_QUERY_FAILED = 2016;
// onArray
const ERROR_SUBSTITUTE_FOUND = 2100;
// Dirty
const ERROR_MISSING_FORM_IN_SIP = 2200;
//
// Store Names: Identifier
//
......@@ -575,6 +581,8 @@ const API_JSON_DISABLED = 'disabled';
const API_JSON_REQUIRED = 'required';
const API_ANSWER_STATUS_SUCCESS = 'success';
const API_ANSWER_STATUS_CONFLICT = 'conflict';
const API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE = 'conflict_allow_force';
const API_ANSWER_STATUS_ERROR = 'error';
const API_ANSWER_REDIRECT_CLIENT = 'client';
const API_ANSWER_REDIRECT_NO = 'no';
......@@ -653,6 +661,7 @@ const F_TABLE_NAME = 'tableName';
const F_REQUIRED_PARAMETER = 'requiredParameter';
const F_EXTRA_DELETE_FORM = 'extraDeleteForm';
const F_FINAL_DELETE_FORM = 'finalDeleteForm';
const F_DIRTY_MODE = 'dirtyMode';
const F_SUBMIT_BUTTON_TEXT = 'submitButtonText';
const F_BUTTON_ON_CHANGE_CLASS = 'buttonOnChangeClass';
......@@ -1078,4 +1087,10 @@ const WKHTML_OPTION_VIEWPORT_VALUE = '1280x1024';
// FormAction.php:
const ACTION_ELEMENT_NO_CHANGE = 0;
const ACTION_ELEMENT_MODIFIED = 1;
const ACTION_ELEMENT_DELETED = -1;
\ No newline at end of file
const ACTION_ELEMENT_DELETED = -1;
// Dirty.php
const DIRTY_MODE_TIMEOUT = 'timeout';
const DIRTY_MODE_READONLY = 'readonly';
const DIRTY_MODE_OVERWRITE = 'overwrite';
const QFQ_USER_SESSION_COOKIE = 'qfqUserSessionCookie';
\ No newline at end of file
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/13/17
* Time: 9:29 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../database/Database.php');
require_once(__DIR__ . '/../../qfq/store/Client.php');
class Dirty {
/**
* @var Database instantiated class
*/
protected $db = null;
/**
* @var array
*/
protected $client = array();
/**
* @var Session
*/
private $session = null;
/**
*
*/
public function __construct($phpUnit = false) {
$this->session = Session::getInstance($phpUnit);
$this->client = Client::getParam();
$this->db = new Database();
}
/**
* @return array|int
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function process() {
$sipClass = new Sip();
$sipVars = $sipClass->getVarsFromSip($this->client[SIP_SIP]);
return $this->acquireDirty($sipVars);
}
/**
* Tries to get a 'DirtyRecord'. Returns an array about success or failure.
*
* @param array $sipVars
* @return array
* @throws CodeException
* @throws DbException
*/
private function acquireDirty(array $sipVars) {
$answer = array();
if (empty($sipVars[SIP_FORM])) {
throw new CodeException("Missing 'form' in SIP. There might be something broken.", ERROR_MISSING_FORM_IN_SIP);
}
$recordId = empty($sipVars[SIP_RECORD_ID]) ? 0 : $sipVars[SIP_RECORD_ID];
// For r=0 (new) , 'dirty' will always succeed.
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] . "'");
$tableName = $tableVars[F_TABLE_NAME];
$formDirtyMode = $tableVars[F_DIRTY_MODE];
$feUser = $this->session->get(SESSION_FE_USER);
// Look for already existing dirty record.
$recordDirty = $this->db->sql("SELECT id, sip, feUser, qfqUserSessionCookie, dirtyMode FROM Dirty AS d WHERE d.tableName LIKE ? AND recordId=? ",
ROW_EXPECT_0_1, [$tableName, $recordId]);
if (count($recordDirty) == 0) {
// No dirty record found.
$answer = $this->writeDirty($recordId, $tableName, $formDirtyMode, $feUser);
} else {
$answer = $this->checkConflict($recordDirty, $formDirtyMode, $feUser);
}
return $answer;
}
/**
* @param array $recordDirty
* @param $currentFormDirtyMode
* @return array
*/
private function checkConflict(array $recordDirty, $currentFormDirtyMode, $feUser) {
$status = API_ANSWER_STATUS_CONFLICT;
if ($this->client[CLIENT_COOKIE_QFQ] == $recordDirty[QFQ_USER_SESSION_COOKIE]) {
$msg = "The record has already been tagged for editing by you (maybe in another browser tab)!";
$status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
} else {
if (empty($feUser)) {
$msgUser = "another user";
} else {
$msgUser = "user '$feUser'";
}
$msg = "The record has already been tagged for editing by $msgUser at " .
$recordDirty[COLUMN_CREATED] . " from " . $recordDirty[CLIENT_REMOTE_ADDRESS];
// Mandatory lock on Record or current Form?
if ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_TIMEOUT || $currentFormDirtyMode == DIRTY_MODE_TIMEOUT) {
$status = API_ANSWER_STATUS_CONFLICT;
} else {
$status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE;
}
}
return [API_STATUS => $status, API_MESSAGE => $msg];
}
/**
* Write a 'Dirty'-Record.
*
* @param $recordId
* @param $tableName
* @param $formDirtyMode
* @return array
* @throws CodeException
* @throws DbException
*/
private function writeDirty($recordId, $tableName, $formDirtyMode, $feUser) {
$record = $this->db->sql("SELECT * FROM $tableName WHERE id=?", ROW_EXPECT_1, [$recordId], "Record to tag 'dirty' not found.");
// Not all tables does have a modified column.
$recordModified = empty($record[COLUMN_MODIFIED]) ? 0 : $record[COLUMN_MODIFIED];
// Write 'dirty' record
$this->db->sql("INSERT INTO Dirty (`tableName`, `recordId`, `recordModified`, `feUser`, `qfqUserSessionCookie`, `dirtyMode`, `remoteAddress`, `created`) " .
"VALUES ( ?,?,?,?,?,?,?,? )", ROW_REGULAR, [$tableName, $recordId, $recordModified, $feUser, $this->client[CLIENT_COOKIE_QFQ], $formDirtyMode, $this->client[CLIENT_REMOTE_ADDRESS], date('YmdHis')]);
return [API_STATUS => API_ANSWER_STATUS_SUCCESS, API_MESSAGE => ''];
}
}
\ No newline at end of file
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 7/9/17
* Time: 3:14 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../../qfq/helper/Sanitize.php');
class Client {
public static function getParam() {
// copy GET and POST and SERVER Parameter. Priority: SERVER, POST, GET
$post = array();
$cookie = array();
$server = array();
$get = \qfq\Sanitize::urlDecodeArr($_GET);
if (isset($_POST)) {
$post = $_POST;
}
if (isset($_COOKIE[SESSION_NAME])) {
$cookie[CLIENT_COOKIE_QFQ] = $_COOKIE[SESSION_NAME];
}
// It's important to merge the SERVER array last: those entries shall overwrite client values.
if (isset($_SERVER)) {
$server = Sanitize::htmlentitiesArr($_SERVER); // $_SERVER values might be compromised.
}
$arr = array_merge($get, $post, $cookie, $server);
return Sanitize::normalize($arr);
}
}
\ No newline at end of file
......@@ -20,6 +20,7 @@ require_once(__DIR__ . '/../../qfq/store/Sip.php');
require_once(__DIR__ . '/../../qfq/store/T3Info.php');
require_once(__DIR__ . '/../../qfq/database/Database.php');
require_once(__DIR__ . '/../../qfq/store/Config.php');
require_once(__DIR__ . '/../../qfq/store/Client.php');
/*
* Stores:
......@@ -348,28 +349,7 @@ class Store {
*/
private static function fillStoreClient() {
// copy GET and POST and SERVER Parameter. Priority: SERVER, POST, GET
$arr = array();
if (isset($_GET)) {
$arr = array_merge($arr, $_GET);
}
$arr = \qfq\Sanitize::urlDecodeArr($arr);
if (isset($_POST)) {
$arr = array_merge($arr, $_POST);
}
if(isset($_COOKIE[SESSION_NAME])){
$arr[CLIENT_COOKIE_QFQ] = $_COOKIE[SESSION_NAME];
}
// It's important to merge the SERVER array last: those entries shall overwrite client values.
if (isset($_SERVER)) {
$server = Sanitize::htmlentitiesArr($_SERVER); // $_SERVER values might be compromised.
$arr = array_merge($arr, $server);
}
$arr = \qfq\Sanitize::normalize($arr);
$arr = Client::getParam();
self::setStore($arr, STORE_CLIENT, true);
......
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