diff --git a/Documentation-develop/CONFIG.md b/Documentation-develop/CONFIG.md new file mode 100644 index 0000000000000000000000000000000000000000..27f440c5bbf8d493c87546244f564348c8701404 --- /dev/null +++ b/Documentation-develop/CONFIG.md @@ -0,0 +1,35 @@ +Config +====== + +During QFQ bootstrap three config files are read: + +* qfq.project.path.php (QFQ) +* fileadmin/protected/qfqProject/qfq.json (QFQ) +* typo3conf/LocalConfiguration.php (Typo3 > QFQ) + +Get config +========== + +Stacktrace bis die LocalConfiguration.php gelesen wird: + +IMATHUZH\Qfq\Core\QuickFormQuery->__construct() +IMATHUZH\Qfq\Core\Store\Store->__construct() +IMATHUZH\Qfq\Core\Store\Store::getInstance() +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: + +ext_conf_template.txt: + +* create new entry +* set a default value in ext_conf_template.txt + +config::setDefaults() + +* Define defaults if nothing is given + +DatabaseUpdate=>checkT3QfqConfig(): + +* In case existing installations should get a new default during QFQ update. diff --git a/Documentation-develop/DRAGANDDROP.md b/Documentation-develop/DRAGANDDROP.md index 700705263d0ed40ebb2373a4e0ec6607dbb24bd5..ee9a5f011985904f568aa351234cb1de329d39a0 100644 --- a/Documentation-develop/DRAGANDDROP.md +++ b/Documentation-develop/DRAGANDDROP.md @@ -1,6 +1,8 @@ -# Drag And Drop +Drag And Drop +============= -## Sort +Sort +---- Initialize a dnd container by adding the class "qfq-dnd" Set container object class to `class="qfq-dnd qfq-dnd-sort"`. @@ -20,7 +22,8 @@ Request will be sent containing following GET variables: Example: http://something/bla?dragId=uno&dragPosition=1&setTo=before&hoverId=tre&hoverPosition=3 -## Drag'n'drop in forms +Drag'n'drop in forms +-------------------- * For FormElement.typ=subrecord DND is automatically detected. * The encoded SIP contains: dnd-subrecord-form-id, dnd-subrecord-form-table, dnd-subrecord-id, dndTable, oderColumn, diff --git a/Documentation-develop/FORM.md b/Documentation-develop/FORM.md new file mode 100644 index 0000000000000000000000000000000000000000..046779a2bc42531b94cfc46e77052d866514a14d --- /dev/null +++ b/Documentation-develop/FORM.md @@ -0,0 +1,3 @@ +Bootstrap QFQ Form +================== + diff --git a/Documentation-develop/FORMEDITOR.md b/Documentation-develop/FORMEDITOR.md index e2486452b117d4ba95d5cf755b0881f1bc1598cc..192c2b84cf5d1aba6e927f1bbb04b643e52515d5 100644 --- a/Documentation-develop/FORMEDITOR.md +++ b/Documentation-develop/FORMEDITOR.md @@ -1,8 +1,10 @@ -# Formeditor +Formeditor +========== The Formeditor is defined in Resources/Private/Form/form.json, formElement.json, formJson.json -# Changes +Changes +======= Any modifications on the Formeditor have to be applied in the JSON files. It is fine to do all customizations and create the JSON file (T3 page 'form' > click on JSON for the wished form) and copy the while content to form.json. diff --git a/Documentation-develop/HTML.md b/Documentation-develop/HTML.md index 51294185d545a3de361bcd2513600b0c2f534a6e..cd4927d9543fd099917b0dd8c884485cf60efa38 100644 --- a/Documentation-develop/HTML.md +++ b/Documentation-develop/HTML.md @@ -1,52 +1,53 @@ -# HTML +HTML +==== This document explains the HTML markup used by QFQ. -## Hooks +Hooks +----- -Hooks are used on the Client to gather information required for -asynchronous requests and to add predefined event handlers to HTML Elements. +Hooks are used on the Client to gather information required for asynchronous requests and to add predefined event +handlers to HTML Elements. +form.data-toggle="validator" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -### form.data-toggle="validator" +Adding the attribute `data-toggle="validator"` to a `<form>` element, enables the Bootstrap Validator on that HTML Form. -Adding the attribute `data-toggle="validator"` to a `<form>` element, -enables the Bootstrap Validator on that HTML Form. +.data-sip ^^^^^^^^^ - -### .data-sip - -Asynchronous requests require to pass a SID to the Server. Elements -triggering an asynchronous request, may gather the SIP from the +Asynchronous requests require to pass a SID to the Server. Elements triggering an asynchronous request, may gather the +SIP from the `data-sip` attribute assigned to the HTML Form Element. - -### .class="record-delete" +.class="record-delete" +^^^^^^^^^^^^^^^^^^^^^^ HTML Form Buttons having the class `record-delete` set, will get an `onclick` handler attached by `QfqNS.QfqRecordList`. Each `<button>` also requires an `data-sip` attribute. +.data-load="" +^^^^^^^^^^^^^ -### .data-load="" - -HTML Form Elements having the attribute `data-load`, will trigger a -call to `api/load.php` upon change. +HTML Form Elements having the attribute `data-load`, will trigger a call to `api/load.php` upon change. ### id="save-button" ### id="close-button" ### id="delete-button" ### id="form-new-button" -## Typeahead +Typeahead +--------- Typeahead capable text input elements will be defined by the following attributes: - -### .class='qfq-typeahead' -### .data-typeahead-sip +.class='qfq-typeahead' +^^^^^^^^^^^^^^^^^^^^^^ + +.data-typeahead-sip ^^^^^^^^^^^^^^^^^^^ -The SIP will store: +The SIP will store: Use with SQL: `typeAheadSql` @@ -56,43 +57,43 @@ Use with LDAP: `typeAheadLdap` * `typeAheadLdapSearch` * `typeAheadLdapValuePrintf` * `typeAheadLdapKeyPrintf` - -### .data-typeahead-limit + +.data-typeahead-limit ^^^^^^^^^^^^^^^^^^^^^ * Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value. The server default is 20. -### .data-typeahead-minlength +.data-typeahead-minlength ^^^^^^^^^^^^^^^^^^^^^^^^^ * Defines the string minlength, typed by the user, before the first lookup is started. Default is 2. - -### data-typeahead-pedantic + +data-typeahead-pedantic ^^^^^^^^^^^^^^^^^^^^^^^ * If present, only suggested values are allowed in the input element +Tags Form Element +----------------- -## Tags Form Element - -The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional +The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional to the attributes for Typeahead (see above). Mockups can be found in `mockup/typahead.php` -### .data-typeahead-tags +.data-typeahead-tags ^^^^^^^^^^^^^^^^^^^^ * If present, the field becomes a tag field -### .data-typeahead-tag-delimiters +.data-typeahead-tag-delimiters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * List of ascii key codes of the keys which may be pressed to add a new tag when typing. -### .value +.value ^^^^^^ * JSON encoded list of key value pairs of existing tags. e.g. `[{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]` - -### POST data + +POST data ^^^^^^^^^ * JSON encoded list of key value pairs of the selected tags. e.g. diff --git a/Documentation-develop/LAYOUT.md b/Documentation-develop/LAYOUT.md index d5479cab0f60b78dbb9fcf715bf8a5cce1cda7c0..71ab753b0b15c30222cc59b5a0347dbc181cb75d 100644 --- a/Documentation-develop/LAYOUT.md +++ b/Documentation-develop/LAYOUT.md @@ -1,4 +1,5 @@ -= Plain = +Plain +===== <form> @@ -13,16 +14,19 @@ </form> # Subrecord + <table> <tr><th> id </th> </tr> <tr><td> 1 </td></tr> <tr><td> 2 </td></tr> </table> -= Table = +Table +===== + <form> <table> - + <tr> # Element 1 <td>Title</td> <td><input type="input"></td> <td>note</td> @@ -45,9 +49,12 @@ </td> </tr> </table> + </form> -# Subrecord +Subrecord +========= + <table> <tr><th> id </th> </tr> <tr><td> 1 </td></tr> @@ -56,7 +63,9 @@ -= Bootstrap = +Bootstrap +========= + <div class="container-fluid"> # Ttitle <div class="row hidden-xs"> diff --git a/Documentation-develop/REST.md b/Documentation-develop/REST.md index 64fa9b10b850cba3c2f386f9dd2eb30ac036ea8b..d3864a4f23cfe425b5506423fd6c69a087b80a0a 100644 --- a/Documentation-develop/REST.md +++ b/Documentation-develop/REST.md @@ -1,4 +1,3 @@ -==== REST ==== diff --git a/Documentation-develop/TABLESORTER.md b/Documentation-develop/TABLESORTER.md index 7c4dd06c3a3a943aa2dd1a6416d46a5bed8a5fb1..85ddf46c081730db6321373e7ac2ba706db5aafc 100644 --- a/Documentation-develop/TABLESORTER.md +++ b/Documentation-develop/TABLESORTER.md @@ -1,4 +1,3 @@ -=========== Tablesorter =========== diff --git a/Documentation-develop/diagram/bootstrap.drawio b/Documentation-develop/diagram/bootstrap.drawio index efebceceaa1c330ff51c2b98150eca2714d29fc2..4cfe061089e31f9ec79715a3e09504ee6357ba51 100644 --- a/Documentation-develop/diagram/bootstrap.drawio +++ b/Documentation-develop/diagram/bootstrap.drawio @@ -1 +1 @@ -<mxfile host="Electron" modified="2021-12-13T20:56:29.538Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.6.5 Chrome/80.0.3987.86 Electron/8.0.0 Safari/537.36" etag="XShEQdH8GtIbQazLTkWy" version="12.6.5" type="device"><diagram id="9q-i_z5IJf1DyX_JrZ7c" name="Page-1">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</diagram></mxfile> \ No newline at end of file +<mxfile host="Electron" modified="2022-12-11T13:03:38.492Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.6.5 Chrome/80.0.3987.86 Electron/8.0.0 Safari/537.36" etag="0kF6e3kMB-1GKugcZT_4" version="12.6.5" type="device" pages="3"><diagram id="9q-i_z5IJf1DyX_JrZ7c" name="Overview">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</diagram><diagram id="PixdtdoYfBAjwVCCUgM2" name="QFQ-Bootstrap">3ZZdb5swFEB/DY+V+C59bJOmrbRMo9lUdS+RMRdwYjA1JkB//ZxiQhiR2k5NJkWKkDnXvibn3jho1iSt7zjKkzkLgWqmHtaaNdVM88rw5HULmha4rtWCmJOwRUYPFuQVFNQVLUkIxWCiYIwKkg8hZlkGWAwY4pxVw2kRo8NdcxTDCCwwomP6REKRtNRz9J7fA4mTbmdDV5EUdZMVKBIUsmoPWbeaNeGMiXaU1hOgW3edF+RHgc1/Nxcvz+WqWno/biC4aJPNPrNk9xU4ZOKfUy+D7ys3jesNepha0/Uc9Oa+S71BtFS+FoJxObyWnxjEQ1YIlGFQBkTTaeWszELYptY166ZKiIBFjvA2Wsk+kiwRKZV3hhxGhNIJo4y/rbXACB24lLwQnK1hL3LlXlrIlRH1WMAF1H/V8R0Jxq4ysqOBpSB4I9epLLaniqm62eiKW/W9sWPJXl+4iiHVjvEuda9cDpT1T1TAHFUgg0qCKRIoQMXXmg8ReBE+ZN7FHgTRCc2b+gnNO8njryX+ubrOUhxtHuFu/S1XP5exeb8keD1jPPVLUNt8lf/Iw4AP+g88x3b0I/o37Q90vnMk/y9PulEFdBnPMXlFqV0luX7Avz/z23+CiMTn6t30/rf38ZkflISG244/V+n2ocPmpNLHxzyHnHFxrsadIx4v8rZ/63qL7b26Wrd/AA==</diagram><diagram id="BMNIQ1UUD8PApaxZDuDF" name="Form">nZNNU4MwEIZ/DUdnApHaXqWoF09Uy3jLkC2JEwgTgkB/vUEWKPbg1BO7z37Avrt4NCq6Z8Mq8ao5KC8gvPPo3gsC3yeBewykH8nDLhxBbiTHpAUk8gwICdJGcqhXiVZrZWW1hpkuS8jsijFjdLtOO2m1fmvFcrgCScbUNT1KbsVItyFZ+AvIXNh5YIwUbEpGUAvGdXuBaOzRyGhtR6voIlCDeJMuPN3ujuH+vmYHkpx6P/lU5G5s9nRLyTyCgdL+u/UbqM7E70FaHzZpcI7khyyxhHwx1aBeOKvtJwGNbkoOQxPi0cdWSAtJxbIh2rqTcUzYQjnPdya2A2Oh+6X/Hx/vz4q6UwRdgDW9q8Mu0w76tdsuG/UnJi62uUHG8IjyufEilDNQq8ldVvoTu/gxaPwN</diagram></mxfile> \ No newline at end of file diff --git a/Documentation/Form.rst b/Documentation/Form.rst index a3171562e7a6094c70944dbf525bf2d63d879a4d..7e68e6be73ba1138e768177f3c1d3a6131924341 100644 --- a/Documentation/Form.rst +++ b/Documentation/Form.rst @@ -210,7 +210,7 @@ Form Settings +-------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+ |Forward Mode | 'auto | close | no | url | url-skip-history | url-sip | url-sip-skip-history' (Default: auto): See :ref:`form-forward`. | +-------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+ -|Forward (Mode) Page | a) URL / Typo3 page slug or b) Forward Mode (via '{{...}}') or combination of a) & b). See :ref:`form-forward`. | +|Forward (Mode) Page | a) URL / Typo3 page slug or b) Forward Mode (via '{{...}}') or combination of a) & b). See :ref:`form-forward`. | +-------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+ |labelAlign | Label align (default/left/center/right)/ Default: 'default' (defined by Config). _`form-label-align` | +-------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+ @@ -437,7 +437,7 @@ Form.parameter +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ | typeAheadLdap | | Enable LDAP as 'Typeahead' data source. | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| typeAheadLdapSearch | string | Regular LDAP search expression. E.g.: `(|(cn=*?*)(mail=*?*))`  | +| typeAheadLdapSearch | string | Regular LDAP search expression. E.g.: `(|(cn=*?*)(mail=*?*))` | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ | typeAheadLdapValuePrintf | string | Value formatting of LDAP result, per entry. E.g.: `'%s / %s / %s', mail, roomnumber, telephonenumber` | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ @@ -504,16 +504,18 @@ Form.parameter +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ | formSubmitLogMode | string | Overwrite default from :ref:`configuration` | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| sessionTimeoutSeconds | int | Overwrite default from :ref:`configuration` . See :ref:`sessionTimeoutSeconds`. | +| sessionTimeoutSeconds | int | Overwrite default from :ref:`configuration`. See :ref:`sessionTimeoutSeconds`. | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| maxFileSize | int | Overwrite default from :ref:`configuration` . | +| maxFileSize | int | Overwrite default from :ref:`configuration` | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| requiredPosition | int | See :ref:`requiredPosition` . | +| requiredPosition | int | See :ref:`requiredPosition` | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ | clearMe | 0 / 1 | Overwrite default from :ref:`configuration`. Show a small 'x' in every input or textarea to clear field. | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ | rememberLastPill | 0 / 1 | Overwrite default from :ref:`configuration`. On form load, bring last used pill to front | +-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| doNotLogColumn | string | Overwrite default from :ref:`configuration`. Comma separated list of Form-Element names. | ++-----------------------------+--------+----------------------------------------------------------------------------------------------------------+ * Example in field Form.parameter:: @@ -1381,12 +1383,25 @@ General input for any text. * *hideZero* = 0|1 (optional): `with hideZero=1` a '0' in the value will be replaced by an empty string. * *emptyMeansNull* = [0|1] (optional): with `emptyMeansNull` or `emptyMeansNull=1` a NULL value will be written if the value is an empty string - * *inputType* = number (optional). Typically the HTML tag 'type' will be 'text', 'textarea' or 'number' (detected automatically). - If necessary, the HTML tag 'type' might be forced to a specific given value. + * *inputType* = number (optional). Typically the HTML tag 'type' will be 'text', 'textarea' or 'number' (detected + automatically). If necessary, the HTML tag 'type' might be forced to a specific given value. * *step* = Step size of the up/down buttons which increase/decrease the number of in the input field. Optional. Default 1. Only useful with `inputType=number` (defined explicit via `inputType` or detected automatically). * *textareaResize* = 0|1 (optional). Be default = 1 (=on). A textarea element is resizable by the user. +.. _`htmlAllow`: + + * *htmlAllow* = p,br,img,table,u,ol,b,h2,h3,h5,sup (optional). By default every html tag is allowed. Allow only specific + html tags. This option is only useful in case `encode` is not `specialchar` (cause otherwise there are no HTML tags). + If any of the following main tags (before colon) are given, the associated tags will be added automatically: + + * table: td, tr, th, tbody, thead + * ol,ul: li + * b: strong + * u,ins,del,s: span + + List of most used html tags: a,b,br,div,em,h1,h2,h3,h4,h5,h6,hr,i,img,table,ol,ul,p,pre,q,section,small,span,strong,sub,sup,title,u + .. _`input-typeahead`: Type Ahead @@ -1567,7 +1582,7 @@ Type: editor .. important:: - *FormElement.encode*: To save HTML code, incl. HTML tags (bold, table, lists, ...), the **htmspecialchar** + *FormElement.encode*: To save HTML code, incl. HTML tags (bold, table, lists, ...), the **htmlspecialchar** encoding can't be used, cause the HTML tags loose their meaning. Therefore **single tick** or **none** is necessary. @@ -1594,6 +1609,7 @@ Type: editor editor-plugins=code link lists searchreplace table textcolor textpattern visualchars editor-toolbar=code searchreplace undo redo | styleselect link table | bullist numlist outdent indent | forecolor backcolor bold italic editor-menubar=false editor-statusbar=false + * To activate drag and drop option for images in TinyMCE add 'image,paste' to editor-plugins. Example: :: editor-plugins=code link lists searchreplace table textcolor textpattern visualchars image,paste @@ -1623,6 +1639,12 @@ Type: editor * *FormElement.size* = <min_height>,<max_height>: in pixels, including top and bottom bars. E.g.: 300,600 + Define allowed html tags. TinyMCE settings will be overwritten if this parameter is set. +* Following tags are not used from TinyMCE: u,del,ins,s. In this case use textDecoration to get comparable function and correct configuration. Example: :: + + htmlAllow = p,br,h1,h3,table,b,textDecoration,ul,img + +* By default every html tag is allowed. List with tags and their automatically associated tags :ref:`htmlAllow` Type: annotate ^^^^^^^^^^^^^^ diff --git a/Documentation/Installation.rst b/Documentation/Installation.rst index a54e922ab270bbadb643591f0e564b901f6c9fc4..45ea43cc5dc3d07c9a6bcf04e4cb8d0cd96da77d 100644 --- a/Documentation/Installation.rst +++ b/Documentation/Installation.rst @@ -448,7 +448,9 @@ Example: *typo3conf/config.qfq.php*: :: qfq.json ^^^^^^^^ -* Additionally to the keywords bellow one can also override the configuration values defined in the Typo3 extension manager: :ref:`extension-manager-qfq-configuration` +* Additionally to the keywords bellow one can also override the configuration values defined in the Typo3 extension + manager: :ref:`extension-manager-qfq-configuration` + * e.g. if `qfq.json` contains `"flagProduction":"no"` then this value is taken instead of the one set in the extension manager. +-------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------------------------------------+ @@ -496,14 +498,14 @@ Example: *fileadmin/protected/qfqProject/qfq.json*: :: config.qfq.php ^^^^^^^^^^^^^^ -**DEPRECATED** : use `qfq.json` as described above. :ref:`qfq.json` +**DEPRECATED** : use :ref:`qfq.json` as described above. .. _extension-manager-qfq-configuration: Extension Manager: QFQ Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* These configuration values can be overwritten by `qfq.json`. :ref:`qfq.json` +* These configuration values can be overwritten by :ref:`qfq.json` +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ | Keyword | Default / Example | Description | @@ -669,6 +671,9 @@ Extension Manager: QFQ Configuration +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ | rememberLastPill | 0 (off), 1 (on) | On form load, bring last used pill to front. Default is on. | +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ +| doNotLogColumn | password | Do not log named FE-Elements during form save in table FormSubmitLog. | +| | | Default: password | ++-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ | **Form-Layout** | +-----------------------------------+-------------------------------------------------------+----------------------------------------------------------------------------+ | labelAlign | left | Label align (left/center/right)/ Default: left. Will be inherited to Form. | diff --git a/Documentation/License.rst b/Documentation/License.rst index 6a51955c8263c231099a2bc7d63042caa5afd274..06f42624a3f2f68e5a271825ec07a6e9b8f45962 100644 --- a/Documentation/License.rst +++ b/Documentation/License.rst @@ -59,3 +59,5 @@ Software distributed together with QFQ * Event Emitter - https://git.io/ee * FullCalendar - https://fullcalendar.io/ * Datetimepicker - https://getdatepicker.com/ +* HTMLPurifier - https://github.com/ezyang/htmlpurifier +* Font Password-Dots - https://fontstruct.com/fontstructions/show/1106896 The FontStruction “Password Dots†by “JimProuty†is licensed under a Creative Commons Attribution license (http://creativecommons.org/licenses/by/3.0/). diff --git a/Documentation/Report.rst b/Documentation/Report.rst index 8a5c7845a50962b52b7ac33cc553999c6b110ecb..c17abd4b76aefa61631e582148f70dd158ad016b 100644 --- a/Documentation/Report.rst +++ b/Documentation/Report.rst @@ -767,6 +767,9 @@ Column: _link +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Tooltip |o:<text> |o:More information here |Tooltip text | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | | Order |Y:<value> |Y:{{p.id:R}} |Value wrapped in `<span style="display: none;">` (hidden) left to rendered link. Use: possibility to order/filter links/buttons in a | +| | |(invisible) | | |column of a html table via tablesorter. | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Alttext |a:<text> |a:Name of person |a) Alttext for images, b) Message text for :ref:`download` popup window. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Class |c:[n|<text>] |c:text-muted |CSS class for link. n:no class attribute, <text>: explicit named | @@ -786,6 +789,10 @@ Column: _link | | |Mode |M:file|pdf|qfqpdf|zip |M:file, M:pdf, M:qfqpdf, |Mode. Used to specify type of download. One or more element sources needs to be configured. See :ref:`download`. | | | | | |M:zip | | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Text before |v:text before link |v:Some Text |Useful to show text before a link, delivered through `... AS _link`. See :ref:`linkTextBeforeAfter` | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Text after |V:text after link |v:Some Text |Useful to show text after a link, delivered through `... AS _link`. See :ref:`linkTextBeforeAfter` | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |File |F:<filename> |F:fileadmin/file.pdf |Element source for download mode file|pdf|zip. See :ref:`download`. | +---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | | |Delete record | x[:a|r|c] |x, x:r, x:c |a: ajax (only QFQ internal used), r: report (default), c: close (current page, open last page) | @@ -886,6 +893,8 @@ Link Examples | SELECT "y|s:1|F:dir/data.R|t:Data|o:Clipboard|b" AS _link | <button class="btn btn-info" onClick="new QfqNS.Clipboard({uri: 'typo3conf/.../download.php?s=badcaffee1234'});" | | | title='Copy to clipboard'>Data</button> | +-----------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| SELECT "p:/form_person|v:Hello |V: world.|t:Link" AS _link | 'Hello <a href="/form_person">Link</a> world.' | ++-----------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ .. _question: @@ -935,6 +944,32 @@ Examples: | SELECT "p:/form_person|q:Edit Person:::10:0" AS _link | The Alert will be shown 10 seconds and is not modal. | +------------------------------------------------------------+---------------------------------------------------------------------------+ + +.. _linkTextBeforeAfter: + +Text before / after link +^^^^^^^^^^^^^^^^^^^^^^^^ + +* Renders text before and/or after a link. +* Example: `SELECT 'p:{{pageAlias:T}}|t:Reload|v:Some text before |v: some text after' AS _link` +* A typical usecase is to get several `AS _link` columns in one HTML table cell, by still using `fbeg,fend`:: + + 10 { + sql = SELECT p.id + , 'p:{{pageAlias:T}}|t:Reload 1|v:<td>Some text before |V: - ' AS '_link|_noWrap' + , 'p:{{pageAlias:T}}|t:Reload 2|V:</td>' AS '_link|_noWrap' + , p.name + FROM Person AS p + head = <table> + tail = </table> + rbeg = <tr> + rend = </tr> + fbeg = <td> + fend = </td> + } + + + .. _column_pageX: Columns: _page[X] diff --git a/Documentation/Tutorial.rst b/Documentation/Tutorial.rst index 97a9f07437ac063e40b64d3620ad7363c3f2d3a2..d473bcbf2ced37733cc885b9129375a5876423f9 100644 --- a/Documentation/Tutorial.rst +++ b/Documentation/Tutorial.rst @@ -1140,28 +1140,3 @@ we automatically add the reviewers. So that when somebody applies for a job X, t job X will automatically be assigned to that specific application. So let's do that. So go into the formEditor and edit the form "JobOffers". You have to add a new formElement. - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Gruntfile.js b/Gruntfile.js index 90a1205800b54321ca88a8301e34763b469c9cdc..84fb901babd4cc71c89a55877a0a9daaf1e224af 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -563,6 +563,19 @@ module.exports = function (grunt) { } ] }, + fontPassword: { + files: [ + { + cwd: 'resources/custom_fonts/', + src: [ + 'password-dots.ttf' + ], + expand: true, + dest: 'extension/Resources/Public/fonts/', + flatten: true + } + ] + }, }, uglify: { options: { diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php index c33cdc12f27284c0fd002c97d3f210de386f71c7..f04a3768881c9a15394940d7be57888b85d1d732 100644 --- a/extension/Classes/Core/AbstractBuildForm.php +++ b/extension/Classes/Core/AbstractBuildForm.php @@ -820,7 +820,7 @@ abstract class AbstractBuildForm { } $value = $formElement[FE_VALUE]; - + $prefetchTypeAheadArray = array(); if ($value === '') { // #2064 / Only take the default, if the FE is a real tablecolumn. // #3426 / Dynamic Update: Inputs loose the new content and shows the old value. @@ -837,12 +837,21 @@ abstract class AbstractBuildForm { // For typeAhead fields: perform prefetch to display description instead of key (#5444) if ($mode == FORM_SAVE && isset($fe[FE_TYPEAHEAD_SQL_PREFETCH])) { + // Get new record store if value is empty. In case if a previous formElement has modified data from actual used record and a new value exists. + $tableName = $this->dbArray[$this->dbIndexData]->getTableNameById($fe[FE_FORM_ID])[0]['tableName']; + if ($value === '' || $value === 0) { + $this->store::fillStoreWithRecord($tableName, $recordId, $this->dbArray[$this->dbIndexData]); + $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore); + } + $config = [FE_TYPEAHEAD_SQL_PREFETCH => $fe[FE_TYPEAHEAD_SQL_PREFETCH]]; $value = TypeAhead::typeAheadSqlPrefetch($config, $value, $this->dbArray[$this->dbIndexData]); + $prefetchTypeAheadArray = $value; + $value = OnArray::getFirstValueFromArray($value); } } - // Typehead might deliver an array, which is unwanted: fix this + // Typehead might deliver an array, which is unwanted. Except typeahead prefetch, which needs key/value pair to show refreshed data. if (is_array($value) && (isset($value[0][API_TYPEAHEAD_VALUE]) || empty($value))) { $value = OnArray::getFirstValueFromArray($value); } @@ -871,6 +880,12 @@ abstract class AbstractBuildForm { $jsonElement = array(); $elementExtra = ''; + // Backup array from typeAheadPrefetch if exists before json is built. + if (!empty($prefetchTypeAheadArray)) { + OnArray::setFirstValueInArray($prefetchTypeAheadArray, 'value', $value); + $value = $prefetchTypeAheadArray; + } + switch ($formElement[FE_TYPE]) { case FE_TYPE_CHECKBOX: $elementHtml = $this->buildCheckbox($formElement, $htmlFormElementName, $value, $jsonElement, $mode); @@ -1463,7 +1478,6 @@ abstract class AbstractBuildForm { $class .= ' ' . CLASS_CHARACTER_COUNT; $attribute .= Support::doAttribute(DATA_CHARACTER_COUNT_ID, $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT); $attributeCC = Support::doAttribute('id', $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT); - $classCC = ($formElement[FE_CHARACTER_COUNT_WRAP] == '') ? Support::doAttribute('class', 'qfq-cc-style') : ''; $elementCharacterCount = "<span $attributeCC $classCC></span>"; @@ -1480,7 +1494,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]); if (isset($formElement[FE_RETYPE_SOURCE_NAME])) { - $htmlFormElementNamePrimary = str_replace(RETYPE_FE_NAME_EXTENSION, '', $htmlFormElementName); + $htmlFormElementNamePrimary = str_replace(FE_RETYPE_NAME_EXTENSION, '', $htmlFormElementName); $attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']'); } @@ -1540,15 +1554,19 @@ abstract class AbstractBuildForm { $class .= ' qfq-password'; } $attribute .= HelperFormElement::getAttributeList($formElement, [FE_TYPE, 'size']); - $attribute .= Support::doAttribute('value', htmlentities($value), false); + + // Typeahead prefetch needs key as attribute. Key/value pair is needed later on client side. + if (isset($formElement[FE_TYPEAHEAD_SQL_PREFETCH]) && is_array($value)) { + $attributeValue = OnArray::getFirstValueFromArray($value, true); + } else { + $attributeValue = $value; + } + + $attribute .= Support::doAttribute('value', htmlentities($attributeValue), false); + // $attribute .= Support::doAttribute('value', htmlentities($value, ENT_QUOTES, 'UTF-8'), false); } - // Set for password to give choice of generated password for user and not autofill password field. Deprecated, we dont use type password anymore. - // if($formElement[FE_TYPE] === 'password') { - // $attribute .= Support::doAttribute('autocomplete', 'new-password'); - // } - $attribute .= HelperFormElement::getAttributeList($formElement, [FE_INPUT_AUTOCOMPLETE, 'autofocus', 'placeholder']); $formElement[FE_CHECK_PATTERN] = Sanitize::getInputCheckPattern($formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN] @@ -1598,11 +1616,6 @@ abstract class AbstractBuildForm { $input .= $formElement[FE_INPUT_EXTRA_BUTTON_INFO]; } - //Generate an empty input type text to ignore autocomplete in other elements. deprecated because not using type password anymore. -// if($formElement[FE_TYPE] === 'password'){ -// $input = '<input type="text" style="display:none;">'.$input; -// } - return $input; } @@ -3412,7 +3425,7 @@ abstract class AbstractBuildForm { // plugins if (!isset($formElement[FE_EDITOR_PREFIX . 'plugins'])) { - $formElement[FE_EDITOR_PREFIX . 'plugins'] = 'code link lists searchreplace table textcolor textpattern visualchars'; + $formElement[FE_EDITOR_PREFIX . 'plugins'] = 'code link lists searchreplace table textcolor textpattern visualchars image,paste'; } // toolbar: https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols @@ -3432,6 +3445,122 @@ abstract class AbstractBuildForm { $formElement[FE_EDITOR_PREFIX . 'auto_focus'] = $htmlFormElementName; } + // valid elements + // Set defaults for tinyMce + $imgToken = 'img[longdesc|usemap|src|border|alt=|title|hspace|vspace|width|height|align]'; + $textDecoration = 'span[style]'; + $table = 'table[style|align|border],td[style],th[style],tr[style],tbody[style],thead[style]'; + $url = 'a[href|target|title]'; + $paragraphToken = 'p[align]'; + $strong = 'strong'; + + $htmlAllowArray = explode(',', $formElement[FE_HTML_ALLOW]); + $formatDropdownElements = array(); + $customEditorToolbar = 'code |'; + + // flags to prevent multiple same values + $listFlag = false; + $decorationFlag = false; + $formatDropdownFlag = false; + $customEditorToolbarFlags = array(); + + foreach ($htmlAllowArray as $htmlToken => $value) { + switch ($value) { + case 'a': + $htmlAllowArray[$htmlToken] = $url; + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'link'); + break; + case 'table': + $htmlAllowArray[$htmlToken] = $table; + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'table'); + break; + case 'textDecoration': + case 'u': + case 'ins': + case 's': + case 'del': + if (!$decorationFlag) { + $htmlAllowArray[$htmlToken] = $textDecoration; + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'textDecoration', 'underline strikethrough'); + $decorationFlag = true; + } + break; + case 'img': + $htmlAllowArray[$htmlToken] = $imgToken; + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'image'); + break; + case 'ul': + if (!$listFlag) { + $htmlAllowArray[$htmlToken] = 'ul,li'; + $listFlag = true; + } else { + $htmlAllowArray[$htmlToken] = 'ul'; + } + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'bullist'); + break; + case 'ol': + if (!$listFlag) { + $htmlAllowArray[$htmlToken] = 'ol,li'; + $listFlag = true; + } else { + $htmlAllowArray[$htmlToken] = 'ol'; + } + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'numlist'); + break; + case 'b': + case 'strong': + $htmlAllowArray[$htmlToken] = $strong; + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'bold'); + break; + case 'i': + case 'em': + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'italic'); + break; + case 'sub': + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'subscript'); + break; + case 'sup': + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'supscript'); + break; + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + case 'p': + case 'div': + case 'pre': + if ($value === 'p') { + $htmlAllowArray[$htmlToken] = $paragraphToken; + } + $this->setTinymceBlockFormats($value, $formatDropdownElements); + if (!$formatDropdownFlag) { + $formatDropdownFlag = true; + } + break; + default: + break; + } + } + + // set format dropdown at the end of the toolbar if its used + if ($formatDropdownFlag) { + $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'formatselect'); + } + + // If htmlAllow is used: toolbar will be overwritten with the customized one + if (!empty($customEditorToolbarFlags)) { + $formElement[FE_EDITOR_PREFIX . 'toolbar'] = $customEditorToolbar; + } + + // Set allowed values and corrected dropdown from formats + if (isset($formElement[FE_HTML_ALLOW])) { + $formElement[FE_EDITOR_PREFIX . 'valid_elements'] = implode(',', $htmlAllowArray); + $formElement[FE_TINYMCE_DROPDOWN_FORMATS] = implode(';', $formatDropdownElements); + $formElement[FE_EDITOR_PREFIX . 'block_formats'] = $formElement[FE_TINYMCE_DROPDOWN_FORMATS]; + } + // Check for min_height, max_height $minMax = explode(',', $formElement[FE_SIZE], 2); if (isset($minMax[0]) && ctype_digit($minMax[0]) && !isset($formElement[FE_EDITOR_PREFIX . 'min_height'])) { @@ -3450,6 +3579,67 @@ abstract class AbstractBuildForm { return $formElement; } + /** + * Tinymce toolbar attributes needs to be defined separately + * + * @param $toolbarFlags + * @param $attributeList + * @param $attributeName + * @param $specialAttributeName + * @return void + */ + private function setTinymceEditorToolbarAttributes(&$toolbarFlags, &$attributeList, $attributeName, $specialAttributeName = '') { + if (!empty($toolbarFlags[$attributeName]) && !$toolbarFlags[$attributeName]) { + if ($specialAttributeName === '') { + $attributeList .= ' ' . $attributeName; + } else { + $attributeList .= ' ' . $specialAttributeName; + } + $toolbarFlags[$attributeName] = true; + } + } + + /** + * Tinymce has individual strings for some html block configurations. They need to be set here. + * + * @param $format + * @param $formatDropdownElements + * @return void + */ + private function setTinymceBlockFormats($format, &$formatDropdownElements) { + + switch ($format) { + case 'h1': + $formatDropdownElements[] = 'Heading 1=h1'; + break; + case 'h2': + $formatDropdownElements[] = 'Heading 2=h2'; + break; + case 'h3': + $formatDropdownElements[] = 'Heading 3=h3'; + break; + case 'h4': + $formatDropdownElements[] = 'Heading 4=h4'; + break; + case 'h5': + $formatDropdownElements[] = 'Heading 5=h5'; + break; + case 'h6': + $formatDropdownElements[] = 'Heading 6=h6'; + break; + case 'p': + $formatDropdownElements[] = 'Paragraph=p'; + break; + case 'div': + $formatDropdownElements[] = 'Div-Container=div'; + break; + case 'pre': + $formatDropdownElements[] = 'Preformat=pre'; + break; + default: + break; + } + } /** * Searches for '$prefix*' elements in $formElement. Collect all found elements, strip $prefix (=$keyName) and diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php index 99157da3e98b90d377b30bf83d53208d1eef15af..5fc81d7fa6cc4b5e37d6bf7f8fb83af584c6769f 100644 --- a/extension/Classes/Core/Constants.php +++ b/extension/Classes/Core/Constants.php @@ -710,6 +710,7 @@ const SYSTEM_CMD_IMG2PDF = 'cmdImg2pdf'; const SYSTEM_CMD_HEIF_CONVERT = 'cmdHeifConvert'; const SYSTEM_CMD_PDF2PS = 'cmdPdf2ps'; const SYSTEM_CMD_PS2PDF = 'cmdPs2pdf'; +const SYSTEM_DO_NOT_LOG_COLUMN_DEFAULT = 'password'; // Thumbnail const SYSTEM_THUMBNAIL_DIR_SECURE_REL_TO_APP = 'thumbnailDirSecure'; const SYSTEM_THUMBNAIL_DIR_PUBLIC_REL_TO_APP = 'thumbnailDirPublic'; @@ -743,6 +744,7 @@ const DOWNLOAD_POPUP_REPLACE_TITLE = '#downloadPopupReplaceTitle#'; const SYSTEM_DRAG_AND_DROP_JS = 'hasDragAndDropJS'; const SYSTEM_SQL_DIRECT_DOWNLOAD = 'sqlDirect'; // becomes sqlDirectdownload.php, sqlDirectdl.php, sqlDirectdl2.php, sqlDirectdl3.php const SYSTEM_EDIT_INLINE_REPORTS = 'editInlineReports'; +const SYSTEM_UNIT_TEST_FORM_CONTENT = 'unitTestFormContent'; const SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME = 'parameterLanguageFieldName'; const CSS_REQUIRED_RIGHT = 'required-right'; @@ -754,6 +756,7 @@ const CSS_REQUIRED_LEFT = 'required-left'; //const SYSTEM_FORM_ELEMENT_FIELD = 'formElementField'; // Type: SANITIZE_ALNUMX / String. Fieldname of processed Formelement. Useful for error reporting. const SYSTEM_QFQ_PROJECT_PATH = 'qfqProjectPath'; +const SYSTEM_DO_NOT_LOG_COLUMN = 'doNotLogColumn'; const MODE_HTML = 'html'; const MODE_JSON = 'json'; @@ -1166,6 +1169,7 @@ const CLIENT_REST_ID = '_id'; const CLIENT_REST_FORM = '_form'; const F_REMEMBER_LAST_PILL = SYSTEM_REMEMBER_LAST_PILL; +const F_DO_NOT_LOG_COLUMN = SYSTEM_DO_NOT_LOG_COLUMN; // Form Columns: Only in form file const F_FILE_FORM_ELEMENT = 'FormElement_ff'; // Key for FormElements array saved in Form File @@ -1346,6 +1350,7 @@ const FE_RETYPE = 'retype'; // value: <none>|0|1 , <none>==1, this element beco const FE_RETYPE_LABEL = 'retypeLabel'; // value: label text for retype FormElement const FE_RETYPE_NOTE = 'retypeNote'; // value: note text for retype FormElement const FE_RETYPE_SOURCE_NAME = '_retypeSourceName'; // QFQ internal reference to name of source FormElement. +const FE_RETYPE_NAME_EXTENSION = 'RETYPE'; const FE_WRAP_ROW = 'wrapRow'; const FE_WRAP_LABEL = 'wrapLabel'; const FE_WRAP_INPUT = 'wrapInput'; @@ -1427,8 +1432,8 @@ const FE_FLAG_ROW_CLOSE_TAG = '_flagRowCloseTag'; // will be automatically compu const FE_MIN = 'min'; const FE_MAX = 'max'; - -const RETYPE_FE_NAME_EXTENSION = 'RETYPE'; +const FE_HTML_ALLOW = 'htmlAllow'; +const FE_TINYMCE_DROPDOWN_FORMATS = 'tinymceDropdownFormats'; // Save form as Json const FE_SAVE_FORM_JSON = 'saveFormJson'; @@ -1869,6 +1874,8 @@ const NAME_SIP = 'sip'; const NAME_URL_PARAM = 'param'; const NAME_RIGHT = 'picturePositionRight'; const NAME_ACTION_DELETE = 'actionDelete'; +const NAME_ORDER_TEXT ='orderText'; +const NAME_ORDER_TEXT_WRAP = 'orderTextWrap'; const NAME_EXTRA_CONTENT_WRAP = 'extraContentWrap'; const NAME_FILE = 'file'; const NAME_THUMBNAIL = 'thumbnail'; @@ -1878,7 +1885,8 @@ const NAME_MONITOR = 'monitor'; const NAME_ATTRIBUTE = 'attribute'; const NAME_EMAIL = 'email'; const NAME_REALNAME = 'realname'; - +const NAME_BEFORE_LINK = 'beforeLink'; +const NAME_AFTER_LINK = 'afterLink'; const FINAL_HREF = 'finalHref'; const FINAL_ANCHOR = 'finalAnchor'; const FINAL_CONTENT = 'finalContent'; @@ -1952,11 +1960,15 @@ const TOKEN_FILE = 'F'; const TOKEN_FILE_DEPRECATED = 'f'; // since 5.12.17 const TOKEN_DOWNLOAD_MODE = 'M'; const TOKEN_ATTRIBUTE = 'A'; +const TOKEN_BEFORE_LINK = 'v'; +const TOKEN_AFTER_LINK = 'V'; + const TOKEN_FUNCTION_CALL = 'call'; const TOKEN_ARGUMENT = 'arg'; const TOKEN_FORM_ID = 'fid'; const TOKEN_ENCODING_BASE_64 = 'b64'; const TOKEN_REDUCE_KEYS = 'reduce'; +const TOKEN_ORDER_TEXT = 'Y'; const TOKEN_THUMBNAIL = 'T'; const TOKEN_THUMBNAIL_DIMENSION = 'W'; diff --git a/extension/Classes/Core/Database/DatabaseUpdate.php b/extension/Classes/Core/Database/DatabaseUpdate.php index 91ea4867de3a9fde5da385ae2f579b492e580696..a47c401a1e395710e0d8f72abe386280334d02b7 100644 --- a/extension/Classes/Core/Database/DatabaseUpdate.php +++ b/extension/Classes/Core/Database/DatabaseUpdate.php @@ -136,13 +136,13 @@ class DatabaseUpdate { * @throws \UserFormException * @throws \UserReportException */ - public function checkNupdate($dbUpdate) { + public function checkNupdate($dbUpdate, $t3ConfigQfq = array()) { $new = $this->getExtensionVersion(); $versionInfo = $this->getDatabaseVersion(); $old = $versionInfo[QFQ_VERSION_KEY] ?? false; - $this->checkT3QfqConfig($old, $new); + $this->checkT3QfqConfig($old, $new, $t3ConfigQfq); if ($dbUpdate === SYSTEM_DB_UPDATE_NEVER) { return; @@ -222,14 +222,24 @@ class DatabaseUpdate { * @param $old * @param $new */ - private function checkT3QfqConfig($old, $new) { - + private function checkT3QfqConfig($old, $new, $t3ConfigQfq) { + $dirty = false; if ($new == $old || $old === false) { return; } if (version_compare($old, '20.2.0') == -1) { - T3Handler::updateT3QfqConfig(SYSTEM_RENDER_BOTH, SYSTEM_RENDER); //Legacy behaviour. + $t3ConfigQfq[SYSTEM_RENDER_BOTH] = SYSTEM_RENDER; + $dirty = true; + } + + if (version_compare($old, '22.12.0') == -1) { + $t3ConfigQfq[SYSTEM_DO_NOT_LOG_COLUMN] = SYSTEM_DO_NOT_LOG_COLUMN_DEFAULT; + $dirty = true; + } + + if ($dirty) { + T3Handler::updateT3QfqConfig($t3ConfigQfq); //Legacy behaviour. } } @@ -277,7 +287,7 @@ class DatabaseUpdate { ##################################################################################### ##### ONLY RUN THIS FUNCTION IF TYPO3 VERSION 9 OR HIGHER AND NOT API ENDPOINT ##### - if ( ! (T3Handler::isTypo3Loaded() && T3Handler::useSlugsInsteadOfPageAlias()) ) { + if (!(T3Handler::isTypo3Loaded() && T3Handler::useSlugsInsteadOfPageAlias())) { return null; } @@ -351,7 +361,9 @@ class DatabaseUpdate { $dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM); $aliasColumn = 'zzz_deleted_alias'; $tableDefinition = $this->db->sql("SHOW FIELDS FROM `$dbT3`.`pages`", ROW_EXPECT_GE_1); - $aliasColumnExists = 0 < count(array_filter($tableDefinition, function ($c) use ($aliasColumn) {return $c['Field'] === $aliasColumn;})); + $aliasColumnExists = 0 < count(array_filter($tableDefinition, function ($c) use ($aliasColumn) { + return $c['Field'] === $aliasColumn; + })); // Get id, slug and alias of all Typo3 pages $pages = $this->db->sql("SELECT `uid`, `slug`" . ($aliasColumnExists ? ", `$aliasColumn`" : '') . " FROM `" . $dbT3 . "`.`pages` WHERE `deleted`=0;"); @@ -369,23 +381,32 @@ class DatabaseUpdate { if (is_numeric($aliasOrId)) { // if its an id (number), get the page with that id - $uidMatches = array_filter($pages, function ($p) use ($aliasOrId) {return $p['uid'] === intval($aliasOrId);}); + $uidMatches = array_filter($pages, function ($p) use ($aliasOrId) { + return $p['uid'] === intval($aliasOrId); + }); $page = reset($uidMatches); return $page !== false ? $page['slug'] : $noSuggestionSymbol; } else { // search alias in page slugs. ('_' was automatically replaced by '-' Typo3 v9 Migration) $slugMatches = array_filter($pages, function ($p) use ($aliasOrId) { - return ($p['slug'] === '/' . str_replace('_', '-', $aliasOrId)) || ($p['slug'] === '/' . $aliasOrId);}); - $slugs = array_map(function ($p) {return $p['slug'];}, $slugMatches); + return ($p['slug'] === '/' . str_replace('_', '-', $aliasOrId)) || ($p['slug'] === '/' . $aliasOrId); + }); + $slugs = array_map(function ($p) { + return $p['slug']; + }, $slugMatches); if (1 === count(array_unique($slugs))) { return reset($slugs); } // search alias in old alias column ('zzz_deleted_alias') if it exists if ($aliasColumnExists) { - $aliasMatches = array_filter($pages, function ($p) use ($aliasColumn, $aliasOrId) {return $p[$aliasColumn] === $aliasOrId;}); - $slugs = array_map(function ($p) {return $p['slug'];}, $aliasMatches); + $aliasMatches = array_filter($pages, function ($p) use ($aliasColumn, $aliasOrId) { + return $p[$aliasColumn] === $aliasOrId; + }); + $slugs = array_map(function ($p) { + return $p['slug']; + }, $aliasMatches); if (1 === count(array_unique($slugs))) { return reset($slugs); } @@ -404,7 +425,7 @@ class DatabaseUpdate { '/([\'"\|]p:)((?:id=)?{{(?:pageAlias|pageId))(:T[0E]*}}[&\|"\'])/s', '/(href=[\'"])((?:index\.php)?\?id=[a-zA-Z0-9_\-]*)([&"\'])/s', '/(href=[\'"])((?:index\.php)?\?id={{(?:pageAlias|pageId))(:T[0E]*}}[&"\'])/s' - ]; + ]; // Patterns for which we can't make a suggestion (applied after the the patterns with suggestions were replaced) $patternsNeedManualFix = [ @@ -477,14 +498,15 @@ class DatabaseUpdate { // get reports from tt_content.bodytext $reports = $this->db->sql("SELECT tt.`uid`, tt.`header`, tt.`bodytext`, tt.`hidden`, p.`hidden` AS pageHidden FROM `" . $dbT3 . "`.`tt_content` AS tt, `" . $dbT3 . "`.`pages` AS p WHERE tt.`CType`='qfq_qfq' AND tt.`deleted`=0 AND p.`deleted`=0 AND p.uid=tt.pid"); - $qfqCodeBlobs = array_map(function($r) use ($dbT3, $KEY_SQL_UPDATE, $KEY_CONTENT, $KEY_TITLE) { + $qfqCodeBlobs = array_map(function ($r) use ($dbT3, $KEY_SQL_UPDATE, $KEY_CONTENT, $KEY_TITLE) { $maybeHidden = (intval($r['hidden']) !== 0) || (intval($r['pageHidden']) !== 0); return [ $KEY_TITLE => 'QFQ Report with uid=' . $r['uid'] . ' and header: ' . $r['header'] . ($maybeHidden ? '<br><small>Note: Content element is probably hidden / not in use.</small>' : ''), $KEY_CONTENT => $r['bodytext'], $KEY_SQL_UPDATE => "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ? WHERE uid=" . $r['uid'] . ";" - ];}, $reports); + ]; + }, $reports); // get Forms $formColumnsToCheck = [ @@ -559,7 +581,7 @@ class DatabaseUpdate { // make sure the control characters are not used in the QFQ code if (OnString::strContains($qfqCode[$KEY_CONTENT], $noSuggestionSymbol) || OnString::strContains($qfqCode[$KEY_CONTENT], $replacedSymbol)) { - Thrower::dbException('Page Slug Migration.', "Unicode character $noSuggestionSymbol or $replacedSymbol found in" . $qfqCode[$KEY_TITLE] . ". The page slug migration script can't continue since it uses those characters as control characters. Please temporarily replace those characters."); + Thrower::dbException('Page Slug Migration.', "Unicode character $noSuggestionSymbol or $replacedSymbol found in" . $qfqCode[$KEY_TITLE] . ". The page slug migration script can't continue since it uses those characters as control characters. Please temporarily replace those characters."); } // add suggestion marks to all occurrences of regex patterns in $patternsWithSuggestions @@ -591,7 +613,9 @@ class DatabaseUpdate { // protect matched string from being matched again by interlacing it with $preventMatchSymbol ( "string" -> "XsXtXrXiXnXg" ) // symbol will be removed before output - $fullMatchProtected = join(array_map(function ($c) use ($preventMatchSymbol) {return $preventMatchSymbol . $c;}, str_split($fullMatch))); + $fullMatchProtected = join(array_map(function ($c) use ($preventMatchSymbol) { + return $preventMatchSymbol . $c; + }, str_split($fullMatch))); $replacement = $placeholderBegin . $fullMatchProtected . $placeholderArrow . $prefix_suggestion . $match_suggestion . $postfix_suggestion . $placeholderEnd; $allMatches[] = $fullMatch . ' ➡ ' . $prefix_suggestion . $match_suggestion . $postfix_suggestion; @@ -602,7 +626,7 @@ class DatabaseUpdate { // find those patterns where we can't give a suggestion if (!$doReplace) { - $replaced_with_placeholder = preg_replace($patternsNeedManualFix, $placeholderBegin . '${1}' . '${2}'. '${3}' . $placeholderArrow . $noSuggestionSymbol . $placeholderEnd, $replaced_with_placeholder); + $replaced_with_placeholder = preg_replace($patternsNeedManualFix, $placeholderBegin . '${1}' . '${2}' . '${3}' . $placeholderArrow . $noSuggestionSymbol . $placeholderEnd, $replaced_with_placeholder); } // do the suggestion replacement if in replacement mode and construct error message @@ -611,13 +635,13 @@ class DatabaseUpdate { // We are in "display" mode, we show suggestions, no replacements are made $message .= '<hr><span style="font-weight: bold; color: blue;">' . $qfqCode[$KEY_TITLE] . '</span><br><br>'; $message .= $qfqCode[$KEY_SQL_UPDATE]; - $message .= '<pre>' . str_replace([$preventMatchSymbol, $placeholderBegin, $placeholderArrow, $placeholderEnd, "\r\n", "\n"], [ - '', - '<span style="font-weight: bold; color: red;">', - '</span> ➡ <span style="font-weight: bold; color: green;">', - '</span>', - "<br>", "<br>" - ], htmlentities($replaced_with_placeholder)) . '</pre>'; + $message .= '<pre>' . str_replace([$preventMatchSymbol, $placeholderBegin, $placeholderArrow, $placeholderEnd, "\r\n", "\n"], [ + '', + '<span style="font-weight: bold; color: red;">', + '</span> ➡ <span style="font-weight: bold; color: green;">', + '</span>', + "<br>", "<br>" + ], htmlentities($replaced_with_placeholder)) . '</pre>'; } elseif (strpos($replaced_with_placeholder, $replacedSymbol) !== false) { // We are in "replacement" mode, show what is been replaced and do the replacement @@ -657,7 +681,7 @@ class DatabaseUpdate { $message = '' . '<h2>Automatic Replacement Completed</h2>' - . 'The following report has been saved to <br>' . $reportPath + . 'The following report has been saved to <br>' . $reportPath . '<br><br>Click <a href="?' . ACTION_SLUG_MIGRATION_UPDATE . '=null">check-again</a> to search for still existing usages of page alias.' . '<h2>Overview Replacement Suggestions</h2>' . join('<br>', array_unique($allMatches)) @@ -691,7 +715,7 @@ class DatabaseUpdate { . '<li><a href="?' . ACTION_SLUG_MIGRATION_UPDATE . '=' . ACTION_SLUG_MIGRATION_DO_REPLACE . '">Auto replace</a>' . ' occurrences of page aliases and page ids with the suggested page slug.' - . '<br>A report file with name "[timestamp]'. $reportFilePostfix .'" will be saved to "' . Path::absoluteLog() . '" after the automatic replacement. </li>' + . '<br>A report file with name "[timestamp]' . $reportFilePostfix . '" will be saved to "' . Path::absoluteLog() . '" after the automatic replacement. </li>' . ' <li>To use the Form Editor you can ' . '<a href="?' . ACTION_SLUG_MIGRATION_UPDATE . '=' . ACTION_SLUG_MIGRATION_DO_PAUSE . '">pause the migration temporarliy</a>.</li>' @@ -703,10 +727,6 @@ class DatabaseUpdate { . "</ul>" - - - - . ($forceRunMigrationCheck ? '<br><br>Note: setting ' . FORCE_RUN_PAGE_SLUG_MIGRATION_CHECK . ' is active in qfq.json.' : '') . '<h2>Overview Replacement Suggestions</h2>' diff --git a/extension/Classes/Core/Helper/HelperFormElement.php b/extension/Classes/Core/Helper/HelperFormElement.php index dd182b64ad3ae1465162c648a3ec00a0d2adff21..f979fbf47f4d5ca21217ae7cc85542fa96096973 100644 --- a/extension/Classes/Core/Helper/HelperFormElement.php +++ b/extension/Classes/Core/Helper/HelperFormElement.php @@ -251,7 +251,7 @@ class HelperFormElement { $fe[FE_RETYPE_SOURCE_NAME] = $fe[FE_NAME]; // Create copy of FE, adjust name, label, note - $fe[FE_NAME] .= RETYPE_FE_NAME_EXTENSION; + $fe[FE_NAME] .= FE_RETYPE_NAME_EXTENSION; if (isset($fe[FE_RETYPE_LABEL])) { $fe[FE_LABEL] = $fe[FE_RETYPE_LABEL]; diff --git a/extension/Classes/Core/Helper/OnArray.php b/extension/Classes/Core/Helper/OnArray.php index e9b1286bd4a3a4004948fe9ed0c9faf4c428253a..681d8626e29b5e70a7fe448f49e619b92d74d240 100644 --- a/extension/Classes/Core/Helper/OnArray.php +++ b/extension/Classes/Core/Helper/OnArray.php @@ -506,7 +506,7 @@ class OnArray { * @param $arr * @return string */ - public static function getFirstValueFromArray($arr): string { + public static function getFirstValueFromArray($arr, $reverse = false): string { if (!is_array($arr)) { return $arr; @@ -516,6 +516,10 @@ class OnArray { $arr = array_pop($arr); } + if ($reverse) { + $arr = array_reverse($arr); + } + $values = array_values($arr); if (count($values) > 1) { $value = $values[1]; @@ -540,4 +544,23 @@ class OnArray { rsort($arr); return isset($arr[0]) && is_array($arr[0]); } + + /** + * Set value in one- or multidimensional array + * + * @param $arr + * @param $value + */ + public static function setFirstValueInArray(&$arr, $key, $value): void { + if (self::is_multi_array($arr)) { + foreach ($arr as &$element) { + if (isset($element[$key])) { + $element[$key] = $value; + break; + } + } + } else if (isset($arr[$key])){ + $arr[$key] = $value; + } + } } \ No newline at end of file diff --git a/extension/Classes/Core/Helper/Token.php b/extension/Classes/Core/Helper/Token.php index f5019c7c5fd66f21c6f7b98f17cc15ebcf111528..a206c9a72503317bf03e52977ae0f03b12a2d6c5 100644 --- a/extension/Classes/Core/Helper/Token.php +++ b/extension/Classes/Core/Helper/Token.php @@ -57,6 +57,8 @@ class Token { case TOKEN_ACTION_DELETE: $value = DEFAULT_ACTION_DELETE; break; + case TOKEN_ORDER_TEXT: + throw new \UserReportException ("Missing value for token '$key'", ERROR_MISSING_VALUE); default: } diff --git a/extension/Classes/Core/QuickFormQuery.php b/extension/Classes/Core/QuickFormQuery.php index 38a267519dc708bc53325e26e67cd28c60f71d4b..c4f15feff0cc4a92a9102c6288440fdb506b27d7 100644 --- a/extension/Classes/Core/QuickFormQuery.php +++ b/extension/Classes/Core/QuickFormQuery.php @@ -165,8 +165,9 @@ class QuickFormQuery { $this->store = Store::getInstance($bodytext, $phpUnit); - $timeout = $this->store::getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM); - Session::checkSessionExpired($timeout); + $t3ConfigQfq = $this->store::getStore(STORE_SYSTEM); + + Session::checkSessionExpired($t3ConfigQfq[SYSTEM_SESSION_TIMEOUT_SECONDS]); // If an FE user logs out and a different user logs in (same browser session) - the old values has to be destroyed! if (Session::getAndDestroyFlagFeUserHasChanged()) { @@ -190,7 +191,7 @@ class QuickFormQuery { $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM); $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq], $this->store); - $updateDb->checkNupdate($dbUpdate); + $updateDb->checkNupdate($dbUpdate, $t3ConfigQfq); $this->store->FillStoreSystemBySql(); // Do this after the DB-update @@ -657,6 +658,11 @@ class QuickFormQuery { $this->formAsFileAfterSave($formFileName, $formModeNew, $formFileNameDelete); } + $unitTestRender = $this->store::getVar(SYSTEM_UNIT_TEST_FORM_CONTENT, STORE_SYSTEM); + if (isset($unitTestRender) && $unitTestRender !== false) { + $data = $unitTestRender; + } + return $data; } @@ -807,28 +813,57 @@ class QuickFormQuery { * @throws \UserFormException */ private function logFormSubmitRequest() { + $formSubmitLogMode = $this->formSpec[F_FORM_SUBMIT_LOG_MODE] ?? $this->store->getVar(SYSTEM_FORM_SUBMIT_LOG_MODE, STORE_SYSTEM, SANITIZE_ALLOW_ALNUMX); + if ($formSubmitLogMode === FORM_SUBMIT_LOG_MODE_NONE) { return; } $formData = $_POST; unset($formData[CLIENT_SIP]); + $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); + + // Check if specific columns should not be logged into table FormSubmitLog + $doNotLogColumnList = $this->formSpec[F_DO_NOT_LOG_COLUMN]; + if (!empty($doNotLogColumnList)) { + $doNotLogColumnListArray = explode(',', $doNotLogColumnList); + // Replace content of all protected columns. + foreach ($doNotLogColumnListArray as $column) { + $column = trim($column); + + // Get final $feName + $feName = HelperFormElement::buildFormElementName([FE_NAME => $column], $recordId); + if (isset($formData[$feName])) { + // Only if such element exist: wipe it. + $formData[$feName] = '*hide in log*'; + } + + // Password fields are often used with option RETYPE. Check and wipe them too. + $feNameRetype = HelperFormElement::buildFormElementName([FE_NAME => $column . FE_RETYPE_NAME_EXTENSION], $recordId); + if (isset($formData[$feNameRetype])) { + $formData[$feNameRetype] = '*hide in log*'; + } + } + } + $formData = json_encode($formData, JSON_UNESCAPED_UNICODE); + + $sql = "INSERT INTO `FormSubmitLog` (`formData`, `sipData`, `clientIp`, `feUser`, `userAgent`, `formId`, `formName`, `recordId`, `pageId`, `sessionId`, `created`)" . + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"; + $clientIp = $_SERVER[CLIENT_REMOTE_ADDRESS] ?? ''; $userAgent = $_SERVER[CLIENT_HTTP_USER_AGENT] ?? ''; $sipData = json_encode($this->store->getStore(STORE_SIP), JSON_UNESCAPED_UNICODE); $formId = $this->formSpec[F_ID]; $formName = $this->formSpec[F_NAME]; - $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); $feUser = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX); $pageId = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX); $sessionId = session_id(); - $sql = "INSERT INTO `FormSubmitLog` (`formData`, `sipData`, `clientIp`, `feUser`, `userAgent`, `formId`, `formName`, `recordId`, `pageId`, `sessionId`, `created`)" . - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"; $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $formName, $recordId, $pageId, $sessionId]; + $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params); } @@ -1402,10 +1437,12 @@ class QuickFormQuery { F_SHOW_ID_IN_FORM_TITLE, F_INPUT_CLEAR_ME, F_DATE_TIME_PICKER_TYPE, + 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. + ]; // By definition: existing vars which are empty, means: EMPTY - do not use any default! diff --git a/extension/Classes/Core/Report/Link.php b/extension/Classes/Core/Report/Link.php index 8fcdf1ca126f9e8526e54dffefa1333bce065c0e..9126402cb918613d6e203f50bdd6c8a0f46a28a2 100644 --- a/extension/Classes/Core/Report/Link.php +++ b/extension/Classes/Core/Report/Link.php @@ -77,8 +77,8 @@ use IMATHUZH\Qfq\Core\Typo3\T3Handler; * T:Thumbnail * u:url * U:URL Param - * v: - * V: + * v:Text before link + * V:Text after link * w:websocket * W:Dimension * x:Delete @@ -158,8 +158,12 @@ class Link { TOKEN_THUMBNAIL_DIMENSION => NAME_THUMBNAIL_DIMENSION, TOKEN_COPY_TO_CLIPBOARD => NAME_COPY_TO_CLIPBOARD, TOKEN_ATTRIBUTE => NAME_ATTRIBUTE, + TOKEN_ORDER_TEXT => NAME_ORDER_TEXT, TOKEN_MONITOR => NAME_MONITOR, + TOKEN_BEFORE_LINK => NAME_BEFORE_LINK, + TOKEN_AFTER_LINK => NAME_AFTER_LINK, + // The following don't need a renaming: already 'long' TOKEN_L_FILE => TOKEN_L_FILE, TOKEN_L_TAIL => TOKEN_L_TAIL, @@ -624,7 +628,8 @@ class Link { throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE); } - return $link; + // Merge span to link if something exists from qualifier 'Y' + return $vars[NAME_BEFORE_LINK] . ($vars[NAME_ORDER_TEXT_WRAP] ?? '') . $link . $vars[NAME_AFTER_LINK]; } /** @@ -842,6 +847,15 @@ class Link { case TOKEN_BOOTSTRAP_BUTTON: $vars = $this->buildBootstrapButton($vars, $value); break; + case TOKEN_ORDER_TEXT: + $vars = $this->buildOrderText($vars, $value); + break; + case TOKEN_BEFORE_LINK: + $vars = $this->buildTextBeforeLink($vars, $value); + break; + case TOKEN_AFTER_LINK: + $vars = $this->buildTextAfterLink($vars, $value); + break; default: break; } @@ -902,6 +916,7 @@ class Link { NAME_COLLECT_ELEMENTS => array(), NAME_COPY_TO_CLIPBOARD => '', NAME_ATTRIBUTE => '', + NAME_ORDER_TEXT => '', NAME_RENDER => '0', NAME_RIGHT => 'l', @@ -916,6 +931,9 @@ class Link { NAME_ACTION_DELETE => '', + NAME_BEFORE_LINK => '', + NAME_AFTER_LINK => '', + FINAL_HREF => '', FINAL_CONTENT => '', FINAL_SYMBOL => '', @@ -2135,6 +2153,26 @@ EOF; return $vars; } + /** + * @param $vars + * @param $value + * @return mixed + */ + private function buildTextBeforeLink($vars, $value) { + $vars[NAME_BEFORE_LINK] = $value; + return $vars; + } + + /** + * @param $vars + * @param $value + * @return mixed + */ + private function buildTextAfterLink($vars, $value) { + $vars[NAME_AFTER_LINK] = $value; + return $vars; + } + /** * Called by $this->callTable * @@ -2224,4 +2262,15 @@ EOF; return $vars; } + + /** + * Builds hidden span tag for column link, used as search criteria in table sorter. + * + * @param array $vars + * @return array + */ + private function buildOrderText(array $vars) { + $vars[NAME_ORDER_TEXT_WRAP] = Support::wrapTag('<span style="display: none;">', $vars[NAME_ORDER_TEXT]); + return $vars; + } } \ No newline at end of file diff --git a/extension/Classes/Core/Save.php b/extension/Classes/Core/Save.php index 926054d639349d1dbc237f28acb6457ccab0cf14..37b5523d007f32ac003a7ce216028070139fcb5d 100644 --- a/extension/Classes/Core/Save.php +++ b/extension/Classes/Core/Save.php @@ -8,6 +8,7 @@ namespace IMATHUZH\Qfq\Core; +use HTMLPurifier; use IMATHUZH\Qfq\Core\Database\Database; use IMATHUZH\Qfq\Core\Exception\Thrower; use IMATHUZH\Qfq\Core\Form\FormAction; @@ -407,6 +408,10 @@ class Save { foreach ($this->feSpecNative as $fe) { $feColumnnTypes[$fe['name']] = $fe['type']; } + + // Get htmlAllow parameters of all formValues and store in $feSpecsTags + $feSpecsTags = $this->getHtmlAllowTags($this->feSpecNative, $formValues); + // Iterate over all table.columns. Built an assoc array $newValues. foreach ($tableColumns as $column) { @@ -459,6 +464,13 @@ class Save { } else { Support::setIfNotSet($formValues, $column); } + + // Check for existing htmlAllow and strip tags, purify html result to prevent XSS + if (isset($feSpecsTags[$column]) && $feSpecsTags[$column] !== '') { + $formValues[$column] = $this->custom_strip_tags($formValues[$column], $feSpecsTags[$column]); + $formValues[$column] = $this->purifierHtml($formValues[$column]); + } + $newValues[$column] = $formValues[$column]; $realColumnFound = true; } @@ -490,6 +502,97 @@ class Save { return $recordId; } + /** + * Get for every formElement htmlAllow tags from parameter + * + * @param $feSpecNative + * @param $formValues + * @return array + */ + private function getHtmlAllowTags($feSpecNative, $formValues): array { + + $feSpecsTags = array(); + + foreach ($feSpecNative as $formElement) { + foreach ($formValues as $keyName => $keyValue) { + if ($formElement[FE_NAME] === $keyName) { + if (isset($formElement[FE_HTML_ALLOW]) && $formElement[FE_HTML_ALLOW] !== '') { + $feSpecsTags[$keyName] = $formElement[FE_HTML_ALLOW]; + } + } + } + } + + return $this->setTinyMceSpecificTags($feSpecsTags); + } + + /** + * For TinyMCE there are specific tags needed for lists and text decoration (underline, strikethrough). + * These tags should be added here. + * + * @param $feSpecsTags + * @return array + */ + private function setTinyMceSpecificTags($feSpecsTags): array { + $listFlag = false; + $decorationFlag = false; + $tableFlag = false; + $strongFlag = false; + foreach ($feSpecsTags as $key => $value) { + $feSpecsTagArray[$key] = explode(',', $value); + foreach ($feSpecsTagArray[$key] as $key2 => $tag) { + switch ($tag) { + case 'ul': + case 'ol': + $listFlag = true; + break; + case 'textDecoration': + case 'u': + case 'ins': + case 'del': + case 's': + $decorationFlag = true; + break; + case 'table': + $tableFlag = true; + break; + case 'b': + $strongFlag = true; + break; + default: + $feSpecsTagArray[$key][$key2] = $tag; + break; + } + } + + if ($listFlag) { + $feSpecsTagArray[$key][] = "li"; + $listFlag = false; + } + + // In case of TinyMCE span is automatically used for underline and strikethrough + if ($decorationFlag) { + $feSpecsTagArray[$key][] = "span"; + $decorationFlag = false; + } + + + if ($strongFlag) { + $feSpecsTagArray[$key][] = "strong"; + $strongFlag = false; + } + + if ($tableFlag) { + array_push($feSpecsTagArray[$key], "th", "td", "tr", "tbody", "thead"); + $tableFlag = false; + } + + $feSpecsTags[$key] = implode(',', $feSpecsTagArray[$key]); + } + + return $feSpecsTags; + } + /** * Process sqlBefore, sqlInsert|.... for all native FE. * @@ -1696,6 +1799,7 @@ class Save { $mode = ($slaveId == '0') ? 'I' : 'U'; // I=Insert, U=Update $mode .= ($modeUpload == UPLOAD_MODE_NEW || $modeUpload == UPLOAD_MODE_DELETEOLD_NEW) ? 'N' : ''; // N=New File, '' if no new file. $mode .= ($modeUpload == UPLOAD_MODE_DELETEOLD) ? 'D' : ''; // Delete slave record only if there is no new and not 'unchanged'. + switch ($mode) { case 'IN': $sql = $fe[FE_SQL_INSERT]; @@ -1732,4 +1836,39 @@ class Save { return $slaveId; } + /** + * Remove not allowed tags. + * + * @param array $html_str + * @param string $allowedTags + * @param string $allowedAttributes + * @return string + */ + function custom_strip_tags($html, string $allowedTags) { + $allowed_tags = explode(',', $allowedTags); + $allowed_tags = array_map('strtolower', $allowed_tags); + $regex_tags = '/<\/?([^>\s]+)[^>]*>/i'; + $matches = array(); + preg_match_all($regex_tags, $html, $matches); + $rhtml = preg_replace_callback($regex_tags, function ($matches) use (&$allowed_tags) { + return in_array(strtolower($matches[1]), $allowed_tags) ? $matches[0] : ''; + }, $html); + return $rhtml; + } + + /** + * Remove not allowed attributes and content which is not in whitelist + * Used in combination with htmlAllow. + * Author: Edward Z. Yang + * Website: http://htmlpurifier.org/ + * + * @param $html + * @return array|string|string[]|null + */ + function purifierHtml($html) { + $purifier = new HTMLPurifier(); + $rhtml = $purifier->purify($html); + return $rhtml; + } + } \ No newline at end of file diff --git a/extension/Classes/Core/Store/Config.php b/extension/Classes/Core/Store/Config.php index 89a01e3f09927eea8c1f69a0558d4f891f2843de..1da4a1bbfc14a8c438d52e8d7c42781c9c2c0513 100644 --- a/extension/Classes/Core/Store/Config.php +++ b/extension/Classes/Core/Store/Config.php @@ -186,7 +186,7 @@ class Config { } /** - * Overwrite the qfq config file with data from given array. + * Overwrite the CONFIG_QFQ_JSON (db credentials) file with data from given array. * * @param array $config * @throws \UserFormException diff --git a/extension/Classes/Core/Store/FillStoreForm.php b/extension/Classes/Core/Store/FillStoreForm.php index bd75b9a34e0abbdfe813a41b3868ef17e333e1c5..aa050ff94e5120e770927435caeed78fac390c3a 100644 --- a/extension/Classes/Core/Store/FillStoreForm.php +++ b/extension/Classes/Core/Store/FillStoreForm.php @@ -296,6 +296,13 @@ class FillStoreForm { $formElement[FE_MODE] === FE_MODE_SHOW || (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] != '0')) { + // Dynamically changed fe mode from show or other to readonly is not fetched here (not yet stored in db). + // For this reason there is no included check for fe mode = readonly. + // Theoretically processReadOnly can be used in formElements with other fe modes too. + if (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] == '0') { + continue; + } + if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) { // TypeAhead Tags received as JSON key/value $cntNew = 0; diff --git a/extension/Classes/Core/Typo3/T3Handler.php b/extension/Classes/Core/Typo3/T3Handler.php index e9310fc30b0ce36f963fcdf3a441943c97150cc0..8426a9e1c8c62ec212119876e595881d22deefb3 100644 --- a/extension/Classes/Core/Typo3/T3Handler.php +++ b/extension/Classes/Core/Typo3/T3Handler.php @@ -149,7 +149,9 @@ class T3Handler { } /** - * Update a single key/value pair in `typo3conf/LocalConfiguration.php` QFQ config. + * Update in `typo3conf/LocalConfiguration.php` QFQ config. + * a) key!==false: a single $key/$value pair (not used anymore) + * b) key===false: array in $value * * @param mixed $value * @param mixed $key @@ -179,11 +181,8 @@ class T3Handler { $configQfq = unserialize($configT3['EXT']['extConf'][EXT_KEY]); } - // If key not given, merge both arrays - if ($key) { - // Set new value - $configQfq[$key] = $value; - } else { + if ($key === false) { + // key===false: merge both arrays // Show debug info should stay dynamically and not overwritten in config file. Unit tests are an example for this reason. $showDebugInfo = $configQfq[SYSTEM_SHOW_DEBUG_INFO]; $configQfq = array_merge($configQfq, $value); @@ -195,6 +194,9 @@ class T3Handler { unset($configQfq['DB_' . $ii . '_' . $key]); } } + } else { + // key given: set indiviudal key/value + $configQfq[$key] = $value; } // Prepare @@ -218,8 +220,7 @@ class T3Handler { * * @return string */ - public static function getTypo3Version() - { + public static function getTypo3Version() { self::t3AutoloadIfNotRunning(); return \TYPO3\CMS\Core\Utility\VersionNumberUtility::getNumericTypo3Version(); } diff --git a/extension/Tests/Unit/Core/Helper/HelperFormElementTest.php b/extension/Tests/Unit/Core/Helper/HelperFormElementTest.php index 069808e073e261d85c9f962381dc7f047b792da9..5884a8d907f8ea4f5e8a91f5cd75b6a6766b56e8 100644 --- a/extension/Tests/Unit/Core/Helper/HelperFormElementTest.php +++ b/extension/Tests/Unit/Core/Helper/HelperFormElementTest.php @@ -94,7 +94,7 @@ class HelperFormElementTest extends TestCase { $b = HelperFormElement::duplicateRetypeElements($a); $this->assertEquals(count($a) + 1, count($b), "Both arrays should be same count"); $this->assertEquals('1', $b[0][FE_RETYPE], "FE_RETYPE should be unchanged"); - $this->assertEquals($b[0][FE_NAME] . RETYPE_FE_NAME_EXTENSION, $b[1][FE_NAME], "New name should be extended by: " . RETYPE_FE_NAME_EXTENSION); + $this->assertEquals($b[0][FE_NAME] . FE_RETYPE_NAME_EXTENSION, $b[1][FE_NAME], "New name should be extended by: " . FE_RETYPE_NAME_EXTENSION); $this->assertEquals($b[0][FE_LABEL], $b[1][FE_LABEL], "Both text should be equal"); $this->assertEquals($b[0][FE_NOTE], $b[1][FE_NOTE], "Both text should be equal"); diff --git a/extension/Tests/Unit/Core/QuickFormQueryTest.php b/extension/Tests/Unit/Core/QuickFormQueryTest.php index 009ac79d1721b08214bade7319b603ab3b429e06..2545ea805f6786049fbd3f9e3ff9aa15d0250599 100644 --- a/extension/Tests/Unit/Core/QuickFormQueryTest.php +++ b/extension/Tests/Unit/Core/QuickFormQueryTest.php @@ -106,4 +106,70 @@ class QuickFormQueryTest extends TestCase { } + public function testRenderMode() { + $store = Store::getInstance(); + + // form={{form:SE}} + // 10.sql = SELECT '&Hello World' + $store->unsetStore(STORE_TYPO3); + + // Set dummys for QuickFormQuery test + $store->setVar(SYSTEM_UNIT_TEST_FORM_CONTENT, 'FormLoaded', STORE_SYSTEM); + $store->setVar(SYSTEM_DRAG_AND_DROP_JS, 'false', STORE_SYSTEM); + $store->setVar('form', 'copyForm', STORE_SIP); + $store->setVar('r', '0', STORE_SIP); + $store->setVar(SIP_SIP, 'fakeSip', STORE_SIP); + $t3data[T3DATA_UID] = "123"; + + // QFQ Config - render = single + $store->setVar(SYSTEM_RENDER, SYSTEM_RENDER_SINGLE, STORE_SYSTEM); + + // In report - nothing given + $t3data[T3DATA_BODYTEXT] = "form={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded', $result); + + // In report - render = both + $t3data[T3DATA_BODYTEXT] = "render=both\nform={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded&Hello World', $result); + + // In report - render = single + $t3data[T3DATA_BODYTEXT] = "render=single\nform={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded', $result); + + // QFQ Config - render = both + $store->setVar(SYSTEM_RENDER, SYSTEM_RENDER_BOTH, STORE_SYSTEM); + + // In report - nothing given + $t3data[T3DATA_BODYTEXT] = "form={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded&Hello World', $result); + + // In report - render = both + $t3data[T3DATA_BODYTEXT] = "render=both\nform={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded&Hello World', $result); + + // In report - render = single + $t3data[T3DATA_BODYTEXT] = "render=single\nform={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('FormLoaded', $result); + + // In report - render = api. Unset unit test variable for this one. + $store->unsetVar(SYSTEM_UNIT_TEST_FORM_CONTENT, STORE_SYSTEM); + $GLOBALS['TYPO3_CONF_VARS'] = 'fakeVars'; + $t3data[T3DATA_BODYTEXT] = "render=api\nform={{form:SE}}\n20.sql = SELECT '&Hello World'"; + $qfq = new QuickFormQuery($t3data, true); + $result = $qfq->process(); + $this->assertEquals('', $result); + + } } diff --git a/extension/Tests/Unit/Core/Report/LinkTest.php b/extension/Tests/Unit/Core/Report/LinkTest.php index 4a3338589b296b4bee33d8a7e1b2c93b0dd2db6e..996d3ec39ba5bba32402200810ed4f9ab21f4ad7 100644 --- a/extension/Tests/Unit/Core/Report/LinkTest.php +++ b/extension/Tests/Unit/Core/Report/LinkTest.php @@ -101,6 +101,9 @@ class LinkTest extends TestCase { 'finalClass' => '', 'finalQuestion' => '', 'finalThumbnail' => '', + 'beforeLink' => '', + 'afterLink' => '', + 'orderText' => '', ]; $link = new Link($this->sip, DB_INDEX_DEFAULT, true); @@ -892,6 +895,24 @@ class LinkTest extends TestCase { $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon-envelope" ></span></a>', $result); } + /** + * @throws \CodeException + * @throws \UserFormException + * @throws \UserReportException + */ + public function testBeforeAfterText() { + $link = new Link($this->sip, DB_INDEX_DEFAULT, true); + + $result = $link->renderLink('u:http://example.com|t:Link|v:Hello '); + $this->assertEquals('Hello <a href="http://example.com" >Link</a>', $result); + + $result = $link->renderLink('u:http://example.com|t:Link|V: world'); + $this->assertEquals('<a href="http://example.com" >Link</a> world', $result); + + $result = $link->renderLink('u:http://example.com|t:Link|v:Hello |V: world'); + $this->assertEquals('Hello <a href="http://example.com" >Link</a> world', $result); + } + /** * @expectedException UserReportException * @@ -1756,6 +1777,20 @@ EOF; $this->assertEquals($expect, $result); } + /** + * @throws \CodeException + * @throws \UserFormException + * @throws \UserReportException + */ + public function testLinkOrderText() { + $link = new Link($this->sip, DB_INDEX_DEFAULT, true); + + // Normal definition + $expect = '<span style="display: none;">searchText</span><a href="'. $this->baseUrl .'testPage" >'. $this->baseUrl .'testPage</a>'; + $result = $link->renderLink('p:id=testPage|Y:searchText'); + $this->assertEquals($expect, $result); + } + /** * @expectedException UserReportException * diff --git a/extension/Tests/Unit/Core/Store/StoreTest.php b/extension/Tests/Unit/Core/Store/StoreTest.php index ec92bc357fd5863f8f3834776c912f3aa3040220..1921ce631bf209f565e62fb89ac86fb57561dc57 100644 --- a/extension/Tests/Unit/Core/Store/StoreTest.php +++ b/extension/Tests/Unit/Core/Store/StoreTest.php @@ -429,12 +429,12 @@ class StoreTest extends TestCase { SYSTEM_CMD_PDF2PS => 'pdf2ps', SYSTEM_CMD_PS2PDF => 'ps2pdf', SYSTEM_REMEMBER_LAST_PILL => 1, - SYSTEM_REMEMBER_LAST_PILL => 1, SYSTEM_DATE_TIME_PICKER_TYPE => 'qfq', SYSTEM_HTTP_ORIGIN => 'http://' . DOMAIN_EXAMPLE_COM, SYSTEM_IMAGE_UPLOAD_DIR => 'fileadmin/imageUploadDir', - 'baseUrlLang' => HTTP_EXAMPLE_COM, - SYSTEM_FORCE_SMTP_SENDER => '' + SYSTEM_BASE_URL_LANG => HTTP_EXAMPLE_COM, + SYSTEM_FORCE_SMTP_SENDER => '', +// SYSTEM_DO_NOT_LOG_COLUMN => SYSTEM_DO_NOT_LOG_COLUMN_DEFAULT, ]; diff --git a/extension/composer.json b/extension/composer.json index 1e0adcf6e5ff6f577ce6c1065c8cd72552a47013..0b570dba8cb4901a8e0b09065c93bfc8bde9cd49 100644 --- a/extension/composer.json +++ b/extension/composer.json @@ -2,7 +2,8 @@ "require": { "phpoffice/phpspreadsheet": "^1.3", "ext-json": "*", - "twig/twig": "^2.0" + "twig/twig": "^2.0", + "ezyang/htmlpurifier": "^4.15" }, "require-dev": { "phpunit/phpunit": "^6.5" diff --git a/extension/ext_conf_template.txt b/extension/ext_conf_template.txt index 0aa66eee90991635c02826702877002118ac5197..1121d73f76c8e774108da781be00bcb9ea934324 100644 --- a/extension/ext_conf_template.txt +++ b/extension/ext_conf_template.txt @@ -193,6 +193,8 @@ clearMe = 0 # cat=form-config/config; type=boolean; label=On form load, bring last used pill to front rememberLastPill = 1 +# cat=form-config/config; type=string; label=Do not log column:Default is 'password'. Comma separated more than one column possible. +doNotLogColumn = password # cat=form-layout/layout; type=string; label=FormElement label align:Default is 'left'. Possible values: 'left', 'center', 'right'. diff --git a/javascript/src/Element/Textual.js b/javascript/src/Element/Textual.js index 7cc73b414e6302deaf79c8d20cef3b2f55ee8270..f22ee575d36749a9ccef7d795519698640054170 100644 --- a/javascript/src/Element/Textual.js +++ b/javascript/src/Element/Textual.js @@ -65,7 +65,21 @@ QfqNS.Element = QfqNS.Element || {}; Textual.prototype.constructor = Textual; Textual.prototype.setValue = function (val) { - this.$element.val(val); + // Typeahead delivers an array with refreshed value after save progress. + if (Array.isArray(val)) { + this.setTypeAheadInput(val); + } else { + this.$element.val(val); + } + }; + + // Apply and see changed value after save (no page reload needed), typeahead elements needs to be handled separately. + Textual.prototype.setTypeAheadInput = function (value) { + this.$element.val(value[0].key); + // Display new value and correctly set new key in hidden input element. + this.$element.eq(1).typeahead('val', value[0].value); + var hiddenElement = this.$element.eq(0).closest('div').find('input[type=hidden]'); + hiddenElement.val(value[0].key); }; Textual.prototype.getValue = function () { diff --git a/javascript/src/Form.js b/javascript/src/Form.js index 359f7a230a5603555ee5c3e4588ff9c3ac228685..ad798e52c6b4b68d7cdd5044148c9ee1c74c1f24 100644 --- a/javascript/src/Form.js +++ b/javascript/src/Form.js @@ -166,10 +166,18 @@ var QfqNS = QfqNS || {}; this.eventEmitter.emitEvent('form.submit.before', n.EventEmitter.makePayload(this, null)); submitUrl = this.makeUrl(to, queryParameters); - $.post(submitUrl, this.$form.serialize()) + + // For better dynamic update compatibility (checkboxes). All input elements need to be not disabled for fully serializing by jquery. + var form = $(this.$form[0]); + // Get even disabled inputs + var disabled = form.find(':input:disabled').removeAttr('disabled'); + var serializedForm = this.$form.serialize(); + // Reset disabled inputs + disabled.attr('disabled','disabled'); + + $.post(submitUrl, serializedForm) .done(this.ajaxSuccessHandler.bind(this)) .fail(this.submitFailureHandler.bind(this)); - console.log(this.$form.serialize()); }; n.Form.prototype.serialize = function () { diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js index ca0c47c9b1f826d5c4760b3d1f0833129ac4690a..ffb973c35bcb4ce787fc2fecf069e1d75a66a75b 100644 --- a/javascript/src/QfqForm.js +++ b/javascript/src/QfqForm.js @@ -521,7 +521,16 @@ var QfqNS = QfqNS || {}; if (this.formImmutableDueToConcurrentAccess) { return; } - $.post(this.dataRefreshUrl, this.form.serialize(), "json") + + // For better dynamic update compatibility (checkboxes). All input elements need to be not disabled for fully serializing by jquery. + var form = $(this.form.$form[0]); + // Get all disabled inputs + var disabled = form.find(':input:disabled').removeAttr('disabled'); + var serializedForm = this.form.serialize(); + // Reset disabled inputs + disabled.attr('disabled','disabled'); + + $.post(this.dataRefreshUrl, serializedForm, "json") .fail(n.Helper.showAjaxError) .done(function (data) { this.handleFormUpdate(data); diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index c86b976ff9ffd3609062f6ab7a7e38858ddb8906..0e479dece7520ef2b1a65c89ffabc240de573e61 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -1369,12 +1369,13 @@ thead.qfq-sticky td { @font-face { font-family: 'password'; font-style: normal; - font-weight: 400; - src: url(https://jsbin-user-assets.s3.amazonaws.com/rafaelcastrocouto/password.ttf); + font-size: 15px; + src: url(../fonts/password-dots.ttf); } input.qfq-password { font-family: 'password'; + letter-spacing: -3px; } /* bootstrap correction for better looking arrows in datetimepicker */