diff --git a/Documentation-develop/CONFIG.md b/Documentation-develop/CONFIG.md index 27f440c5bbf8d493c87546244f564348c8701404..b71f07f1b81d3839ee5bd16fde6744276de914aa 100644 --- a/Documentation-develop/CONFIG.md +++ b/Documentation-develop/CONFIG.md @@ -19,17 +19,109 @@ IMATHUZH\Qfq\Core\Store\Store::fillStoreSystem() IMATHUZH\Qfq\Core\Store\Config::getConfigArray() IMATHUZH\Qfq\Core\Store\Config::readConfig() -To create a new QFQ Config option: +How to create a new config option +================================== -ext_conf_template.txt: +To create a new config option, you have to make the changes specified below in the following files. -* create new entry -* set a default value in ext_conf_template.txt +ext_conf_template.txt +--------------------- -config::setDefaults() +The following variables must be set: -* Define defaults if nothing is given +**cat** +(category where the new option will be located, in the extension configuration of your typo3 backend) -DatabaseUpdate=>checkT3QfqConfig(): +**type** (datatype of the config option) -* In case existing installations should get a new default during QFQ update. +possible datatypes: +* boolean (checkbox) +* color (colorpicker) +* int (integer value) +* int+ (positive integer value) +* integer (integer value) +* offset (offset) +* options (option select) +```type=options[label1=value1,label2=value2,value3];``` +* small (small text field) +* string (text field) +* user (user function) +```type=user[Vendor\MyExtensionKey\ViewHelpers\MyConfigurationClass->render];``` +* wrap (wrap field) + +**label** (title and description of the config option, split by ":") + +**myVariable** (name the variable of the config option and assign a default value) + +**Example** + +``` +# cat=config/config; type=boolean; label=MyLabel:Description +myVariable = value1 +``` + +Constants.php +------------- + +Best practice would be defining constants with the name of your variable, +since this name should never be changed. + +``` +const SYSTEM_MY_VARIABLE = 'myVariable'; +const F_MY_VARIABLE = 'SYSTEM_MY_VARIABLE'; +const FE_MY_VARIABLE = 'SYSTEM_MY_VARIABLE'; +``` + + +Config.php +--------------------- + +In the function **setDefaults()** a default value should be set. +</br>Important in case of new variables: new variables do not exist in QFQ extension config and do not get the default defined in ext_conf_template.txt + +``` +default = [ + ... + SYSTEM_MY_VARIABLE => 'true', + ... +]; +``` + +Support.php +---------- + +To set the default value of a FormElement you can use the **setFeDefaults()** function. +</br>Wich provides the default value for the FormElement using the **system store**. +</br>The **system store** contains all the variables defined in the typo3 extension configuration. + +``` +self::setIfNotSet($formElement, FE_MY_VARIABLE, $store->getVar(SYSTEM_MY_VARIABLE, STORE_SYSTEM)); +``` + +StoreTest.php +------------- + +The expected default value must be specified in the **testConfigIniDefaultValues()** function so that the unit test can run without errors. + +``` +$expect = [ + ... + SYSTEM_MY_VARIABLE => 'true', + ... +]; +``` + +How to handle variables +-------------------------------------- + +Here is an example on how you would go about handling variables that are defined on all levels (SYSTEM -> Form -> FormElement) + +``` +$myVar = $store->getVar(SYSTEM_MY_VARIABLE, STORE_SYSTEM); +if(isset($this->formSpec[F_MY_VARIABLE])){ + $myVar = $this->formSpec[F_MY_VARIABLE]; +} +if(isset($this->formElement[FE_MY_VARIABLE])){ + $myVar = $this->formElement[FE_MY_VARIABLE]; +} +``` diff --git a/Documentation/Concept.rst b/Documentation/Concept.rst index aabbc5431b0340bc0213dc2e2f6b91bb4f16044c..d6d7dbde60ef0247c61951b4a04e9c5a8b63c871 100644 --- a/Documentation/Concept.rst +++ b/Documentation/Concept.rst @@ -89,103 +89,100 @@ QFQ Keywords (Bodytext) **All of these parameters are optional.** -+-------------------------+---------------------------------------------------------------------------------+ -| Name | Explanation | -+=========================+=================================================================================+ -| form | | Formname. | -| | | Static: **form = person** | -| | | By SIP: **form = {{form:SE}}** | -| | | By SQL: **form = {{SELECT c.form FROM Config AS c WHERE c.id={{a:C}} }}** | -+-------------------------+---------------------------------------------------------------------------------+ -| r | | <record id>. The form will load the record with the specified id. | -| | | Static: **r = 123** | -| | | By SQL: **r = {{SELECT ...}}** | -| | | If not specified, the SIP parameter 'r' is used. | -+-------------------------+---------------------------------------------------------------------------------+ -| dbIndex | E.g. `dbIndex = {{indexQfq:Y}}` Select a DB index. Only necessary if a | -| | different than the standard DB should be used. | -+-------------------------+---------------------------------------------------------------------------------+ -| debugShowBodyText | If='1' and :ref:`configuration`:*showDebugInfo: yes*, shows a | -| | tooltip with bodytext | -+-------------------------+---------------------------------------------------------------------------------+ -| sqlLog | Overwrites :ref:`configuration`: :ref:`SQL_LOG` . Only affects `Report`, | -| | not `Form`. | -+-------------------------+---------------------------------------------------------------------------------+ -| sqlLogMode | Overwrites :ref:`configuration`: :ref:`SQL_LOG_MODE<SQL_LOG_MODE>` . | -| | Only affects `Report`, not `Form`. | -+-------------------------+---------------------------------------------------------------------------------+ -| render | See :ref:`report-render`. Overwrites :ref:`configuration`: render. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.fbeg | Start token for every field (=column) | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.fend | End token for every field (=column) | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.fsep | Separator token between fields (=columns) | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.fskipwrap | Skip wrapping (via fbeg, fsep, fend) of named columns. Comma separated list of | -| | column id's (starting at 1). See also the special column name '_noWrap' to | -| | suppress wrapping. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.shead | Static start token for whole <level>, independent if records are selected | -| | Shown before `head`. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.stail | Static end token for whole <level>, independent if records are selected. | -| | Shown after `tail`. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.head | Dynamic start token for whole <level>. Only if at least one record is select. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.tail | Dynamic end token for whole <level>. Only if at least one record is select. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.rbeg | Start token for row. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.rbgd | Alternating (per row) token. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.rend | End token for row. Will be rendered **before** subsequent levels are processed | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.renr | End token for row. Will be rendered **after** subsequent levels are processed | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.rsep | Seperator token between rows | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.sql | SQL Query | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.twig | Twig Template | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.althead | If <level>.sql has no rows selected (empty), these token will be rendered. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.altsql | If <level>.sql has no rows selected (empty) or affected (delete, update, insert)| -| | the <altsql> will be fired. Note: Sub queries of <level> are not fired, even if | -| | <altsql> selects some rows. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.content | | *show* (default): content of current and sub level are directly shown. | -| | | *hide*: content of current and sub levels are **stored** and not shown. | -| | | *hideLevel*: content of current and sub levels are **stored** and only sub | -| | | levels are shown. | -| | | *store*: content of current and sub levels are **stored** and shown. | -| | | To retrieve the content: `{{<level>.line.content}}`. | -| | | See :ref:`syntax-of-report` | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.count | Current row index. Will be replaced before the query is fired in case of | -| <alias>.line.count | ``<level>``/``<alias>`` is an outer/previous level or it will be replaced after | -| | a query is fired in case ``<level>``/``<alias>`` is the current level. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.total | Total rows (MySQL ``num_rows`` for *SELECT* and *SHOW*, MySQL ``affected_rows`` | -| <alias>.line.total | for *UPDATE* and *INSERT*. | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.insertId | Last insert id for *INSERT*. | -| <alias>.line.insertId | | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.content | Show content of `<level>`/`<alias>` (content have to be stored via | -| <alias>.line.content | <level>.content=... or <alias>.content=...). | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.altCount | Like 'line.count' but for 'alt' query. | -| <alias>.line.altCount | | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.altTotal | Like 'line.total' but for 'alt' query. | -| <alias>.line.altTotal | | -+-------------------------+---------------------------------------------------------------------------------+ -| <level>.line.altInsertId| Like 'line.insertId' but for 'alt' query. | -| <alias>.line.altInsertId| | -+-------------------------+---------------------------------------------------------------------------------+ ++--------------------------------+---------------------------------------------------------------------------------+ +| Name | Explanation | ++================================+=================================================================================+ +| form | | Formname. | +| | | Static: **form = person** | +| | | By SIP: **form = {{form:SE}}** | +| | | By SQL: **form = {{SELECT c.form FROM Config AS c WHERE c.id={{a:C}} }}** | ++--------------------------------+---------------------------------------------------------------------------------+ +| r | | <record id>. The form will load the record with the specified id. | +| | | Static: **r = 123** | +| | | By SQL: **r = {{SELECT ...}}** | +| | | If not specified, the SIP parameter 'r' is used. | ++--------------------------------+---------------------------------------------------------------------------------+ +| dbIndex | E.g. `dbIndex = {{indexQfq:Y}}` Select a DB index. Only necessary if a | +| | different than the standard DB should be used. | ++--------------------------------+---------------------------------------------------------------------------------+ +| debugShowBodyText | If='1' and :ref:`configuration`:*showDebugInfo: yes*, shows a | +| | tooltip with bodytext | ++--------------------------------+---------------------------------------------------------------------------------+ +| sqlLog | Overwrites :ref:`configuration`: :ref:`SQL_LOG` . Only affects `Report`, | +| | not `Form`. | ++--------------------------------+---------------------------------------------------------------------------------+ +| sqlLogMode | Overwrites :ref:`configuration`: :ref:`SQL_LOG_MODE<SQL_LOG_MODE>` . | +| | Only affects `Report`, not `Form`. | ++--------------------------------+---------------------------------------------------------------------------------+ +| render | See :ref:`report-render`. Overwrites :ref:`configuration`: render. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.fbeg | Start token for every field (=column) | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.fend | End token for every field (=column) | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.fsep | Separator token between fields (=columns) | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.fskipwrap | Skip wrapping (via fbeg, fsep, fend) of named columns. Comma separated list of | +| | column id's (starting at 1). See also the :ref:`special-column-names` '_noWrap' | +| | to suppress wrapping. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.shead | Static start token for whole <level>, independent if records are selected | +| | Shown before `head`. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.stail | Static end token for whole <level>, independent if records are selected. | +| | Shown after `tail`. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.head | Dynamic start token for whole <level>. Only if at least one record is select. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.tail | Dynamic end token for whole <level>. Only if at least one record is select. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.rbeg | Start token for row. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.rbgd | Alternating (per row) token. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.rend | End token for row. Will be rendered **before** subsequent levels are processed | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.renr | End token for row. Will be rendered **after** subsequent levels are processed | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.rsep | Seperator token between rows | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.sql | SQL Query | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.twig | Twig Template | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.althead | If <level>.sql has no rows selected (empty), these token will be rendered. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.altsql | If <level>.sql has no rows selected (empty) or affected (delete, update, insert)| +| | the <altsql> will be fired. Note: Sub queries of <level> are not fired, even if | +| | <altsql> selects some rows. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level>.content | | *show* (default): content of current and sub level are directly shown. | +| | | *hide*: content of current and sub levels are **stored** and not shown. | +| | | *hideLevel*: content of current and sub levels are **stored** and only sub | +| | | levels are shown. | +| | | *store*: content of current and sub levels are **stored** and shown. | +| | | To retrieve the content: `{{<level>.line.content}}`. | +| | | See :ref:`syntax-of-report` | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.count | Current row index. Will be replaced before the query is fired in case of | +| | ``<level>``/``<alias>`` is an outer/previous level or it will be replaced after | +| | a query is fired in case ``<level>``/``<alias>`` is the current level. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.total | Total rows (MySQL ``num_rows`` for *SELECT* and *SHOW*, MySQL ``affected_rows`` | +| | for *UPDATE* and *INSERT*. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.insertId | Last insert id for *INSERT*. | +| <alias>.line.insertId | | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.content | Show content of `<level>`/`<alias>` (content have to be stored via | +| | <level>.content=... or <alias>.content=...). | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.altCount | Like 'line.count' but for 'alt' query. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.altTotal | Like 'line.total' but for 'alt' query. | ++--------------------------------+---------------------------------------------------------------------------------+ +| <level|alias>.line.altInsertId | Like 'line.insertId' but for 'alt' query. | ++--------------------------------+---------------------------------------------------------------------------------+ .. _`report-render`: diff --git a/Documentation/Form.rst b/Documentation/Form.rst index 23f25081d7dbe32da48a1df36b7b4a450914d4b2..677d91d975dc1ef3710fc0a92d58b9169529c179 100644 --- a/Documentation/Form.rst +++ b/Documentation/Form.rst @@ -126,31 +126,41 @@ FormElement processing order: .. _record_locking: + Record locking -------------- -Forms and 'record delete'-function support basic record locking. A user opens a form: starting with the first change of content, a -record lock will be acquired in the background. If the lock is denied (e.g. another user already owns a lock on the record) an -alert is shown. -By default the record lock mode is 'exclusive' and the default timeout is 15 minutes. Both can be configured per form. -The general timeout can also be configured in :ref:`configuration` (will be copied to the form during creating the form). - -The lock timeout counts from the first change, not from the last modification on the form. - -If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again. +Support for record locking is given with mode: -A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record. +* *exclusive*: user can't force a write. -If a `Form` acts on further records (e.g. via FE action), those further records are not protected by this basic record locking. + * Including a timeout (default 15 mins recordLockTimeoutSeconds in :ref:`configuration`) for maximum lock time. -If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied. +* *advisory*: user is only warned, but allowed to overwrite. +* *none*: no bookkeeping about locks. -If there are different locking modes in multiple forms, the most restricting mode applies for the current lock. +Details: + +* For 'new' records (r=0) there is no locking at all. +* The record locking protection is based on the `tablename` and the `record id`. Different `Forms`, with the same primary table, + will be protected by record locking. +* Action-`FormElements` updating non primary table records are not + protected by 'record locking': the QFQ record locking is *NOT 100%*. +* The 'record locking' mode will be specified per `Form`. If there are multiple Forms with different modes, and there is + already a lock for a `tablename` / `record id` pair, the most restrictive will be applied. +* A user opens a form: starting with the first change of content, a record lock will be acquired in the background. If + the lock is denied (e.g. another user already owns a lock on the record) an alert is shown. This means: the lock timeout + counts from the first change, not from the last modification on the form. +* If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again. +* A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record. +* If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied. +* If there are different locking modes in multiple forms, the most restricting mode applies for the current lock. +* If the same user opens the same recording in different tabs or browsers, the user has the possibility to skip a lock. Exclusive ^^^^^^^^^ -An existing lock on a record forbids any write action on that record. +An existing lock on a record forbids any write action on that record. Exception: locks owned by the same user might be overwritten. Advisory ^^^^^^^^ @@ -3158,30 +3168,6 @@ To automatically delete slave records, use a form and create `beforeDelete` Form You might also check the form 'form' how the slave records 'FormElement' will be deleted. -.. _locking-record: - -Locking Record / Form ---------------------- - -Support for record locking is given with mode: - -* *exclusive*: user can't force a write. - - * Including a timeout (default 15 mins recordLockTimeoutSeconds in :ref:`configuration`) for maximum lock time. - -* *advisory*: user is only warned, but allowed to overwrite. -* *none*: no bookkeeping about locks. - -For 'new' records (r=0) there is no locking at all. - -The record locking protection is based on the `tablename` and the `record id`. Different `Forms`, with the same primary table, -will be protected by record locking. On the other side, action-`FormElements` updating non primary table records are not -protected by 'record locking': the QFQ record locking is *NOT 100%*. - -The 'record locking' mode will be specified per `Form`. If there are multiple Forms with different modes, and there is -already a lock for a `tablename` / `record id` pair, the most restrictive will be applied. - - Best practice ------------- diff --git a/Documentation/Release.rst b/Documentation/Release.rst index 051c77d211869f7dfde85be9cdddbab32f85c284..0203f61e86520496b7c6ce1f6d5a35f4d3d468ba 100644 --- a/Documentation/Release.rst +++ b/Documentation/Release.rst @@ -262,6 +262,214 @@ Bug Fixes * #15523 / Search/Refactor broken for Multi-DB. * #15626 / Multi-DB: FormEditor save error. +Version 23.10.1 +--------------- + +Date: 22.10.2023 + +Notes +^^^^^ + +Features +^^^^^^^^ + +* #15098 / Implemented qfqFunction in QFQ variable for usage in forms. +* Doc: Replace many places single back tick by double back tick. Add hint for 'Row size too large'. Added Enis & Jan + as Developer. Add hint use mysqldump with one row per record. + +Bug Fixes +^^^^^^^^^ + +* #17003 / inline edit - dark mode has wrong css path. +* #17075 / Fix broken '... AS _restClient'. +* #17091 / upload_Incorrect_integer_value_fileSize. +* #15795 / Upload: download button not shown after pressing save. +* RTD: Fix broken readthedocs rendering. + +Version 23.10.0 +--------------- + +Date: 05.10.2023 + +Features +^^^^^^^^ + +* #16350 / QFQ Table 'FormSubmiLog': update recordid after insert. +* #16350 / sql.log: reference to FormSubmitLog entry. All SQL statements generated by one HTTP Post (Form Submit) can + be identified. +* #16350 / Do not log Dirty. +* #16350 / If the T3 instance is behind a proxy, log HTTP_X_REAL_IP instead of REMOTE_ADDR in logfiles. +* #16584 / FormEditor Report: Default without statistics. +* #16589 / Implemented language configuration in backend for tt-content type qfq. +* #16798 / Report inline edit v2. Improved search inside inline edit report. Whole content will be searched. Added + ability to switch the editor to dark mode. +* Doc: Refactor description of {{random:V}}. +* Doc: Add config option 'protectedFolderCheck'. + +Bug Fixes +^^^^^^^^^ + +* #16573 / Fixed wrong built date and datetime string if default value was given. +* #16574 / Added multiple siteConfigurations compatibility for typo3 v10 and 11. +* #16616 / Fixed typeahead api query response problem if typeahead sql is not used. +* #16664 / Fix multi db user error. +* #16975 / Fix problem if a 'Form Submit' contains more than 64kB data. This can happen easily for 'fabric' elements. + +Version 23.6.4 +-------------- + +Date: 26.06.2023 + +Bug Fixes +^^^^^^^^^ + +* #16485 / TypeAhead: strpos() string, array given error. +* #16488 / Missing default values break saving records. New custom FE.parameter.defaultValue. +* #16491 / FE Typ Upload - JS failure: document.querySelector() is null. + +Version 23.6.3 +-------------- + +Date: 22.06.2023 + +Bug Fixes +^^^^^^^^^ + +* #16478 / Rest API: Fixed stream_get_contents failure in PHP8.1 Generic Error. + +Version 23.6.2 +-------------- + +Date: 21.06.2023 + +Bug Fixes +^^^^^^^^^ + +* #16475 / Spontaneous spaces in HTML emails. +* #16476 / SQL columns Text/Blog with default empty string becomes "''" + + +Version 23.6.1 +-------------- + +Date: 16.06.2023 + +Notes +^^^^^ + +* QFQ is Typo3 V11 compatible + +Bug Fixes +^^^^^^^^^ + +* #16372 / Upload Element 'Undefined index htmlDownloadButton' und 'unknown mode ID' +* #16381 / Form title dynamic update broken. +* #16392 / Reevaluate sanitize class for each store. +* Fix db column enum dropdown 'data truncated' +* DB Update 'alter index' pre check if exists +* FE: Upload - fix undefined index. + +Version 23.6.0 +-------------- + +Date: 09.06.2023 + +Features +^^^^^^^^ + +* Typo3 V11 compatible +* #9579 / Multiform with Process Row. +* #15770 / httpOrigin: Parameter missing in QFQ Config. +* #16285 / PHP V8 Migration. +* #16364 / T3 >=V10 pageSlug and ForwardPage. +* Add .phpunit.result.cache to .gitignore. +* Add bullet-orange.gif. +* Add index createdFeUserFormId to table FormSubmitLog. +* Doc: Add link to icons. Update use-case self-registration and add info in case of namesake. Unify word 'spam' to 'junk'. + Add bootstrap links. +* Remove Form/FormElement parameter 'feGroup'. +* QFQ Typo3 Updatewizard: Sort output by page and tt-content ordering. Add page uid an title to be reported. + Inline Edit: add tt_content uid and header as tooltip. +* Update package.json. +* Update to PHPUnit version 9. + +Bug Fixes +^^^^^^^^^ + +* #12468 / Form: Dynamic Update Form.title after save. +* #14636 / 'Clear Me'-cross lighter and 2 px up. +* #15445 / Doc: JS Output Widged added. +* #15497 / Delete link: a) broken for 'table', b) broken with 'r:3'. +* #15527 / Form/subrecord: class 'qfq-table-50' no impact. +* #15654 / Form FE required 'Undefined index Error'. +* #15659 / Multi Form: Datetime Picker not aligned. +* #15691 / T3 V10: Export PDF (PDF Generator) invalid link causes error. +* #15726 / Formelement upload stays required when changed from required to hidden. +* #15729 / Form: text readonly input should show newline as '<br>'. +* #15747 / Typeahead does not work with special character. Added croatian special chars (Ć,ć,ÄŒ,Ä,Ä,Ä‘,Å ,Å¡,Ž,ž) to alnumx + whitelist. +* #15763 / Search/Refactor: a) search for ID, b) message if nothing is found - New highlighting for disabled content. + Added output for not found in search. +* #15773 / TypeAhead missing check type warning. +* #15813 / Pressing 'Enter' in Form asks: Do you really want to delete the record? +* #15875 / Unecessary qfq-config save. +* #15905 / FE type time undefined index. +* #15913 / Refactor: Footer displayed at the wrong place. +* #15921 / Frontend edit Report Codemirror resizeable. +* #16004 / Template Group Fields broken. +* #16046 / Datepicker not initialized in Template Group. +* #16051 / Dropdown menu: Download file broken with ZIP File. +* #16055 / Dropdown option with render mode 3 format problem. +* #16064 / Dynamic Update Checkbox and Form Store in parameter. +* #16073 / saveButtonActive is disabled after first save. +* #16201 / Character Count Class Quotation Mark. +* #16204 / TypeAhead Prefetch Expects String Array Given. +* #16228 / extraButtonPassword unhide broken. +* #16264 / Multi DB Broken since QFQ V23.2.0. +* #16273 / TinyMCE image upload path incorrect for T3 v10 and upwards. + +Version 23.3.1 +-------------- + +Date: 31.03.2023 + +Bug Fixes +^^^^^^^^^ + +* #15920 / QFQ variable evaluation broken: wrong variable name + +Version 23.3.0 +-------------- + +Date: 30.03.2023 + +Features +^^^^^^^^ + +* #15491 / Search refactor redesign and T3 V10 compatiblity: underscore character, counter fixed, page alias and slug handling. +* #15529 / Form/subrecord: Design Titel / Box. +* #15570 / Changed DB handling in class FormAction. Fixed multi-db problem with this. Included change for check of + existing form editor report. +* #15579 / Ability for searching all possible characters, includes not replaced QFQ variables. +* #15627 / Added character count and maxLength feature for TinyMCE. +* Add various icons to documentation. +* Doc Form.rst: reference 'orderColumn'. +* Doc Report.rst: fix typo, add icons, improved example for tablesorter. +* Add indexes for table FormSubmitLog. +* FormEditor: Show Tablename in pill 'table definition'. +* FormEditor: FE subrecord > show container id below name. + +Bug Fixes +^^^^^^^^^ + +* #14754 / Using double quotes in tableview config caused sql error. +* #15483 / Protected folder check fixed. Changed default of wget, preventing errors. Changed handling from protected + folder check, new once a day. +* #15521 / FormEditor assigns always container, even none is selected. Change handling of form variables from type select, + radio and checkbox. Expected 0 from client request instead of empty string. +* #15523 / Search/Refactor broken for Multi-DB. +* #15626 / Multi-DB: FormEditor save error. + Version 23.2.0 -------------- diff --git a/Documentation/Report.rst b/Documentation/Report.rst index a46b869a0c4d46bde7fc0d035d73a14be55e2c43..9ac07ad5b681bc94767079f34ef71e6206af5903 100644 --- a/Documentation/Report.rst +++ b/Documentation/Report.rst @@ -398,8 +398,8 @@ To get the same result, the following is also possible:: '|p:/export', '|t:Download') AS _pdf -Nesting of levels (report notation `numeric`) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Nesting of levels: `numeric` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Levels can be nested. E.g.:: @@ -417,8 +417,8 @@ This is equal to:: 10.5.sql = SELECT ... 10.5.head = ... -Nesting of levels (report notation `alias`) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Nesting of levels: `alias` +^^^^^^^^^^^^^^^^^^^^^^^^^^ Levels can be nested without levels. E.g.:: @@ -442,7 +442,7 @@ An alias can be used instead of levels. E.g.:: myAlias { sql = SELECT ... - myAlias2 { + nextAlias { sql = SELECT ... head = ... } @@ -460,12 +460,14 @@ Allowed characters for an alias: [a-zA-Z0-9_-]. .. important:: -The first level determines whether report notation `numeric` or `alias` is used. Using an alias or no level triggers report notation `alias`. -It requires the use of delimiters throughout the report. A combination with the notation '10.sql = ...' is not possible. +The first level determines whether report notation `numeric` or `alias` is used. Using an alias or no level triggers +report notation `alias`. It requires the use of delimiters throughout the report. A combination with the notation +'10.sql = ...' is not possible. .. important:: -Report notation `alias` does not require that each level be assigned an alias. If an alias is used, it must be on the same line as the opening delimiter. +Report notation `alias` does not require that each level be assigned an alias. If an alias is used, it must be on the +same line as the opening delimiter. Leading / trailing spaces ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/extension/Classes/Core/BodytextParser.php b/extension/Classes/Core/BodytextParser.php index bcaaa1e8ce98e90de0e63473048bd0aea207e448..9e4fd8d50caa681e325ec0b27eb42043dc5dcea0 100644 --- a/extension/Classes/Core/BodytextParser.php +++ b/extension/Classes/Core/BodytextParser.php @@ -217,14 +217,14 @@ class BodytextParser { // This later determines the notation mode // Possible values for $firstToken: '10', '10.20', '10.sql=...', '10.head=...', 'myAlias {', 'myAlias{' - // Values such as 'form={{form:SE}}' are disregarded + // Values such as 'form={{form:SE}}' are valid but not parsed as a level/alias. if (empty($firstToken) && 1 !== preg_match('/^(' . TOKEN_VALID_LIST . ')\s*=/', $row)) { $firstToken = (strpos($row, $nestingOpen) !== false) ? trim(substr($row, 0, strpos($row, $nestingOpen))) : $row; } - // If the open delimiter is missing while using an alias, this is necessary to get the correct error message later on - // Starts a new line if the previous line only contained '}' - // It prevents that the lines '}' and 'myAlias' will be joined + // If the open delimiter is missing while using an alias, this is necessary to get the correct error message later on + // Starts a new line if the previous line only contained '}' + // It prevents that the lines '}' and 'myAlias' will be joined } elseif ($full === $nestingClose) { $data[] = $full; @@ -249,15 +249,15 @@ class BodytextParser { // Combines line numbers ($key) from tt-content record with content ($value) from corresponding line: [line => content] // E.g. [0 => 4, 1 => "[", 2 => "sql = SELECT ...", 3 => "[", 4 => "sql = SELECT ...", ...]: line 0 is empty - foreach($reportLines as $key => $value) { + foreach ($reportLines as $key => $value) { $reportLines[$value] = $data[$key]; } // Removes every element that is not an SQL statement: [line => content] // E.g. [2 => "sql = SELECT ...", 4 => "sql = SELECT ...", ...] - foreach($reportLines as $key => $value) { + foreach ($reportLines as $key => $value) { if (strpos($value, '=') !== false) { - $arr = explode('"', $value,2); + $arr = explode('"', $value, 2); if (strpos($arr[0], TOKEN_SQL) === false) { unset($reportLines[$key]); } @@ -392,7 +392,7 @@ class BodytextParser { $index = substr_count($pre, NESTING_TOKEN_OPEN); // Check for report notation 'alias' - if($notationMode === TOKEN_NOTATION_ALIAS) { + if ($notationMode === TOKEN_NOTATION_ALIAS) { $aliasLevel = null; // $firstToken === $level checks if we are in the 'first loop' @@ -430,7 +430,7 @@ class BodytextParser { // Removes alias or level added by user to continue auto numbering scheme // E.g. User input: {\nsql = SELECT ...\n}\nmyAlias{\nsql = SELECT ...\n} // $pre = "1.sql = SELECT ...\nmyAlias" -> $pre = "1.sql = SELECT ...\n" - $pre = substr($pre,0, strrpos($pre, PHP_EOL) + $adjustLength); + $pre = substr($pre, 0, strrpos($pre, PHP_EOL) + $adjustLength); } else { // Remove 'level' from last line @@ -462,7 +462,7 @@ class BodytextParser { if (strpos($valueResult, '=')) { $arr = explode("=", $valueResult, 2); if (strpos($arr[0], TOKEN_SQL) !== false) { - $reportLines[$keyLines] = str_replace('.' . TOKEN_SQL , '', trim($arr[0])); + $reportLines[$keyLines] = str_replace('.' . TOKEN_SQL, '', trim($arr[0])); } else { continue; } diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index a9af77a6fb3026cd0f68dc924ab60a3350b9a999..3e93c4e96b0b10206d46428fad0254eb524be50f 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -1658,7 +1658,7 @@ const COLUMN_UID = 'uid'; const COLUMN_HEADER = 'header'; const COLUMN_TITLE = 'title'; const COLUMN_SUBHEADER = 'subheader'; - +const COLUMN_EXPIRE = 'expire'; const INDEX_PHP = 'index.php'; // QuickFormQuery.php diff --git a/extension/Classes/Core/Form/Dirty.php b/extension/Classes/Core/Form/Dirty.php index 457190674aee58a4df82eb765fe74724fee7d4da..6c4c42dc6f0eaeac9720e34da74dda66873ea60e 100644 --- a/extension/Classes/Core/Form/Dirty.php +++ b/extension/Classes/Core/Form/Dirty.php @@ -10,6 +10,7 @@ namespace IMATHUZH\Qfq\Core\Form; use IMATHUZH\Qfq\Core\Database\Database; use IMATHUZH\Qfq\Core\Helper\OnArray; +use IMATHUZH\Qfq\Core\Helper\Support; use IMATHUZH\Qfq\Core\Store\Client; use IMATHUZH\Qfq\Core\Store\Session; use IMATHUZH\Qfq\Core\Store\Sip; @@ -67,7 +68,7 @@ class Dirty { $this->client[DIRTY_RECORD_HASH_MD5] = ''; } $this->doDbArray($dbIndexData, $dbIndexQfq); - + $this->store = Store::getInstance(); } /** @@ -119,7 +120,6 @@ class Dirty { return [API_STATUS => 'success', API_MESSAGE => '']; } - $this->store = Store::getInstance(); $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM); $this->dbIndexData = empty($sipVars[PARAM_DB_INDEX_DATA]) ? $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM) : $sipVars[PARAM_DB_INDEX_DATA]; @@ -180,13 +180,15 @@ class Dirty { if ($formDirtyMode == DIRTY_MODE_NONE) { $answer = [API_STATUS => 'success', API_MESSAGE => '']; } else { - // No dirty record found. + // No dirty record found: create lock $answer = $this->writeDirty($this->client[SIP_SIP], $recordId, $tableVars, $feUser, $rcMd5, $tabUniqId); } } else { if ($tabUniqId == $recordDirty[TAB_UNIQ_ID]) { + // In case it's the same tab (page reload): OK $answer = [API_STATUS => 'success', API_MESSAGE => '']; } else { + // Here is probably a conflict. $answer = $this->conflict($recordDirty, $formDirtyMode, $primaryKey); } } @@ -221,6 +223,7 @@ class Dirty { } /** + * Aquire lock conflict detected * * @param array $recordDirty * @param string $currentFormDirtyMode @@ -233,17 +236,20 @@ class Dirty { */ private function conflict(array $recordDirty, $currentFormDirtyMode, $primaryKey) { $status = API_ANSWER_STATUS_CONFLICT; - $at = "at " . $recordDirty[COLUMN_CREATED] . " from " . $recordDirty[DIRTY_REMOTE_ADDRESS]; + $until = "until " . date_format(date_create($recordDirty[COLUMN_EXPIRE]), "d.m.Y H:i:s"); - // Compare modified timestamp + // Compare modified timestamp: in case there is a lock conflict and current form is based on outdated data: force reload. if ($this->isRecordModified($recordDirty[DIRTY_TABLE_NAME], $primaryKey, $recordDirty[DIRTY_RECORD_ID], $recordDirty[DIRTY_RECORD_HASH_MD5], $dummy)) { - return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime. Please reload the form, edit and save again. [2]']; + return [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => 'The record has been modified in the meantime (your changes are lost). Please reload the form, edit and save again.']; } - if ($this->client[CLIENT_COOKIE_QFQ] == $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE]) { - $msg = "The record has already been locked by you (maybe in another browser tab) $at!"; - $status = ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) ? API_ANSWER_STATUS_CONFLICT : API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE; + // Conflict for same user / same QFQ Session: the user can force aquire lock. + // Hint: after this, the form with the first lock still thinks it has the lock - that one will get a 'record modified in the meantime' on save. + $userMatch = ($recordDirty[DIRTY_FE_USER] != '' && $recordDirty[DIRTY_FE_USER] == $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3)); + if ($userMatch || $this->client[CLIENT_COOKIE_QFQ] == $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE]) { + $msg = "Record already locked (by you)"; + $status = API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE; } else { if (empty($recordDirty[DIRTY_FE_USER])) { @@ -252,7 +258,7 @@ class Dirty { $msgUser = "user '" . $recordDirty[DIRTY_FE_USER] . "'"; } - $msg = "The record has already been locked by $msgUser at $at."; + $msg = "Record already locked by $msgUser $until."; // Mandatory lock on Record or current Form? if ($recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE || $currentFormDirtyMode == DIRTY_MODE_EXCLUSIVE) { @@ -292,6 +298,8 @@ 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 + + $userAgent = $this->store->getVar(CLIENT_HTTP_USER_AGENT, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); $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, @@ -366,20 +374,18 @@ class Dirty { return LOCK_NOT_FOUND; } - if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) { + $msgUser = (empty($recordDirty[DIRTY_FE_USER])) ? "another user" : "user '" . $recordDirty[DIRTY_FE_USER] . "'"; + $rc = LOCK_FOUND_CONFLICT; + + $userMatch = ($recordDirty[DIRTY_FE_USER] != '' && $recordDirty[DIRTY_FE_USER] == $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3)); + if ($userMatch || $recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) { $msgUser = "you"; - } else { - $msgUser = (empty($recordDirty[DIRTY_FE_USER])) ? "another user" : "user '" . $recordDirty[DIRTY_FE_USER] . "'"; + $rc = LOCK_FOUND_OWNER; } - $msgAt = "at " . $recordDirty[COLUMN_CREATED] . " from " . $recordDirty[DIRTY_REMOTE_ADDRESS]; - $msg = "The record has been locked by $msgUser $msgAt"; + $until = "until " . date_format(date_create($recordDirty[COLUMN_EXPIRE]), "d.m.Y H:i:s"); + $msg = "The record has been locked by $msgUser $until."; - // Is the dirtyRecord mine? - if ($recordDirty[DIRTY_QFQ_USER_SESSION_COOKIE] == $this->client[CLIENT_COOKIE_QFQ]) { - return LOCK_FOUND_OWNER; - } else { - return LOCK_FOUND_CONFLICT; - } + return $rc; } /** diff --git a/extension/Classes/Core/QuickFormQuery.php b/extension/Classes/Core/QuickFormQuery.php index 0ae795506db9fddea7218c97df957b668f2221b3..34855b931a7e55ab27ea89beb3d3540be76a4495 100644 --- a/extension/Classes/Core/QuickFormQuery.php +++ b/extension/Classes/Core/QuickFormQuery.php @@ -183,7 +183,7 @@ class QuickFormQuery { // Adds aliases together with level to TYPO3 store // E.g. [alias.1 => "myAlias", alias.1.2 => "mySecondAlias", ...] foreach ($btp->aliases as $key => $value) { - $this->store->setVar(TOKEN_ALIAS. '.' . $key, $value, STORE_TYPO3); + $this->store->setVar(TOKEN_ALIAS . '.' . $key, $value, STORE_TYPO3); } } @@ -617,7 +617,10 @@ class QuickFormQuery { $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq); $recordDirty = array(); $rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg); - if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) { + + // Switch to READONLY + if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) + && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) { $this->formSpec[F_MODE_GLOBAL] = F_MODE_READONLY; } } @@ -1592,7 +1595,6 @@ class QuickFormQuery { F_DO_NOT_LOG_COLUMN, FE_FILE_MAX_FILE_SIZE, - F_FE_DATA_PATTERN_ERROR_SYSTEM, // Not a classical element to overwrite by form definition, but should be copied to detect changes per custom setting. ]; diff --git a/extension/Classes/Core/Report/Variables.php b/extension/Classes/Core/Report/Variables.php index f50d9bba43db3a4a6e465fa9b01d9aca0cb1956b..a9ee655a8d5a7cc2a3e7900ea46ab0427b12a498 100644 --- a/extension/Classes/Core/Report/Variables.php +++ b/extension/Classes/Core/Report/Variables.php @@ -123,7 +123,7 @@ class Variables { // No numeric value implies that an alias was used if (!is_numeric($alias)) { // Get typo3 store - // Aliases are saved like [alias.1 => "myAlias", alias1.2 => "mySecondAlias", ...] + // Aliases are saved like [alias.1 => "myAlias", alias.1.2 => "mySecondAlias", ...] $storeT3 = Store::getStore(STORE_TYPO3); // Check for matching value of $alias $match = array_search($alias, $storeT3, true); @@ -153,12 +153,12 @@ class Variables { $arr = explode(':', $matches[3]); $data = OnString::escape($arr[1] ?? '', $this->resultArray[$fullLevel][$varName], $rcFlagWipe); } - // This is for the specific case, that the variable references its own level - // E.g. myAlias { \n sql = SELECT '{{myAlias.line.count}}' \n } - // Note: This is only used for line.count and line.total, because non-existing variables must stay unchanged - // E.g. myAlias { \n sql = SELECT 1 \n } \n { \n sql = SELECT '{{myAlias.varName}}' \n } - // '{{myAlias.varName}}' will not be changed to '{{1.varName}}' - } elseif($varName === LINE_COUNT || $varName === LINE_TOTAL) { + // This is for the specific case, that the variable references its own level + // E.g. myAlias { \n sql = SELECT '{{myAlias.line.count}}' \n } + // Note: This is only used for line.count and line.total, because non-existing variables must stay unchanged + // E.g. myAlias { \n sql = SELECT 1 \n } \n { \n sql = SELECT '{{myAlias.varName}}' \n } + // '{{myAlias.varName}}' will not be changed to '{{1.varName}}' + } elseif ($varName === LINE_COUNT || $varName === LINE_TOTAL) { // myAlias needs to be replaced by the level // E.g. {{1.2.line.count}} $data = $matches[0]; diff --git a/extension/Tests/Unit/Core/Form/DirtyTest.php b/extension/Tests/Unit/Core/Form/DirtyTest.php index 3fe8a8c1098c372b81aedfe20d9012d6e3f9df3f..f8a18f000fa7d8e1165cf280d25f6aef6c3ff69b 100644 --- a/extension/Tests/Unit/Core/Form/DirtyTest.php +++ b/extension/Tests/Unit/Core/Form/DirtyTest.php @@ -9,7 +9,7 @@ namespace IMATHUZH\Qfq\Tests\Unit\Core\Form; use IMATHUZH\Qfq\Core\Database\Database; - + use IMATHUZH\Qfq\Core\Form\Dirty; use IMATHUZH\Qfq\Core\Store\Session; use IMATHUZH\Qfq\Core\Store\Sip; @@ -17,6 +17,8 @@ use IMATHUZH\Qfq\Tests\Unit\Core\Database\AbstractDatabaseTest; require_once(__DIR__ . '/../Database/AbstractDatabaseTest.php'); +const MSG_RECORD_ALREADY_LOCKED = 'Record already locked'; + /* * Open to check * - FORM_DELETE @@ -352,7 +354,7 @@ class DirtyTest extends AbstractDatabaseTest { // Alice lock again $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -508,7 +510,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -700,8 +702,8 @@ class DirtyTest extends AbstractDatabaseTest { // Alice lock again $result = $dirty->process(); - $msg = 'The record has already'; - $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg]; + $msg = MSG_RECORD_ALREADY_LOCKED; + $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg]; // cut IP, User and Timestamp $result[API_MESSAGE] = substr($result[API_MESSAGE], 0, strlen($msg)); @@ -855,7 +857,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -977,7 +979,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT_ALLOW_FORCE, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -1021,7 +1023,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = 'Record already locked '; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -1065,7 +1067,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg]; // cut IP, User and Timestamp @@ -1109,7 +1111,7 @@ class DirtyTest extends AbstractDatabaseTest { $result = $dirty->process(); - $msg = 'The record has already'; + $msg = MSG_RECORD_ALREADY_LOCKED; $expected = [API_STATUS => API_ANSWER_STATUS_CONFLICT, API_MESSAGE => $msg]; // cut IP, User and Timestamp diff --git a/extension/Tests/Unit/Core/Store/StoreTest.php b/extension/Tests/Unit/Core/Store/StoreTest.php index 449738d1ce62b92c38529e0215f9cdb79de54682..c50ddf77a148896829fce635faf577fd535483bd 100644 --- a/extension/Tests/Unit/Core/Store/StoreTest.php +++ b/extension/Tests/Unit/Core/Store/StoreTest.php @@ -439,7 +439,6 @@ class StoreTest extends TestCase { // SYSTEM_DO_NOT_LOG_COLUMN => SYSTEM_DO_NOT_LOG_COLUMN_DEFAULT, SYSTEM_PROTECTED_FOLDER_CHECK => 1, SYSTEM_CMD_WGET => 'wget >/dev/null 2>&1' - ]; $body = json_encode([ diff --git a/extension/ext_conf_template.txt b/extension/ext_conf_template.txt index a0e4908e21f6dccd33aa3062927fa1efce5e712b..677b072fd45c842303994dbe66a4cdc2ea782135 100644 --- a/extension/ext_conf_template.txt +++ b/extension/ext_conf_template.txt @@ -133,7 +133,6 @@ encryptionMethod = protectedFolderCheck = 1 - # cat=form-config/config; type=string; label=Dirty record lock timeout (seconds):Default is '900'. Time in seconds to lock a record, starting from the first modification. If lock expires, it is acquired again on the next modification. recordLockTimeoutSeconds = 900 diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js index 0a26cb5d6cee905a14460516281f87bd95973989..fae0ac550e00509376f3e34c188b987b99d30fec 100644 --- a/javascript/src/QfqForm.js +++ b/javascript/src/QfqForm.js @@ -213,7 +213,7 @@ var QfqNS = QfqNS || {}; }]; if (obj.data.status == "conflict_allow_force") { messageButtons.push({ - label: "Ignore", + label: "Continue", eventName: 'ignore' }); } @@ -278,10 +278,14 @@ var QfqNS = QfqNS || {}; break; case "conflict_allow_force": messageType = "warning"; - messageButtons.push({ - label: "Ignore", + + messageButtons = [{ + label: "Continue", eventName: 'ignore' - }); + }, { + label: "Cancel", + eventName: 'reload' + }]; break; case "error": messageType = "error";