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>
......@@ -221,8 +229,8 @@ abstract class AbstractBuildForm {
* If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
*
* @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
* @param int $recordId id of form or formElement
* @param array $param
* @param int $recordId id of form or formElement
* @param array $param
* @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
* @throws CodeException
*/
......@@ -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,12 +390,12 @@ 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
* @param int $feIdContainer
* @param array $json
* @param string $modeCollectFe
* @param bool $htmlElementNameIdZero
* @param bool $htmlElementNameIdZero
* @param string $storeUseDefault
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
......@@ -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);
/**
......@@ -634,16 +665,22 @@ abstract class AbstractBuildForm {
/**
* Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
*
* @param $htmlFormElementName
* @param $value
* @param $htmlFormElementName
* @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();
/**
......@@ -691,9 +728,9 @@ abstract class AbstractBuildForm {
* 'Generic Element Update': add via API_ELEMENT_UPDATE 'label' and 'note'.
* All collected data as array - will be later converted to JSON.
*
* @param string $htmlFormElementName
* @param string $htmlFormElementName
* @param string|array $value
* @param array $formElement
* @param array $formElement
* @return array
*/
private function getFormElementForJson($htmlFormElementName, $value, array $formElement) {
......@@ -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) {
......@@ -862,10 +899,10 @@ abstract class AbstractBuildForm {
* [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
* [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
*
* @param array $formElement
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json Return updates in this array - will be later converted to JSON.
* @param array $json Return updates in this array - will be later converted to JSON.
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string complete rendered HTML input element.
* @throws \qfq\UserFormException
......@@ -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) {
......@@ -1172,7 +1209,7 @@ abstract class AbstractBuildForm {
*
* @param array $formElement
* @param array $attributeList
* @param bool $flagOmitEmpty
* @param bool $flagOmitEmpty
* @return string
*/
private function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
......@@ -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
*/
......@@ -1288,11 +1325,11 @@ abstract class AbstractBuildForm {
* <input name="$htmlFormElementName" type="checkbox" [autofocus="autofocus"]
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
* @param array $formElement
* @param string $htmlFormElementName
* @param sgtring $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
* @return string
* @throws CodeException
* @throws \qfq\UserFormException
......@@ -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
*/
......@@ -1493,10 +1530,10 @@ 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 array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -1526,11 +1563,11 @@ abstract class AbstractBuildForm {
* </label>
* </div>
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attribute
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $attribute
* @param string $value
* @param array $json
* @return string
*/
public function constructCheckboxSingleButton(array $formElement, $htmlFormElementName, $attribute, $value, array &$json) {
......@@ -1583,11 +1620,11 @@ abstract class AbstractBuildForm {
* <input name="$htmlFormElementName" type="radio" [autofocus="autofocus"]
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attribute
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $attribute
* @param string $value
* @param array $json
* @return string
*/
public function constructCheckboxSinglePlain(array $formElement, $htmlFormElementName, $attribute, $value, array &$json) {
......@@ -1628,10 +1665,10 @@ 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 array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -1652,9 +1689,9 @@ abstract class AbstractBuildForm {
}
/**
* @param array $formElement
* @param $htmlFormElementName
* @param $htmlHidden
* @param array $formElement
* @param string $htmlFormElementName
* @param string $htmlHidden
* @throws CodeException
* @throws UserFormException
*/
......@@ -1677,13 +1714,13 @@ abstract class AbstractBuildForm {
* Layout: The Bootstrap Layout needs very special setup, the checkboxes are wrapped differently with <div class=checkbox>
* depending of if they aligned horizontal or vertical.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attributeBase
* @param $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $attributeBase
* @param string $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
* @return string
*/
public function constructCheckboxMultiButton(array $formElement, $htmlFormElementName, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
......@@ -1747,13 +1784,13 @@ abstract class AbstractBuildForm {
* Layout: The Bootstrap Layout needs very special setup, the checkboxes are wrapped differently with <div class=checkbox>
* depending of if they aligned horizontal or vertical.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $attributeBase
* @param $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $attributeBase
* @param string $value
* @param array $itemKey
* @param array $itemValue
* @param array $json
* @return string
*/
public function constructCheckboxMultiPlain(array $formElement, $htmlFormElementName, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) {
......@@ -1836,10 +1873,10 @@ abstract class AbstractBuildForm {
* Sometimes, it's usefull to precalculate values during formload and to submit them as hidden fields.
* To avoid any manipulation on those fields, the values will be transferred by SIP.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -1859,10 +1896,10 @@ abstract class AbstractBuildForm {
* <input name="$htmlFormElementName" type="radio" [autofocus="autofocus"]
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -1906,10 +1943,10 @@ abstract class AbstractBuildForm {
*
* </div>
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -1978,10 +2015,10 @@ abstract class AbstractBuildForm {
* <input name="$htmlFormElementName" type="radio" [autofocus="autofocus"]
* [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -2067,10 +2104,10 @@ abstract class AbstractBuildForm {
/**
* Builds a Selct (Dropdown) Box.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return mixed
* @throws CodeException
......@@ -2133,10 +2170,10 @@ abstract class AbstractBuildForm {
* Construct a HTML table of the subrecord data.
* Column syntax definition: https://wikiit.math.uzh.ch/it/projekt/qfq/qfq-jqwidgets/Documentation#Type:_subrecord
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -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
......@@ -2451,9 +2488,9 @@ abstract class AbstractBuildForm {
* url: The cell will be rendered as an <a> tag. The value will be exploded by '|'. $value[0] = href, value[1] = text.
* 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 array $control
* @param string $columnName
* @param string $columnValue
* @return string
*/
private function renderCell(array $control, $columnName, $columnValue) {
......@@ -2511,7 +2548,7 @@ abstract class AbstractBuildForm {
*
* @param string $formName if there is a form, specify that
* @param string $tableName if there is no form , specify the table from where to delete the record.
* @param int $recordId record to delete
* @param int $recordId record to delete
* @param string $mode
* * mode=RETURN_URL: return complete URL
* * mode=RETURN_SIP: returns only the sip
......@@ -2537,10 +2574,10 @@ abstract class AbstractBuildForm {
/**
* Build an Upload (File) Button.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws CodeException
......@@ -2618,10 +2655,10 @@ abstract class AbstractBuildForm {
* [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
*
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws UserFormException
......@@ -2731,10 +2768,10 @@ abstract class AbstractBuildForm {
* [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
*
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @return string
* @throws UserFormException
......@@ -2814,10 +2851,10 @@ abstract class AbstractBuildForm {
* Build a HTML 'textarea' element which becomes a TinyMCE Editor.
* List of possible plugins: https://www.tinymce.com/docs/plugins/
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $json
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param array $json
* @param string $mode
* @return string
* @throws \qfq\UserFormException
......@@ -2861,8 +2898,8 @@ abstract class AbstractBuildForm {
/**
* Parse $formElement[FE_EDITOR_*] settings and build editor settings.
*
* @param array $formElement
* @param $htmlFormElementName
* @param array $formElement
* @param string $htmlFormElementName
* @return array
*/
private function setEditorConfig(array $formElement, $htmlFormElementName) {
......@@ -2915,7 +2952,7 @@ abstract class AbstractBuildForm {
* Empty $settings are ok.
*
* @param string $prefix
* @param array $formElement
* @param array $formElement
* @return string
* @throws \qfq\UserFormException
*/
......@@ -2948,10 +2985,10 @@ abstract class AbstractBuildForm {
/**
* Build Grid JQW element.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param $fake
* @param array $formElement
* @param string $htmlFormElementName
* @param string $value
* @param string $fake
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
* @throws UserFormException
*/
......@@ -2963,11 +3000,11 @@ abstract class AbstractBuildForm {
/**
* Build Note.
*
* @param array $formElement
* @param $htmlFormElementName
* @param $value
* @param array $formElement