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

#4185 / Detect modified record

modifiedRecord.pu: State Diagram
Dirty.php: implement $recordHashMd5 to detect modified records.
OnArray.php: new getMd5()
AbstractBuildForm.php: implemented but not working update of hidden input 'recordHashMd5'. Add hidden input 'recordHashMd5'.
BuildFormBootstrap:  Add hidden input 'recordHashMd5'.
formEditor.sql: Rename 'Dirty.recordModified' to 'Dirty.recordHashMd5'.
parent 1c65cdf7
@startuml
actor Alice #FFBBBB
actor Bob #BBBBFF
== Detect modified record: on lock ==
Alice -> form: open page
form -> Alice: form, recordHashMd5='a'
...
Bob -> form: open page
form -> Bob: form, recordHashMd5='a'
...
Alice -> dirty: edit: action=lock, recordHashMd5='a' (hash from form!)
dirty -> Alice: status=success, lock_timeout=<secs>
...
Alice -> save: POST (incl. recordHashMd5)
save -> Alice: action=success, update new recordHashMd5='b' in form.
...
Bob -> dirty: edit: action=lock, recordHashMd5='a' (hash from form!)
dirty -> Bob: status=conclict, msg=record has changed, reload form
== Detect modified record: on save ==
Alice -> form: open page
form -> Alice: form, recordHashMd5='a'
...
Alice -> dirty: edit: action=lock, recordHashMd5='a' (hash from form!)
dirty -> Alice: status=success, lock_timeout=<secs>
...
note over Alice: some background task.
...
System -> System: change record.
...
Alice -> save: POST (incl. recordHashMd5)
save -> Alice: status=error, msg=record has changed, reload form
@enduml
......@@ -179,6 +179,14 @@ abstract class AbstractBuildForm {
} else {
$recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
$htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
if ($mode === FORM_SAVE && $recordId != 0) {
$record = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id=?", ROW_EXPECT_1, [$recordId], "Record to load not found.");
$newMd5 = OnArray::getMd5($record);
$json[] = [API_ELEMENT_UPDATE => [DIRTY_RECORD_HASH_MD5 => [API_ELEMENT_ATTRIBUTE => ['value' => $newMd5]]]];
// $json[] = [API_ELEMENT_UPDATE => [DIRTY_RECORD_HASH_MD5 => ['value' => $newMd5]]];
// $json[DIRTY_RECORD_HASH_MD5][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $newMd5;
}
}
// <form>
......@@ -250,8 +258,8 @@ abstract class AbstractBuildForm {
/**
* Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
*
* @param $item
* @param $value
* @param string $item
* @param string $value
* @param bool|false $flagOmitEmpty
* @return string
*/
......@@ -270,12 +278,29 @@ abstract class AbstractBuildForm {
* @return string
*/
public function getFormTag() {
$md5 = '';
$attribute = $this->getFormTagAtrributes();
$honeypot = $this->getHoneypotVars();
return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot;
$md5 = $this->buildRecordHashMd5();
return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot . $md5;
}
/**
* Build MD5 from the current record. Return HTML Input element.
*/
public function buildRecordHashMd5() {
$record = array();
$recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
if ($recordId != 0) {
$record = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id=?", ROW_EXPECT_1, [$recordId], "Record to load not found.");
}
return "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='hidden' value='" . OnArray::getMd5($record) . "'>";
}
/**
......@@ -301,6 +326,7 @@ abstract class AbstractBuildForm {
return $html;
}
/**
* Build an assoc array with standard form attributes.
*
......@@ -364,7 +390,7 @@ abstract class AbstractBuildForm {
/**
* Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
*
* @param $recordId
* @param int $recordId
* @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
* @param int $feIdContainer
* @param array $json
......@@ -382,6 +408,8 @@ abstract class AbstractBuildForm {
$storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
$html = '';
// $html .= $this->buildRecordHashMd5();
// The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
$skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER];
......@@ -615,6 +643,9 @@ abstract class AbstractBuildForm {
}
}
/**
*
*/
abstract public function fillWrapLabelInputNote($label, $input, $note);
/**
......@@ -635,15 +666,21 @@ abstract class AbstractBuildForm {
* Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
*
* @param $htmlFormElementName
* @param $value
* @param string $value
* @return string
*/
public function buildNativeHidden($htmlFormElementName, $value) {
return '<input type="hidden" name="' . $htmlFormElementName . '" value="' . htmlentities($value) . '">';
}
/**
*
*/
abstract public function tail();
/**
*
*/
abstract public function doSubrecords();
/**
......@@ -776,10 +813,10 @@ abstract class AbstractBuildForm {
/**
* Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
*
* @param $feMode
* @param $hidden
* @param $disabled
* @param $required
* @param string $feMode
* @param string $hidden
* @param string $disabled
* @param string $required
* @throws \qfq\UserFormException
*/
private function getFeMode($feMode, &$hidden, &$disabled, &$required) {
......@@ -1046,8 +1083,8 @@ abstract class AbstractBuildForm {
* Checks if $sql contains a SELECT statement.
* Check for existence of a LIMIT Parameter. If not found add one.
*
* @param $sql
* @param $limit
* @param string $sql
* @param int $limit
* @return string Checked and maybe extended $sql statement.
* @throws \qfq\UserFormException
*/
......@@ -1110,7 +1147,7 @@ abstract class AbstractBuildForm {
/**
* Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
*
* @param $column
* @param string $column
* @return bool|int a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
*/
private function getColumnSize($column) {
......@@ -1197,8 +1234,8 @@ abstract class AbstractBuildForm {
*
* For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
*
* @param $type
* @param $data
* @param string $type
* @param string $data
* @return string
* @throws \qfq\UserFormException
*/
......@@ -1289,8 +1326,8 @@ abstract class AbstractBuildForm {
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param sgtring $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
* @return string
......@@ -1415,8 +1452,8 @@ abstract class AbstractBuildForm {
/**
* Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
*
* @param $column
* @param $fieldType
* @param string $column
* @param string $fieldType
* @return array
* @throws UserFormException
*/
......@@ -1494,8 +1531,8 @@ abstract class AbstractBuildForm {
* Build a Checkbox based on two values. Either in HTML plain layout or with Bootstrap Button class.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -1527,9 +1564,9 @@ abstract class AbstractBuildForm {
* </div>
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attribute
* @param $value
* @param string $htmlFormElementName
* @param string $attribute
* @param string $value
* @param array $json
* @return string
*/
......@@ -1584,9 +1621,9 @@ abstract class AbstractBuildForm {
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attribute
* @param $value
* @param string $htmlFormElementName
* @param string $attribute
* @param string $value
* @param array $json
* @return string
*/
......@@ -1629,8 +1666,8 @@ abstract class AbstractBuildForm {
* Build a Checkbox based on two values. Either in HTML plain layout or with Bootstrap Button class.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -1653,8 +1690,8 @@ abstract class AbstractBuildForm {
/**
* @param array $formElement
* @param $htmlFormElementName
* @param $htmlHidden
* @param string $htmlFormElementName
* @param string $htmlHidden
* @throws CodeException
* @throws UserFormException
*/
......@@ -1678,9 +1715,9 @@ abstract class AbstractBuildForm {
* depending of if they aligned horizontal or vertical.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attributeBase
* @param $value
* @param string $htmlFormElementName
* @param string $attributeBase
* @param string $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
......@@ -1748,9 +1785,9 @@ abstract class AbstractBuildForm {
* depending of if they aligned horizontal or vertical.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attributeBase
* @param $value
* @param string $htmlFormElementName
* @param string $attributeBase
* @param string $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
......@@ -1837,8 +1874,8 @@ abstract class AbstractBuildForm {
* To avoid any manipulation on those fields, the values will be transferred by SIP.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -1860,8 +1897,8 @@ abstract class AbstractBuildForm {
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -1907,8 +1944,8 @@ abstract class AbstractBuildForm {
* </div>
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -1979,8 +2016,8 @@ abstract class AbstractBuildForm {
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -2068,8 +2105,8 @@ abstract class AbstractBuildForm {
* Builds a Selct (Dropdown) Box.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return mixed
......@@ -2134,8 +2171,8 @@ abstract class AbstractBuildForm {
* Column syntax definition: https://wikiit.math.uzh.ch/it/projekt/qfq/qfq-jqwidgets/Documentation#Type:_subrecord
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -2243,10 +2280,10 @@ abstract class AbstractBuildForm {
* - check if there is an SELECT statement for the subrecords.
* - determine &$nameColumnId
*
* @param $formElement
* @param $primaryRecord
* @param $rcText
* @param $nameColumnId
* @param array $formElement
* @param array $primaryRecord
* @param string $rcText
* @param string $nameColumnId
* @return bool
* @throws \qfq\UserFormException
*/
......@@ -2298,9 +2335,9 @@ abstract class AbstractBuildForm {
* x_id = 12 (constant)
*
*
* @param $formElement
* @param $targetRecordId
* @param $record
* @param array $formElement
* @param string $targetRecordId
* @param array $record
* @return string
* @throws UserFormException
*/
......@@ -2350,7 +2387,7 @@ abstract class AbstractBuildForm {
/**
* Get the name for the given form $formName. If not found, return ''.
*
* @param $formName
* @param string $formName
* @return string tableName for $formName
* @throws CodeException
* @throws DbException
......@@ -2452,8 +2489,8 @@ abstract class AbstractBuildForm {
* E.g. $value = 'www.math.uzh.ch/?id=45&v=234|Show details for Modul 123' >> <a href="www.math.uzh.ch/?id=45&v=234">Show details for Modul 123</a>
*
* @param array $control
* @param $columnName
* @param $columnValue
* @param string $columnName
* @param string $columnValue
* @return string
*/
private function renderCell(array $control, $columnName, $columnValue) {
......@@ -2538,8 +2575,8 @@ abstract class AbstractBuildForm {
* Build an Upload (File) Button.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -2619,8 +2656,8 @@ abstract class AbstractBuildForm {
*
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -2732,8 +2769,8 @@ abstract class AbstractBuildForm {
*
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -2815,8 +2852,8 @@ abstract class AbstractBuildForm {
* List of possible plugins: https://www.tinymce.com/docs/plugins/
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode
* @return string
......@@ -2862,7 +2899,7 @@ abstract class AbstractBuildForm {
* Parse $formElement[FE_EDITOR_*] settings and build editor settings.
*
* @param array $formElement
* @param $htmlFormElementName
* @param string $htmlFormElementName
* @return array
*/
private function setEditorConfig(array $formElement, $htmlFormElementName) {
......@@ -2949,9 +2986,9 @@ abstract class AbstractBuildForm {
* Build Grid JQW element.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param $fake
* @param string $htmlFormElementName
* @param string $value
* @param string $fake
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @throws UserFormException
*/
......@@ -2964,8 +3001,8 @@ abstract class AbstractBuildForm {
* Build Note.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @param array $json
* @return mixed
......@@ -2982,8 +3019,8 @@ abstract class AbstractBuildForm {
* Build Pill.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @return mixed
*/
......@@ -2995,8 +3032,8 @@ abstract class AbstractBuildForm {
* Build a HTML fieldset. Renders all assigned FormElements inside the fieldset.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return mixed
......@@ -3043,7 +3080,7 @@ abstract class AbstractBuildForm {
/**
* @param array $formElementArr
* @param $tgMaxCopies
* @param int $tgMaxCopies
* @return array
*/
private function fillFeSpecNativeCheckboxWithTgMax(array $formElementArr, $tgMaxCopies) {
......@@ -3061,8 +3098,8 @@ abstract class AbstractBuildForm {
* If there are already vlaues for the formElements, fill as much copies as values exist
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return mixed
......
......@@ -445,8 +445,9 @@ class BuildFormBootstrap extends AbstractBuildForm {
}
$honeypot = $this->getHoneypotVars();
$md5 = $this->buildRecordHashMd5();
return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot;
return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot . $md5;
}
/**
......
......@@ -260,6 +260,7 @@ const ERROR_DIRTY_DELETE_RECORD = 2201;
const ERROR_DIRTY_UNKNOWN_ACTION = 2202;
const ERROR_DIRTY_MISSING_LOCK = 2203;
const ERROR_DIRTY_ALREADY_LOCKED = 2204;
const ERROR_DIRTY_RECORD_MODIFIED = 2205;
//
// Store Names: Identifier
......@@ -1120,7 +1121,7 @@ const DIRTY_FE_USER = 'feUser';
const DIRTY_EXPIRE = 'expire';
const DIRTY_TABLE_NAME = 'tableName';
const DIRTY_RECORD_ID = 'recordId';
const DIRTY_RECORD_MODIFIED = 'recordModified';
const DIRTY_RECORD_HASH_MD5 = 'recordHashMd5';
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';
......
......@@ -270,12 +270,13 @@ class QuickFormQuery {
// Check (and release) dirtyRecord.
if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) {
$dirty = new Dirty();
// Comment as long as long as 4127 is open
$answer = $dirty->checkDirtyAndRelease($formMode, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
$this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $recordId);
$this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $recordId, true);
// in case of a conflict, return immediately
if($answer[API_STATUS]!= API_ANSWER_STATUS_SUCCESS) {
if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
$answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
return $answer;
}
}
......@@ -445,7 +446,7 @@ class QuickFormQuery {
* used parameters. Do this by building a new SIP with the new recordId.
*
* @param array $formSpec
* @param $recordId
* @param int $recordId
* @return array
* @throws CodeException
* @throws UserFormException
......@@ -700,7 +701,7 @@ class QuickFormQuery {
* Depending on $mode various formSpec fields might be adjusted.
* E.g.: the form title is not important during a delete.
*
* @param $mode
* @param string $mode
* @param array $form
* @return array
*/
......@@ -816,10 +817,11 @@ class QuickFormQuery {
/**
* Check if loading of the given form is permitted. If not, throw an exception.
*
* @param $formNameFoundInStore
* @param string $formNameFoundInStore
* @param string $formMode
* @return bool 'true' if SIP exists, else 'false'
* @throws CodeException
* @throws UserFormException
* @throws \qfq\CodeException
* @throws \qfq\UserFormException
* @internal param $foundInStore
*/
private function validateForm($formNameFoundInStore, $formMode) {
......@@ -933,8 +935,11 @@ class QuickFormQuery {
}
unset($data[API_ELEMENT_UPDATE]);
}
if(count($data)>0){
$collect[] = $data;
}
}
return $collect;
}
......@@ -996,10 +1001,10 @@ class QuickFormQuery {
/**
* Based on the given SIP, create a new uniqe SIP by copying the relevant old params and taking the new recordId..
*
* @param $sipArray
* @param $recordId
* @param array $sipArray
* @param int $recordId
*/
private function newRecordCreateSip($sipArray, $recordId) {
private function newRecordCreateSip(array $sipArray, $recordId) {
$tmpParam = array();
......
......@@ -48,6 +48,9 @@ class Dirty {
$this->session = Session::getInstance($phpUnit);
$this->client = Client::getParam();
if (!isset($this->client[DIRTY_RECORD_HASH_MD5])) {
$this->client[DIRTY_RECORD_HASH_MD5] = '';
}
$this->db = new Database();
}
......@@ -81,7 +84,7 @@ class Dirty {
switch ($this