diff --git a/doc/HTML.md b/doc/HTML.md index 2fd09e25523f921b14765800f42a4def042834dc..0c1db3313c5dadda63c913688724d9449c13c53b 100644 --- a/doc/HTML.md +++ b/doc/HTML.md @@ -50,9 +50,9 @@ The SIP will store: Use with SQL: `typeAheadSql` -Use with LDAP: -* `typeAheadLdapServer` -* `typeAheadLdapBaseDn` +Use with LDAP: `typeAheadLdap` +* `ldapServer` +* `ldapBaseDn` * `typeAheadLdapSearch` * `typeAheadLdapValuePrintf` * `typeAheadLdapKeyPrintf` diff --git a/doc/TINYMCE.md b/doc/TINYMCE.md new file mode 100644 index 0000000000000000000000000000000000000000..d47a924f0ed8dd0f318a23df5ccc5b3a7bd76963 --- /dev/null +++ b/doc/TINYMCE.md @@ -0,0 +1,6 @@ +Notes +===== + +* Surround <p> Tag - https://www.tinymce.com/docs/configure/content-filtering/#forced_root_block >> + In QFQ Formeditor on the specific FormElement.parameter: `editor-forced_root_block=false` + \ No newline at end of file diff --git a/extension/Documentation/Index.rst b/extension/Documentation/Index.rst index bac666e318dda4b3c8e2eb3a287e336a2c31dc6d..e8151442028da861d3125094df81a0f366ab3ff8 100644 --- a/extension/Documentation/Index.rst +++ b/extension/Documentation/Index.rst @@ -58,4 +58,5 @@ QFQ Extension :maxdepth: 4 Manual + Release Links diff --git a/extension/Documentation/Manual.rst b/extension/Documentation/Manual.rst index 8831b008be46ae9490e96b691354541e1a326d14..fa67ab660a7593ef869aae375edfc14709e41baa 100644 --- a/extension/Documentation/Manual.rst +++ b/extension/Documentation/Manual.rst @@ -417,10 +417,12 @@ Escape * 's' - single ticks will be escaped. * 'd' - double ticks will be escaped. + * 'l' - LDAP search filter values will be escaped. + * 'L' - LDAP DN values will be escaped. -* It's not possible to escape single and double ticks at the same time. +* Even it's possible to escape single and double ticks at the same time, this makes no sense. * Which of them to escape (single or double) depends on the surrounding SQL query. -* Escaping is only necessary inside of SQL queries. +* Escaping is only necessary inside of SQL or LDAP queries. Sanitize class ^^^^^^^^^^^^^^ @@ -485,6 +487,8 @@ Only variables that are known in a specified store can be substituted. +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | V | :ref:`STORE_VARS`: Generic variables | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ + | L | :ref:`STORE_LDAP`: Will be filled on demand during processing of a *FormElement* | Custom specified list of LDAP attributes | + +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | 0 | *Zero* - allways value: 0, might be helpful if a variable is empty or undefined and | Any key | | | will be used in an SQL statement. | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ @@ -669,6 +673,21 @@ Store: *VARS* - V | fileDestinaton | Destination (path & filename) for an uploaded file. Defined in an 'upload'-FormElement.parameter. Valid: same as 'filename'. | +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ +.. _STORE_LDAP: + +Store: *LDAP* - L +^^^^^^^^^^^^^^^^^ + +* Sanatized: *yes* +* See :ref:`STORE_LDAP`: + + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Name | Explanation | + +=========================+============================================================================================================================================+ + | <custom defined> | See *ldapAttributes* | + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + + .. _STORE_SYSTEM: @@ -762,6 +781,116 @@ SQL Statement * This is only possible for the outermost SELECT. +.. _LDAP: + +LDAP +==== + +A form can retrieve values from an LDAP server to display or to save them. Configuration options for LDAP will be specified +in the *parameter* field of the *Form* and/or the *FormElement*. Definitions of the *FormElement* will overwrite definitions +of the *Form*. If LDAP access is: + +* only necessary in one *FormElement*, most usefull setup is to specify all values in that specific *FormElement*, +* needed on multiple *FormElement*s (of the same *Form*, e.g. one *input* with *typeAhead*, one *note* and one *action*), it's more + efficient to specify the base parameter *ldapServer*, *ldapBaseDn* in *Form.parameter* and the rest on the current + *FormElement*. + ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| Parameter | Example | Description | Form | FormElement | Used for | ++==========================+==================================+============================================================+======+=============+==========+ +| ldapServer | directory.example.com | Hostname | x | x | TA, FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| ldapBaseDn | ou=Addressbook,dc=example,dc=com | Base DN to start the search | x | x | TA, FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| ldapAttributes | cn, email | List of attributes to save in STORE_LDAP | x | x | FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| ldapSearch | (mail=john.doe@example.com) | Regular LDAP search expresssion | x | x | FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| ldapTimeLimit | 3 (default) | Maximum time to wait for an answer of the LDAP Server | x | x | TA, FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadLdap | - | Enable LDAP as 'Typeahead' data source | | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadLdapSearch | `(|(cn=*?*)(mail=*?*))` | Regular LDAP search expresssion | x | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadLdapValuePrintf | `'%s / %s', cn, email` | Custom format to disply attributes, as value | x | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadLdapKeyPrintf | `'%s', email` | Custom format to disply attributes, as key | x | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadLimit | 20 (default) | Result will be limited to this number of entries | x | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| typeAheadMinLength | 2 (default) | Minimum number of characters before starting the search | x | x | TA | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ +| fillStoreLdap | - | Activate `Fill STORE LDAP` with the first retrieved record | | x | FSL | ++--------------------------+----------------------------------+------------------------------------------------------------+------+-------------+----------+ + +* At the moment only anonymous access is supported. +* *typeAheadLimit*: there might be a hard limit on the server side (e.g. 100) - which can't be extended. + +.. _LDAP_Typeahead: + +Typeahead (TA) +-------------- + +*Typeahead* offers continous searching of a LDAP directoy by using a regular *FormElement* of type *text*. +The *FormElement.parameter*=*typeAheadLdap* will trigger LDAP searches on every user **keystroke** +(starting after *typeAheadMinLength* keystrokes) for the current *FormElement* - this is different from *dynamicUpdate* +(triggered by leaving focus of an input element). Typeahead delivers a list of elements. + +* *FormElement.parameter.typeAheadLdap* - activate the mode *Typeahead* - no value is needed, the existence is suffucient. +* *Form.parameter* or *FormElement.parameter*: + + * *ldapServer* = `directory.example.com` + * *ldapBaseDn* = `ou=Addressbook,dc=example,dc=com` + * *typeAheadLdapSearch* = `(|(cn=*?*)(mail=*?*))` + * *typeAheadLdapValuePrintf* = `'%s / %s', cn, email` + * *typeAheadLdapKeyPrintf* = `'%s', email` + +All fetched LDAP values will be formatted with: +* *typeAheadLdapValuePrintf*, shown to the user in a drop-down box and +* *typeAheadLdapKeyPrintf*, which represents the final data to save. + +The `key/value` translation is compareable to a regular select drop-down box with key/value pairs. +Only attributes, defined in *typeAheadLdapValuePrintf* / *typeAheadLdapKeyPrintf* will be fetched from the LDAP directory. +To examine all possible values of an LDAP server, use the commandline tool `ldapsearch`. E.g.:: + + ldapsearch -x -h directory.example.com -L -b ou=Addressbook,dc=example,dc=com "(mail=john.doe@example.com)" + +All occurences of a '?' in *ldapSearch* will be replaced by the user data typed in via the text-*FormElement*. +The typed data will be escaped to fullfill LDAP search limitations. +Regular *Form* variables might be used on all parameter and will be evaluated during form load - *not* at the time when +the user types something. + +.. _Fill_LDAP_STORE: + +Fill STORE LDAP (FSL) +--------------------- + +Before processing a *FormElement*, an optional configured FLS-action loads **one** record from a LDAP directory and stores +the named attributes in STORE_LDAP. If the LDAP search query selects more than one record, only the first record is processed. +The attributes names always becomes lowercase (PHP implentation detail on get_ldap_entries()) in the store. To make +accessing STORE_LDAP easily, the keys are implemented case insensitive for this specific store. FLS is triggered during *Form*-... +* load, +* dynamic update, +* save. + +The FLS happens *before* the main *FormElement* processing starts. Therefore the fetched LDAP data (specified by *ldapAttributes*), +are available via `{{<attributename>:L:allbut:s}}` during the regular *FormElement* processing. Take care to specify +a sanatize class and optional escaping on further processing of those data. + +Important: LDAP access might slow down the *Form* processing on load, update or save! The timeout (default: 3 seconds) have + to be multiplied by the number of accesses. E.g. a broken LDAP connection and 3 *FormELements* with *FSL* + results to 9 seconds delay on save. Also be prepared not to receive the expected data. + +* *FormElement.parameter.fillStoreLdap* - activate the mode *Fill S* - no value is needed, the existence is suffucient. +* *Form.parameter* or *FormElement.parameter*: + + * *ldapServer* = `directory.example.com` + * *ldapBaseDn* = `ou=Addressbook,dc=example,dc=com` + * *typeAheadLdapSearch* = `(|(cn=*?*)(mail=*?*))` + * *ldapAttributes* = `givenName, sn, telephoneNumber, email` + * *ldapSearch* = `(mail={{email::l}})` + +After filling the store, access the content via `{{<attributename>:allbut:L:s}}`. Form ==== @@ -862,31 +991,55 @@ parameter * Comments: lines starting with a '#' are treated as a comment and will not be parsed. -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| Name | Type | Description | -+========================+========+==========================================================================================================+ -| maxVisiblePill | int | Show pills upto <maxVisiblePill> as button, all further in a dropdown menu. Eg.: maxVisiblePill=3 | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| class | string | HTML div with given class, surrounding the whole form. Eg.: class=container-fluid | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| classPill | string | HTML div with given class, surrounding the `pill` title line. | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| classBody | string | HTML div with given class, surrounding all *FormElement*. | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| submitButtonText | string | Show save button, with the <submitButtonText> at the bottom of the form | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| extraDeleteForm | string | Name of a form which specifies how to delete the primary record and optional slave records | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| data-pattern-error | string | Pattern violation: Text for error message used for all FormElements of current form | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| data-required-error | string | Required violation: Text for error message used for all FormElements of current form | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| data-match-error | string | Match violation: Text for error message used for all FormElements of current form | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| data-error | string | If none specific is defined: Text for error message used for all FormElements of current form | -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ -| buttonOnChangeClass | string | Color for save button after user modified some content or current form. E.g.: 'btn-info alert-info' + -+------------------------+--------+----------------------------------------------------------------------------------------------------------+ ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| Name | Type | Description | ++==========================+========+==========================================================================================================+ +| maxVisiblePill | int | Show pills upto <maxVisiblePill> as button, all further in a drop-down menu. Eg.: maxVisiblePill=3 | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| class | string | HTML div with given class, surrounding the whole form. Eg.: class=container-fluid | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| classPill | string | HTML div with given class, surrounding the `pill` title line. | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| classBody | string | HTML div with given class, surrounding all *FormElement*. | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| submitButtonText | string | Show save button, with the <submitButtonText> at the bottom of the form | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| extraDeleteForm | string | Name of a form which specifies how to delete the primary record and optional slave records | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| data-pattern-error | string | Pattern violation: Text for error message used for all FormElements of current form | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| data-required-error | string | Required violation: Text for error message used for all FormElements of current form | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| data-match-error | string | Match violation: Text for error message used for all FormElements of current form | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| data-error | string | If none specific is defined: Text for error message used for all FormElements of current form | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| buttonOnChangeClass | string | Color for save button after user modified some content or current form. E.g.: 'btn-info alert-info' | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| ldapServer | string | FQDN Ldap Server. E.g.: directory.example.com | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| ldapBaseDn | string | E.g.: `ou=Addressbook,dc=example,dc=com` | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| ldapAttributes | string | List of attributes to fill STORE_LDAP with. E.g.: cn, email | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| ldapSearch | string | E.g.: `(mail={{email::alnumx:l}})` | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| ldapTimeLimit | int | Maximum time to wait for an answer of the LDAP Server | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadLdap | - | Enable LDAP as 'Typeahead' data source | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadLdapSearch | string | Regular LDAP search expresssion. E.g.: `(|(cn=*?*)(mail=*?*))` | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadLdapValuePrintf | string | Value formatting of LDAP result, per entry. E.g.: `'%s / %s / %s', mail, roomnumber, telephonenumber` | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadLdapKeyPrintf | string | Key formatting of LDAP result, per entry. E.g.: `'%s', mail` | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadLimit | int | Maximum number of entries. The limit is applied to the server (LDAP or SQL) and the Client | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| typeAheadMinLength | int | Minimum number of characters which have to typed to start the search. | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| fillStoreLdap | - | Activate `Fill STORE LDAP` with the first retrieved record | ++--------------------------+--------+----------------------------------------------------------------------------------------------------------+ * Example: @@ -1007,13 +1160,13 @@ Type: pill * FormElement settings: * *name*: technical name, used as HTML identifier. - * *label*: Label shown on the corresponding pill button or inside the dropdown menu. + * *label*: Label shown on the corresponding pill button or inside the drop-down menu. * *type*: *pill* * *feIdContainer*: `0` - Pill's can't be nested. * *parameter*: * *maxVisiblePill*: `<nr>` - Number of Pill-Buttons shown. Undefined means unlimited. Excess Pill buttons will be - displayed as a dropdown menu. + displayed as a drop-down menu. .. _class-native: @@ -1389,6 +1542,8 @@ Type: text * *retype* = 1 (optional): Current input element will be rendered twice. The form can only submitted if both elements are equal. * *retypeLabel* =<text> (optional): The label of the second element. * *retypeNote* =<text> (optional): The note of the second element. + * *characterCountWrap* = <text1>|<text2> (optional). Displays a character counter below the input/textarea element. If + `text1` / `text2` is missing, just display `<current>/</max>`. Customization: `characterCountWrap=<div class=qfq-cc-style>Count: |</div>` * Also check the :ref:`fe-parameter-attributes` *data-...-error* to customize error messages shown by the validator. @@ -1396,9 +1551,9 @@ Type Ahead '''''''''' Activating `typeahead` functionality offers an instant lookup of data and displaying them to the user, while the user is -typing. A dropdown box offers the results. As datasource the regular SQL connection or a LDAP query can be used. +typing, a drop-down box offers the results. As datasource the regular SQL connection or a LDAP query can be used. With every keystroke (starting from the *typeAheadMinLength* characters), the already typed value will be transmitted to -the server, the lookup will be performed and the result is displayed as the dropdown box. +the server, the lookup will be performed and the result, upto *typeAheadLimit* entries, are displayed as a drop-down box. * *FormElement.parameter*: @@ -1408,6 +1563,12 @@ the server, the lookup will be performed and the result is displayed as the drop Depending of the `typeahead` setup, the given FormElement will contain the displayed `value` or `key` (if a key/value dict is configured). +Configuration via Form / FormElement +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +All of the `typeAhead*` (except `typeAheadLdap`) and `ldap*` parameter can be specified either in +*Form.parameter* or in *FormElement.parameter*. + SQL ;;; @@ -1423,14 +1584,7 @@ SQL LDAP ;;;; -* *FormElement.parameter*: - - * *typeAheadLdapServer* = FQDN of the searched server. E.g.: `directory.uzh.ch` - * *typeAheadLdapBaseDn* = Base DN. E.g.: `ou=Addressbook,dc=uzh,dc=ch` - * *typeAheadLdapSearch* = LDAP search expression. E.g.: `(|(cn=*?*)(mail=*?*)(ou=*?*)(roomNumber=*?*)(telephoneNumber=*?*))` - * *typeAheadLdapValuePrintf* = regular printf expression, LDAP attributenames will be used as variablenames. Will be shown - in the dropdownbox. E.g.: `'%s / %s / %s', mail, roomNumber, telephoneNumber` - * *typeAheadLdapKeyPrintf* = Same as `ldapValuePrintf` - on save, these content will be saved. E.g.: `'%s', mail` +See :ref:`LDAP_Typeahead` Type: editor ^^^^^^^^^^^^ @@ -1438,8 +1592,12 @@ Type: editor * TinyMCE (https://www.tinymce.com, community edition) is used as the QFQ Rich Text Editor. * The content will be saved as HTML inside the database. * All configuration and plugins will be configured via the 'parameter' field. Just prepend the word 'editor-' in front - of each TinyMCE keyword. Check possible options under https://www.tinymce.com/docs/configure/, - https://www.tinymce.com/docs/plugins/, https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols + of each TinyMCE keyword. Check possible options under: + + * https://www.tinymce.com/docs/configure/, + * https://www.tinymce.com/docs/plugins/, + * https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols + * Bars: * Top: *menubar* - by default hidden. @@ -1453,6 +1611,12 @@ Type: editor editor-toolbar=code searchreplace undo redo | styleselect link table | fontselect fontsizeselect | bullist numlist outdent indent | forecolor backcolor bold italic editor-menubar=false editor-statusbar=false +* To deactivate the surrouding `<p>` tag, configure in *FormElement.parameter*:: + + editor-forced_root_block=false + + This might have impacts on the editor. See https://www.tinymce.com/docs/configure/content-filtering/#forced_root_block + * *FormElement.size*: * <min_height>,<max_height>: in pixels, including top and bottom bars. E.g.: 300,600 @@ -1546,7 +1710,7 @@ Type: select * *FormElement.size*: `<value>` - * `<value>`: <empty>|0|1: Dropdown list. + * `<value>`: <empty>|0|1: drop-down list. * `<value>`: >1: Select field with *size* rows height. Multiple selection of items is possible. * *FormElement.parameter*: @@ -1919,26 +2083,29 @@ Dynamic Update -------------- The 'Dynamic Update' feature makes a form more interactive. If a user change a *FormElement* who is tagged with -'dynamicUpdate', *all* elements who are tagged with 'DynamicUpdate', will be recalculated and rerendered. +'dynamicUpdate', *all* elements who are tagged with 'dynamicUpdate', will be recalculated and rerendered. The following fields will be recalculated during 'Dynamic Update' * 'modeSql' - Possible values: 'show', 'required', 'readonly', 'hidden' +* 'label' * 'value' -* 'parameter.*' - especially 'itemList' * 'note' -* 'label' +* 'parameter.*' - especially 'itemList' To make a form dynamic: -* Mark all *FormElements* with `dynamic update`=`enabled`, which **initiates** or **receives** updates. -* Define the receiving *FormElements* in a way, that they will interpret the recent user change. The form variable of the +* Mark all *FormElements* with `dynamic update`=`enabled`, which should **initiate** or **receive** updates. +* Define the receiving *FormElements* in a way, that they will interpret the recent user change! The form variable of the specific sender *FormElement* `{{<sender element>:F:<sanitize>}}` should be part of one of the above fields to get an impact. E.g.: :: [receiving *FormElement*].parameter: itemList={{ SELECT IF({{carPriceRange:FE:alnumx}}='expensive','Ferrari,Tesla,Jaguar','General Motors,Honda,Seat,Fiat') }} + Remember to specify a 'sanatize' class - a missing sanatize class means 'digit', every content which is not numeric + violate the sanatize class and becomes therefore an empty string! + Examples ^^^^^^^^ @@ -1962,7 +2129,6 @@ Show / Hide a *FormElement* modeSql={{SELECT IF( '{{music:FR:alnumx}}'='pop' ,'show', 'hidden' }} - .. _form-layout: Form Layout @@ -2082,17 +2248,17 @@ via SIP parameter to the secondary form. On the secondary form: for 'new' records choose the computed value, for existing records leave the value unchanged. -* Primary form, `subrecord` *FormElement*, field `parameter`: set :: +* Master form, `subrecord` *FormElement*, field `parameter`: set :: detail=id:formId,{{SELECT '&', IFNULL(fe.ord,0)+10 FROM Form AS f LEFT JOIN *FormElement* AS fe ON fe.formId=f.id WHERE f.id={{r:S0}} ORDER BY fe.ord DESC LIMIT 1}}:ord -* Secondary form, `ord` *FormElement*, field `value`: set +* Slave form, `ord` *FormElement*, field `value`: set :: - `{{RS0}}`. + `{{ord:RS0}}`. Version 2 ''''''''' @@ -2440,6 +2606,94 @@ Table: Note sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} sqlAfter={{UPDATE Person SET noteIdAvatar={{slaveId}} WHERE id={{id:R0}} LIMIT 1 +Typeahead: SQL +^^^^^^^^^^^^^^ + +Table: Person + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | name | varchar(255) | + +---------------------+--------------+ + +* Form: + + * Name: PersonNameTypeahead + * Table: Person + +* FormElements + + * Name: name + + * Type: text + * Label: Name + * Parameter:: + + typeAheadSql = SELECT name WHERE name LIKE ? OR firstName LIKE ? LIMIT 100 + +Typeahead: LDAP with additional values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Table: Person + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | name | varchar(255) | + +---------------------+--------------+ + | firstname | varchar(255) | + +---------------------+--------------+ + | email | varchar(255) | + +---------------------+--------------+ + +* Form: + + * Name: PersonNameTypeaheadSetNames + * Table: Person + * Parameter:: + + ldapServer = directory.example.com + ldapBaseDn = ou=Addressbook,dc=example,dc=com + +* FormElements + + * Name: email + + * Class: native + * Type: text + * Label: Email + * Note: Name: {{cn:LE}}<br>Email: {{mail:LE}} + * dynamicUpdate: checked + * Parameter:: + + # Typeahead + typeAheadLdapSearch = (|(cn=*?*)(mail=*?*)) + typeAheadLdapValuePrintf ‘%s / %s’, cn, email + typeAheadLdapKeyPrintf ‘%s’, email + + # dynamicUpdate: show note + fillStoreLdap + ldapSearch = (mail={{email::alnumx}}) + ldapAttributes = cn, email + + * Name: fillLdapValues + + * Class: action + * Type: afterSave + * Parameter:: + + fillStoreLdap + ldapSearch = (mail={{email::alnumx}}) + ldapAttributes = cn, email + + slaveId={{id:R0}} + sqlUpdate={{ UPDATE Person AS p SET p.name='{{cn:L:alnumx:s}}' WHERE p.id={{slaveId}} LIMIT 1 }} + FAQ --- @@ -3795,249 +4049,3 @@ Same as above, but written in the nested notation :: } * Columns starting with a '_' won't be printed but can be accessed as regular columns. - - -.. _release: - -Release -======= - -Version 0.future ----------------- - -Changes -^^^^^^^ - - * Play formEditor.sql. - - * Dropdownlist of containerassigment updated. - -Features -^^^^^^^^ - -Bug Fixes -^^^^^^^^^ - -Version 0.13 ------------- - -Changes -^^^^^^^ - - * Play formEditor.sql. - * formEditor.sql: - - * Checktype of `Form.name` restricted to `alnumx` (prior `all`). - * Changed `access` for Form `form` & '`ormElement` from `always` to `sip`. - - * Table `FormElement` - - * Modified column: `checkType` - new value `numerical` - - ALTER TABLE FormElement MODIFY COLUMN checkType ENUM('alnumx','digit','numerical','email','min|max','min|max date', - 'pattern','allbut','all') NOT NULL DEFAULT 'alnumx' - - * Example Report for `forms` extended by a delete button per row. - -Features -^^^^^^^^ - - * print.php: offers 'print page' for any local page - create a PDF on the fly (printout is then browser independent). - - * Install `wkhtmltopdf` on the webserver (http://wkhtmltopdf.org/). - * In config.qfq.ini setup: - - BASE_URL_PRINT=http://www.../ - WKHTMLTOPDF=/opt/wkhtmltox/bin/wkhtmltopdf - - * Check and error report if 'php_intl' is missing. - * New Checktype 'allow numerical'. - * Documentation: example for 'radio' with no pre selection. - * #3063, Radios and checkboxes optional rendered in Bootstrap layout. - * Added 'help-box with-errors'-DIV after radios and checkboxes. - * Respect attribute `data-class-on-change` on save buttons. - - -Bug Fixes -^^^^^^^^^ - - * #2138 / digit sanitize: new class 'numerical' implemented. - * Fixed recursive thrown exception. - * #2064 / search of a default value for a non existing tablecolumn returns 'false'. - - * Fixed setting of STORE_SYSTEM / showDebugInfo during API call. - - * #2081, #3180 Form: Label & note - update via `DynamicUpdate` - * #3253, if there is no STORE_TYPO3 (calls through .../api/ like save, delete, load): use SIP / CLIENT_TYPO3VARS. - * qfq-bs.css: - - * Alignment of checkboxes and radios optimized. - * CSS class 'qfq-note' for 'notes' (third column in a form). - - -Version 0.12 ------------- - -Changes -^^^^^^^ - - * Table 'FormElement' - * New column: rowLabelInputNote - - ALTER TABLE `FormElement` ADD `rowLabelInputNote` set('row','label','/label','input','/input','note','/note','/row') - NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row' AFTER `bsNoteColumns` ; - - * Modified column: 'type' - new value 'templateGroup' - - ALTER TABLE `FormElement` CHANGE `type` `type` ENUM( 'checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', - 'gridJQW', 'text', 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', - 'templateGroup', 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', - 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail' ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'text'; - - * formEditor.sql: Added HTML 'placeholder' in FormEditor for bs*Columns. - - * PLAY 'formEditor.sql'. - - * User Input will be UTF8 normalized. - - * INSTALL 'php5-intl' or 'php7.0-intl' on Webserver. - - * Add globalize.js to be included. Needed by jqx-all.js - - * UPDATE EXISTING TypoScript TEMPLATES of QFQ Installation. - - * Name of variable '_filename' (used in field 'parameter') has changed. Old: '_filename', New: 'filename' - - * UPDATE `FormElement` SET parameter = REPLACE(parameter, '_filename', 'filename') - - -Features -^^^^^^^^ - - * User input will be UTF8 normalized - * config.qfq-ini: - * New configuration values: FORM_BS_LABEL_COLUMNS / FORM_BS_INPUT_COLUMNS / FORM_BS_NOTE_COLUMNS - * Comment empty variables - the new default setting is, that empty parameter in config.qfq.ini means EMPTY (=parameter is set and will not be overwritten by internal default), not UNDEFINED (overwritten by internal default). - * FileUpload: - * Implemented new Formelement.parameter: fileReplace=always - will replace existing files. - * Multiple / Advanced Upload: new logic implements slaveId, sqlInsert, sqlUpdate, sqlDelete. - * FormElement.parameter: sqlBefore / sqlAfter fired during 'Form' save for action elements. - * STORE FORM: variable 'filename' moved to STORE VAR - sanatize class needs no longer specified. - * STORE VAR: two new variables 'filename' and 'fileDestination' valid during processing of current upload FormElement. - * Default store priority list changed. Old: 'FSRD', New: 'FSRVD'. - * CODING.md: update doc for FormElement 'upload' and general 'Form' rendering & save (recursive rendering). - * User manual: - * Described form layout options: description for bsLabelColumn, bsInputColumn, bsNoteColumn - * Update 'file-upload' doc. - * Described 3 examples for upload forms. - * Administrator manual: - * Add description page.meta... - * New FormElement (type= 'container') added: 'templateGroup' - * FormElement.parameter.tgAddClass | tgAddText | tgRemoveClass | tgRemoveText | tgClass - * FormElement.maxSize: max number of duplicates - * #3230 templateGroup: margin between copies. 'tgClass' implemented. - * Native FormElements: - * FormElement.parameter.htlmlBefore|htmlAfter - add the specified HTML code before or after the element (outside of any wrapping) - * #3224, #3231 Html Tag <hr> als FormElement. >> htmlBefore | htmlAfter. - * FormElement.parameter.wrapLabel | wrapInput | wrapAfter | wrapRow - if specified, any default wrapping is omitted. - * FormElement.bsNoteColumns | bsInputColumns | bsNoteColumns - a '0' will suppress the whole rendering of the item. - * FormElement.rowLabelInputNote - switch on/off rendering of the corresponding system wrapping items. - * #3232 Define custom 'on-change' color - used for the save button: Form.parameter.buttonOnChangeClass=... - * Form.parameter & FormElement.parameter: Lines starting with '#' are treated as comments and will not be parsed. - -Bug fixes -^^^^^^^^^ - - * User manual: - * Fixed double include of validator.js in T3 Typoscript template example. - * Fixed wrong store name SYSTEM: S > Y - * Fixed wrong STORE_FORM variable names. - * Reformat FormElement.parameter description. - * Styling errors fixed. - * Use of 'decryptCurlyBraces()' to get better error messages. - * Skip unwanted parameter expansion during save. - * Fixed bug with uninitialized FE_SLAVE_ID - * formEditor.sql: - * The defintion as 'editor' (not text) for FormElement 'note' has been lost - reinserted. - * Fixed problem while playing SQL query - deleting old FormElements of Formeditor deleted also FormElements of other forms. - * #3066 / help-text with-error - CSS class 'hidden' will be rendered by default (as long there is no error). - * Labels are skipped, if FormElement.bsLabelColumns=0. - * Respect attribute `data-class-on-change` on save buttons. - -Version 0.11 ------------- - -Features -^^^^^^^^ - - * Added STORE_BEFORE, #3146 - Mainly used to compare old and new values during a form 'save' action. - * Added 'best practice' for defining and using of 'Central configure values' in UserManual. - * Added accent characters to sanatize class 'alnumx', #3183. - * Set default all QFQ send mails to 'auto-submit'. - * Added possibility to customize error messages ('data-pattern-error', 'data-rquired-error', 'data-match-error', - 'data-error') if validation fails. Customization can be done on global level (config.qfq.ini), per Form or per FormElement. - * *FormElement*: Double an input element and validate that the input match: FormElement.parameter.retype=1 - * Autofocus in Forms is now supported. By default the first Input Element receives the focus. Can be customized. - * Added a timestamp in shown exceptions. Usefull for screenshots, send by customer, to find the problem in SQL logfiles. - -Bug fixes -^^^^^^^^^ - - * Fixed missing docutmentation for FormElement 'note'. - * Failed SQL queries will now always be logged, even if they do not modify some data. - -Version 0.10 ------------- - -Features -^^^^^^^^ - - * Implemented Parameter 'extraDeleteForm' for 'forms' and 'subrecords'. Update doc. - -Bug fixes -^^^^^^^^^ - - * Suppress rendering of form title during a 'delete' call. No one will see it and required parameters are not supplied. - * In case of broken SQL queries, print them in ajax error message. - * Remove parameter 'table' from Delete SIP URLs. ToolTip updated. - -Version 0.9 ------------ - -Features -^^^^^^^^ - - * FormEditor: - * design update - new default background color: grey. - * per form configureable background colors. - * Optional right align of all form element labels. - * Added config.qfq.ini values CSS_CLASS_QFQ_FORM_PILL, CSS_CLASS_QFQ_FORM_BODY, CSS_CLASS_QFQ_CONTAINER. - -Bug fixes -^^^^^^^^^ - - * BuildFormBootstrap.php: added new class name 'qfq-label' to form labels - needed to assign 'qfq-form-right' class. Changed wrapping of formelements from 'col-md-8' (wrong) to 'col-md-12'. - * QuickFormQuery.php: Set default for new F_CLASS_PILL & F_CLASS_BODY. - * formEditor.sql: New default background color for formElements is blue. - * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-..., qfq-form-right. - * Index.rst: Add note to hierachy chars. Fixed uncomplete doc to a) bs*Columns, showButton. Add classPill, classBody. Rewrote form.paramter.class. - * QuickFormQuery.php: Button save/ close/ delete/ new - align to right border of form. - * UsersManual/index.rst: renamed chapter for formelements. Cleanup formelement types. Wrote chapter 'Detailed concept'. - * QuickFormQuery.php, FormAction.php: '#2931 / afterSave Hauptrecord xId nicht direkt verfügbar' - load master record again, after 'action'-elements has been processed. - * UsersManual/index.rst: Startet FAQ section. - * config.qfq.example.ini: Added comment where to save config.qfq.ini. - * UsersManual/index.rst: Rewrite of 'action'-FormElement definition. - * #2739: beforeDelete / afterDelete. - * PROTOCOL.md: update 'delete' description. - * delete.php: fixed unwanted loose of MSG_CONTENT. - * Report.php: Fixed double '&&' in building UrlParam. - * FormAction.php: In case of 'AFTER_DELETE', do not try to load primary record - that one is already deleted. - * Sip.php: Do not skip SIP_TARGET_URL as parameter for the SIP. - * #3001 Report: delete implementieren. - * Index.rst, Constants.php: reverted parameter '_table' in delete links back to 'table' - Reason: 'form' needs to be 'form' (instead of '_form') due to many used places already. - * Sip.php: move SIP_TARGET_URL back to stored inside SIP - it's necessary for 'delete'-links. - * Report.php, Constants.php: Remove code to handle unecessary 'p:' tag for delete links. - * Link.php: Check paged / Paged that the parameter r, table and form are given in the right combination. - * Link.php, Report.php: New '_link' token 'x'. '_paged' and '_Paged' are rendered via Link() class, Link() class now supports delete links. - * QuickFormQuery.php: for modeForm='Form Delete' the 'required param' are not respected - this makes sense, cause these parameters typically filled in newly created records. - * Fixed: #3076 Delete Button bei Subrecords erzeugt sporadisch Javascript Exceptions (Webkit: Chrome / Vivaldi) - kein loeschen moeglich. diff --git a/extension/Documentation/Release.rst b/extension/Documentation/Release.rst new file mode 100644 index 0000000000000000000000000000000000000000..c8df042f67b59bef4b99389d7e18bd778dd23029 --- /dev/null +++ b/extension/Documentation/Release.rst @@ -0,0 +1,285 @@ +.. _release: + +Release +======= + +Version 0.future +---------------- + +Changes +^^^^^^^ + +Features +^^^^^^^^ + +Bug Fixes +^^^^^^^^^ + +Version 0.14 +------------ + +GIT Last Commit: Sun Mar 19 20:38:18 2017 +0100 + + +Changes +^^^^^^^ + + * Play formEditor.sql. + + * All Form & FormEditor input elements now have a maxlength definition of 0, which means take the column definition value. + * Drop-down list of container assignment: + + * Display 'type' ('pill', 'fieldset', 'templategroup') instead of 'class' (always 'container'). + * Display 'name' (internal name) instead of 'label' (shown on the website and might not so usefull as 'name' which is nowhere else used than in that drop-down. + + * FormElement.placeholder colum width extended to 512: + + ALTER TABLE `FormElement` CHANGE `placeholder` `placeholder` VARCHAR(512) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT ''; + + * New class Ldap.php. + +Features +^^^^^^^^ + + * Typeahead for SQL and LDAP Datasources implemented. + * formEditor.sql: Changed width of column FormElement.placeholder from 255 to 512. Removed hardcoded 'size' in FormElement 'placeholder'. + * Character Count: Display a `counter` on input or textarea fields, activated by specifying the formElement.parameter 'characterCountWrap'. + * Evaluate.php: Two new escape options 'l' and 'L'. Backport of ldap_escape() for PHP <5.6. Multiple escaping for one value now possible. + * Manual.rst: add some example for TypeAhead and for saving LDAP value. + +Bug Fixes +^^^^^^^^^ + + * Dynamic Update has been broken since implementing of 'element-update' (#3180). Now both methods, 'element-update' and 'form-update' should be fine. + * qfq-bs.css.less: Fixed problem with 'typeahead input elements' not expanded to Bootstrap column width. Changed + Layout/Design Typeahead drop-down box. Add hoover for the drop-down box with a blue background + * AbstractBuildForm.php: #3374 - textarea elements now contains 'maxlength' attribute. + * BuildFormBootstrap.php: wrapping of optional 'submitButtonText' now done with the 'per form' values. + * typeahead.php: if there is an exception, the message body is sent as regular 'content' for the drop-down box. At the + moment this is the only way to transmit any error messages. + + + +Version 0.13 +------------ + +Changes +^^^^^^^ + + * Play formEditor.sql. + * formEditor.sql: + + * Checktype of `Form.name` restricted to `alnumx` (prior `all`). + * Changed `access` for Form `form` & '`ormElement` from `always` to `sip`. + + * Table `FormElement` + + * Modified column: `checkType` - new value `numerical` + + ALTER TABLE FormElement MODIFY COLUMN checkType ENUM('alnumx','digit','numerical','email','min|max','min|max date', + 'pattern','allbut','all') NOT NULL DEFAULT 'alnumx' + + * Example Report for `forms` extended by a delete button per row. + +Features +^^^^^^^^ + + * print.php: offers 'print page' for any local page - create a PDF on the fly (printout is then browser independent). + + * Install `wkhtmltopdf` on the webserver (http://wkhtmltopdf.org/). + * In config.qfq.ini setup: + + BASE_URL_PRINT=http://www.../ + WKHTMLTOPDF=/opt/wkhtmltox/bin/wkhtmltopdf + + * Check and error report if 'php_intl' is missing. + * New Checktype 'allow numerical'. + * Documentation: example for 'radio' with no pre selection. + * #3063, Radios and checkboxes optional rendered in Bootstrap layout. + * Added 'help-box with-errors'-DIV after radios and checkboxes. + * Respect attribute `data-class-on-change` on save buttons. + + +Bug Fixes +^^^^^^^^^ + + * #2138 / digit sanitize: new class 'numerical' implemented. + * Fixed recursive thrown exception. + * #2064 / search of a default value for a non existing tablecolumn returns 'false'. + + * Fixed setting of STORE_SYSTEM / showDebugInfo during API call. + + * #2081, #3180 Form: Label & note - update via `DynamicUpdate` + * #3253, if there is no STORE_TYPO3 (calls through .../api/ like save, delete, load): use SIP / CLIENT_TYPO3VARS. + * qfq-bs.css: + + * Alignment of checkboxes and radios optimized. + * CSS class 'qfq-note' for 'notes' (third column in a form). + + +Version 0.12 +------------ + +Changes +^^^^^^^ + + * Table 'FormElement' + * New column: rowLabelInputNote + + ALTER TABLE `FormElement` ADD `rowLabelInputNote` set('row','label','/label','input','/input','note','/note','/row') + NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row' AFTER `bsNoteColumns` ; + + * Modified column: 'type' - new value 'templateGroup' + + ALTER TABLE `FormElement` CHANGE `type` `type` ENUM( 'checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', + 'gridJQW', 'text', 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', + 'templateGroup', 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', + 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail' ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'text'; + + * formEditor.sql: Added HTML 'placeholder' in FormEditor for bs*Columns. + + * PLAY 'formEditor.sql'. + + * User Input will be UTF8 normalized. + + * INSTALL 'php5-intl' or 'php7.0-intl' on Webserver. + + * Add globalize.js to be included. Needed by jqx-all.js + + * UPDATE EXISTING TypoScript TEMPLATES of QFQ Installation. + + * Name of variable '_filename' (used in field 'parameter') has changed. Old: '_filename', New: 'filename' + + * UPDATE `FormElement` SET parameter = REPLACE(parameter, '_filename', 'filename') + + +Features +^^^^^^^^ + + * User input will be UTF8 normalized + * config.qfq-ini: + * New configuration values: FORM_BS_LABEL_COLUMNS / FORM_BS_INPUT_COLUMNS / FORM_BS_NOTE_COLUMNS + * Comment empty variables - the new default setting is, that empty parameter in config.qfq.ini means EMPTY (=parameter is set and will not be overwritten by internal default), not UNDEFINED (overwritten by internal default). + * FileUpload: + * Implemented new Formelement.parameter: fileReplace=always - will replace existing files. + * Multiple / Advanced Upload: new logic implements slaveId, sqlInsert, sqlUpdate, sqlDelete. + * FormElement.parameter: sqlBefore / sqlAfter fired during 'Form' save for action elements. + * STORE FORM: variable 'filename' moved to STORE VAR - sanatize class needs no longer specified. + * STORE VAR: two new variables 'filename' and 'fileDestination' valid during processing of current upload FormElement. + * Default store priority list changed. Old: 'FSRD', New: 'FSRVD'. + * CODING.md: update doc for FormElement 'upload' and general 'Form' rendering & save (recursive rendering). + * User manual: + * Described form layout options: description for bsLabelColumn, bsInputColumn, bsNoteColumn + * Update 'file-upload' doc. + * Described 3 examples for upload forms. + * Administrator manual: + * Add description page.meta... + * New FormElement (type= 'container') added: 'templateGroup' + * FormElement.parameter.tgAddClass | tgAddText | tgRemoveClass | tgRemoveText | tgClass + * FormElement.maxSize: max number of duplicates + * #3230 templateGroup: margin between copies. 'tgClass' implemented. + * Native FormElements: + * FormElement.parameter.htlmlBefore|htmlAfter - add the specified HTML code before or after the element (outside of any wrapping) + * #3224, #3231 Html Tag <hr> als FormElement. >> htmlBefore | htmlAfter. + * FormElement.parameter.wrapLabel | wrapInput | wrapAfter | wrapRow - if specified, any default wrapping is omitted. + * FormElement.bsNoteColumns | bsInputColumns | bsNoteColumns - a '0' will suppress the whole rendering of the item. + * FormElement.rowLabelInputNote - switch on/off rendering of the corresponding system wrapping items. + * #3232 Define custom 'on-change' color - used for the save button: Form.parameter.buttonOnChangeClass=... + * Form.parameter & FormElement.parameter: Lines starting with '#' are treated as comments and will not be parsed. + +Bug fixes +^^^^^^^^^ + + * User manual: + * Fixed double include of validator.js in T3 Typoscript template example. + * Fixed wrong store name SYSTEM: S > Y + * Fixed wrong STORE_FORM variable names. + * Reformat FormElement.parameter description. + * Styling errors fixed. + * Use of 'decryptCurlyBraces()' to get better error messages. + * Skip unwanted parameter expansion during save. + * Fixed bug with uninitialized FE_SLAVE_ID + * formEditor.sql: + * The defintion as 'editor' (not text) for FormElement 'note' has been lost - reinserted. + * Fixed problem while playing SQL query - deleting old FormElements of Formeditor deleted also FormElements of other forms. + * #3066 / help-text with-error - CSS class 'hidden' will be rendered by default (as long there is no error). + * Labels are skipped, if FormElement.bsLabelColumns=0. + * Respect attribute `data-class-on-change` on save buttons. + +Version 0.11 +------------ + +Features +^^^^^^^^ + + * Added STORE_BEFORE, #3146 - Mainly used to compare old and new values during a form 'save' action. + * Added 'best practice' for defining and using of 'Central configure values' in UserManual. + * Added accent characters to sanatize class 'alnumx', #3183. + * Set default all QFQ send mails to 'auto-submit'. + * Added possibility to customize error messages ('data-pattern-error', 'data-rquired-error', 'data-match-error', + 'data-error') if validation fails. Customization can be done on global level (config.qfq.ini), per Form or per FormElement. + * *FormElement*: Double an input element and validate that the input match: FormElement.parameter.retype=1 + * Autofocus in Forms is now supported. By default the first Input Element receives the focus. Can be customized. + * Added a timestamp in shown exceptions. Usefull for screenshots, send by customer, to find the problem in SQL logfiles. + +Bug fixes +^^^^^^^^^ + + * Fixed missing docutmentation for FormElement 'note'. + * Failed SQL queries will now always be logged, even if they do not modify some data. + +Version 0.10 +------------ + +Features +^^^^^^^^ + + * Implemented Parameter 'extraDeleteForm' for 'forms' and 'subrecords'. Update doc. + +Bug fixes +^^^^^^^^^ + + * Suppress rendering of form title during a 'delete' call. No one will see it and required parameters are not supplied. + * In case of broken SQL queries, print them in ajax error message. + * Remove parameter 'table' from Delete SIP URLs. ToolTip updated. + +Version 0.9 +----------- + +Features +^^^^^^^^ + + * FormEditor: + * design update - new default background color: grey. + * per form configureable background colors. + * Optional right align of all form element labels. + * Added config.qfq.ini values CSS_CLASS_QFQ_FORM_PILL, CSS_CLASS_QFQ_FORM_BODY, CSS_CLASS_QFQ_CONTAINER. + +Bug fixes +^^^^^^^^^ + + * BuildFormBootstrap.php: added new class name 'qfq-label' to form labels - needed to assign 'qfq-form-right' class. Changed wrapping of formelements from 'col-md-8' (wrong) to 'col-md-12'. + * QuickFormQuery.php: Set default for new F_CLASS_PILL & F_CLASS_BODY. + * formEditor.sql: New default background color for formElements is blue. + * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-..., qfq-form-right. + * Index.rst: Add note to hierachy chars. Fixed uncomplete doc to a) bs*Columns, showButton. Add classPill, classBody. Rewrote form.paramter.class. + * QuickFormQuery.php: Button save/ close/ delete/ new - align to right border of form. + * UsersManual/index.rst: renamed chapter for formelements. Cleanup formelement types. Wrote chapter 'Detailed concept'. + * QuickFormQuery.php, FormAction.php: '#2931 / afterSave Hauptrecord xId nicht direkt verfügbar' - load master record again, after 'action'-elements has been processed. + * UsersManual/index.rst: Startet FAQ section. + * config.qfq.example.ini: Added comment where to save config.qfq.ini. + * UsersManual/index.rst: Rewrite of 'action'-FormElement definition. + * #2739: beforeDelete / afterDelete. + * PROTOCOL.md: update 'delete' description. + * delete.php: fixed unwanted loose of MSG_CONTENT. + * Report.php: Fixed double '&&' in building UrlParam. + * FormAction.php: In case of 'AFTER_DELETE', do not try to load primary record - that one is already deleted. + * Sip.php: Do not skip SIP_TARGET_URL as parameter for the SIP. + * #3001 Report: delete implementieren. + * Index.rst, Constants.php: reverted parameter '_table' in delete links back to 'table' - Reason: 'form' needs to be 'form' (instead of '_form') due to many used places already. + * Sip.php: move SIP_TARGET_URL back to stored inside SIP - it's necessary for 'delete'-links. + * Report.php, Constants.php: Remove code to handle unecessary 'p:' tag for delete links. + * Link.php: Check paged / Paged that the parameter r, table and form are given in the right combination. + * Link.php, Report.php: New '_link' token 'x'. '_paged' and '_Paged' are rendered via Link() class, Link() class now supports delete links. + * QuickFormQuery.php: for modeForm='Form Delete' the 'required param' are not respected - this makes sense, cause these parameters typically filled in newly created records. + * Fixed: #3076 Delete Button bei Subrecords erzeugt sporadisch Javascript Exceptions (Webkit: Chrome / Vivaldi) - kein loeschen moeglich. diff --git a/extension/qfq/api/typeahead.php b/extension/qfq/api/typeahead.php index e635c8ec782044f6b870db0d5ac54243064fe2a9..cfd2e44e1d19dec4578e3303ebb4a7f997e06911 100644 --- a/extension/qfq/api/typeahead.php +++ b/extension/qfq/api/typeahead.php @@ -19,15 +19,14 @@ require_once(__DIR__ . '/../qfq/Constants.php'); * Return JSON encoded answer * */ - - try { $qfq = new \qfq\TypeAhead(); $answer = $qfq->process(); } catch (\Exception $e) { - $answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage(); +// $answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage(); + $answer[] = [API_TYPEAHEAD_KEY => 'errror', API_TYPEAHEAD_VALUE => "Error: " . $e->getMessage()]; } header("Content-Type: application/json"); diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 849b0f73fafde49df56a29b4903721c72f26be43..8f41eb176309a7611b8bc7ec2f8303f3f3c6088a 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -22,6 +22,7 @@ require_once(__DIR__ . '/../qfq/Database.php'); require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php'); require_once(__DIR__ . '/../qfq/helper/Support.php'); require_once(__DIR__ . '/../qfq/helper/OnArray.php'); +require_once(__DIR__ . '/../qfq/helper/Ldap.php'); require_once(__DIR__ . '/../qfq/report/Link.php'); /** @@ -375,13 +376,16 @@ abstract class AbstractBuildForm { continue; // skip this FE } - $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed not trasnmitted to the form + $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form $debugStack = array(); // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); + // Fill STORE_LDAP + $fe = $this->prepareFillStoreFireLdap($fe); + // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'. if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) { Support::setIfNotSet($fe, FE_SLAVE_ID); @@ -389,7 +393,7 @@ abstract class AbstractBuildForm { $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR); } - // evaluate current FormElement + // ** evaluate current FormElement ** $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack); // Some Defaults @@ -402,8 +406,24 @@ abstract class AbstractBuildForm { //In case the current element is a 'RETYPE' element: take the element name of the source FormElement. Needed in the next row to retrieve the default value. $name = (isset($formElement[FE_RETYPE_SOURCE_NAME])) ? $formElement[FE_RETYPE_SOURCE_NAME] : $formElement[FE_NAME]; - // If there is a value explicit defined: take it + $value = ''; + Support::setIfNotSet($formElement, FE_VALUE); + + // If is FormElement['value'] explicit defined: take it + // There are two options: a) single value, b) array of values (template Group) +// if (is_array($formElement[FE_VALUE])) { +// // For Templates Groups, the 'value' has to be defined as '{{!SELECT ...' wich returns all selected records in an array. +// $idx = isset($formElement[FE_TEMPLATE_GROUP_CURRENT_IDX]) ? $formElement[FE_TEMPLATE_GROUP_CURRENT_IDX] - 1 : 0; +// if (isset($formElement[FE_VALUE][$idx]) && is_array($formElement[FE_VALUE][$idx])) { +// $value = current($formElement[FE_VALUE][$idx]); +// if ($value === false) { +// $value = ''; +// } +// } +// } else { $value = $formElement[FE_VALUE]; +// } + if ($value === '') { // Only take the default, if the FE is a real tablecolumn. See #2064 if ($this->store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) !== false) { @@ -461,6 +481,41 @@ abstract class AbstractBuildForm { return $html; } + /** + * Checks if LDAP search is requested. + * Yes: prepare configuration and fire the query. + * No: do nothing. + * + * @param array $formElement + * @return array + * @throws CodeException + * @throws UserFormException + */ + private function prepareFillStoreFireLdap(array $formElement) { + $config = array(); + + if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) { + $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES, F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH, F_TYPEAHEAD_LIMIT, + F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF, F_TYPEAHEAD_LDAP_KEY_PRINTF, F_LDAP_TIME_LIMIT]; + $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames); + } else { + return $formElement; // nothing to do. + } + + if (isset($formElement[FE_FILL_STORE_LDAP])) { + + // Extract necessary elements + $config = OnArray::getArrayItems($formElement, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]); + $config = $this->evaluate->parseArray($config); + + $ldap = new Ldap(); + $arr = $ldap->process($config, '', MODE_LDAP_SINGLE); + $this->store->setStore($arr, STORE_LDAP, true); + } + + return $formElement; + } + /** * Check if there is an explicit 'autofocus' definition in at least one FE. * Found: do nothing, it will be rendered at the correct position. @@ -727,6 +782,7 @@ abstract class AbstractBuildForm { $textarea = ''; $attribute = ''; $class = 'form-control'; + $elementCharacterCount = ''; $typeAheadUrlParam = $this->typeAheadBuildParam($formElement); if ($typeAheadUrlParam != '') { @@ -737,15 +793,43 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]); } + if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) { + $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>"; + + if ($formElement[FE_CHARACTER_COUNT_WRAP] != '') { + $arr = explode('|', $formElement[FE_CHARACTER_COUNT_WRAP], 2); + $arr[] = ''; + $arr[] = ''; //skip check that at least 2 elements exist + $elementCharacterCount = $arr[0] . $elementCharacterCount . $arr[1]; + } + } + $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]); $attribute .= Support::doAttribute('name', $htmlFormElementName); $attribute .= Support::doAttribute('class', $class); + if (isset($formElement[FE_RETYPE_SOURCE_NAME])) { $htmlFormElementNamePrimary = str_replace(RETYPE_FE_NAME_EXTENSION, '', $htmlFormElementName); $attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']'); } + $this->adjustMaxLength($formElement); + + if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') { + // crop string only if it's not empty (substr returns false on empty strings) + $value = substr($value, 0, $formElement[FE_MAX_LENGTH]); + } + // 'maxLength' needs an upper 'L': naming convention for DB tables! + if ($formElement[FE_MAX_LENGTH] > 0) { + $attribute .= Support::doAttribute('maxlength', $formElement[FE_MAX_LENGTH], false); + } + // Check for input type 'textarea'. $colsRows = explode(',', $formElement['size'], 2); if (count($colsRows) === 2) { @@ -759,15 +843,7 @@ abstract class AbstractBuildForm { } else { $htmlTag = '<input'; - $this->adjustMaxLength($formElement); - - if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') { - // crop string only if it's not empty (substr returns false on empty strings) - $value = substr($value, 0, $formElement[FE_MAX_LENGTH]); - } - - // 'maxLength' needs an upper 'L': naming convention for DB tables! - $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']); + $attribute .= $this->getAttributeList($formElement, ['type', 'size']); $attribute .= Support::doAttribute('value', htmlentities($value), false); } @@ -781,17 +857,18 @@ abstract class AbstractBuildForm { $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement); - return "$htmlTag $attribute>$textarea" . $this->getHelpBlock(); + return "$htmlTag $attribute>$textarea" . $elementCharacterCount . $this->getHelpBlock(); } /** * Check $formElement for FE_TYPE_AHEAD_SQL or FE_TYPE_AHEAD_LDAP_SERVER. - * If one of them is given: fill $urlParam. - * Set some parameter for later outside use, especially FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_MINLENGTH + * If one of them is given: build $urlParam with typeAhead Params. + * Additionally set some parameter for later outside use, especially FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_MINLENGTH * * @param array $formElement * @return string + * @throws UserFormException */ private function typeAheadBuildParam(array &$formElement) { @@ -803,16 +880,26 @@ abstract class AbstractBuildForm { if (isset($formElement[FE_TYPEAHEAD_SQL])) { $sql = $this->checkSqlAppendLimit($formElement[FE_TYPEAHEAD_SQL], $formElement[FE_TYPEAHEAD_LIMIT]); $urlParam = FE_TYPEAHEAD_SQL . '=' . $sql; - } elseif (isset($formElement[FE_TYPEAHEAD_LDAP_SERVER])) { - $formElement[FE_TYPEAHEAD_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SERVER); - $formElement[FE_TYPEAHEAD_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_BASE_DN); + } elseif (isset($formElement[FE_TYPEAHEAD_LDAP])) { + $formElement[FE_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_LDAP_SERVER); + $formElement[FE_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_LDAP_BASE_DN); $formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH); $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF); $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_KEY_PRINTF); + foreach ([FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_TYPEAHEAD_LDAP_SEARCH] as $key) { + if ($formElement[$key] == '') { + throw new UserFormException('Missing definition: ' . $key, ERROR_MISSING_DEFINITON); + } + } + + if ($formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] . $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] == '') { + throw new UserFormException('Missing definition: ' . FE_TYPEAHEAD_LDAP_VALUE_PRINTF . ' or ' . FE_TYPEAHEAD_LDAP_KEY_PRINTF, ERROR_MISSING_DEFINITON); + } + $arr = [ - FE_TYPEAHEAD_LDAP_SERVER => $formElement[FE_TYPEAHEAD_LDAP_SERVER], - FE_TYPEAHEAD_LDAP_BASE_DN => $formElement[FE_TYPEAHEAD_LDAP_BASE_DN], + FE_LDAP_SERVER => $formElement[FE_LDAP_SERVER], + FE_LDAP_BASE_DN => $formElement[FE_LDAP_BASE_DN], FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH], FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF], FE_TYPEAHEAD_LDAP_KEY_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF], @@ -820,6 +907,7 @@ abstract class AbstractBuildForm { ]; $urlParam = OnArray::toString($arr); + } return $urlParam; @@ -2694,7 +2782,8 @@ abstract class AbstractBuildForm { * @return mixed */ public function buildNote(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) { - return Support::wrapTag("<div class='qfq-note'>", $value); +// + return Support::wrapTag("<div class='" . CLASS_NOTE . "'>", $value); } /** @@ -2888,29 +2977,38 @@ EOT; } $default = $this->store->getStore(STORE_TABLE_DEFAULT); // current defaults + // evaluate FE_VALUE on all templateGroup FormElements. + $maxForeignRecords = $this->templateGroupDoValue(); + $lastFilled = 0; // Marker if there is at least one element per copy who is filled. $feSpecNativeCopy = array(); - for ($ii = 1; $ii < $max; $ii++) { + for ($ii = 1; $ii <= $max; $ii++) { - // Per copy, iterate over all templateGroup FormElements + // Per copy, iterate over all templateGroup FormElements. foreach ($this->feSpecNative as $fe) { $columnName = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_NAME]); $fe[FE_LABEL] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_LABEL]); $fe[FE_NOTE] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_NOTE]); + // Column of primary table? if (isset($record[$columnName])) { if ($record[$columnName] != $default[$columnName]) { - $lastFilled = $ii; + $lastFilled = max($ii, $lastFilled); } + $fe[FE_NAME] = $columnName; + } else { - throw new UserFormException("Not implemented: templateGroup FormElement-columns not found in primary table.", ERROR_NOT_IMPLEMENTED); + $lastFilled = max($maxForeignRecords, $lastFilled); + if (is_array($fe[FE_VALUE]) && isset($fe[FE_VALUE][$ii - 1])) { + $fe[FE_VALUE] = current($fe[FE_VALUE][$ii - 1]); // replace array with current value + } +// $fe[FE_TEMPLATE_GROUP_CURRENT_IDX] = $ii; } - $fe[FE_NAME] = $columnName; $feSpecNativeCopy[$ii - 1][] = $fe; // Build array with current copy of templateGroup. } - // Append $htmlDelete on the last element of all copies,, but not the first. + // Append $htmlDelete on the last element of all copies, but not the first. if ($ii > 1) { // Count defined FormElements in the current templateGroup $last = count($feSpecNativeCopy[$ii - 1]) - 1; @@ -2926,6 +3024,7 @@ EOT; $feSpecNativeSave = $this->feSpecNative; + $lastFilled = min($lastFilled, $max); // It's possible (external records) that there are more fetched values than the maximum allows - skip those. $html = ''; for ($ii = 0; $ii < $lastFilled; $ii++) { $this->feSpecNative = $feSpecNativeCopy[$ii]; @@ -2939,6 +3038,31 @@ EOT; return $html; } + /** + * Evaluate for all FormElements of the current templateGroup the field FE_VALUE. + * If the specific FormElement is not a real column of the primary table, than the value is probably a '{{!SELECT ...' + * Statement, that one will be fired. Additional the maximum count of all select rows will be determined and returned. + * + * @return int max number of records in FormElement[FE_VALUE] over all FormElements. + * @throws UserFormException + */ + private function templateGroupDoValue() { + + // Fire 'value' statement + $tgMax = 0; + foreach ($this->feSpecNative as $key => $arr) { + $this->feSpecNative[$key][FE_VALUE] = $this->evaluate->parse($arr[FE_VALUE]); + + if (is_array($this->feSpecNative[$key][FE_VALUE])) { + $cnt = count($this->feSpecNative[$key][FE_VALUE]); + + $tgMax = max($cnt, $tgMax); + } + } + + return $tgMax; + } + abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName); } \ No newline at end of file diff --git a/extension/qfq/qfq/BuildFormBootstrap.php b/extension/qfq/qfq/BuildFormBootstrap.php index 5455a18122c23073166cc619bc9d2139f62afcbe..8dcfecbe915d8827dc3e6c6e36af7c909e04f18e 100644 --- a/extension/qfq/qfq/BuildFormBootstrap.php +++ b/extension/qfq/qfq/BuildFormBootstrap.php @@ -53,7 +53,7 @@ class BuildFormBootstrap extends AbstractBuildForm { $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_END] = "</h1></div></div>"; // Element: Label + Input + Note - $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_START] = "<div class='form-group'>"; + $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_START] = "<div class='form-group clearfix'>"; $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_END] = "</div>"; $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_START] = "<div class='col-md-12'>"; @@ -329,6 +329,11 @@ class BuildFormBootstrap extends AbstractBuildForm { // Button Save at bottom of form - only if there is a button text given. if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') { + + + // Default setzen: + $this->fillWrapLabelInputNote($this->formSpec[F_BS_LABEL_COLUMNS], $this->formSpec[F_BS_INPUT_COLUMNS], $this->formSpec[F_BS_NOTE_COLUMNS]); + $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT]; $htmlElement = $this->buildButtonCode('save-button', $buttonText, '', '', $this->formSpec[F_BUTTON_ON_CHANGE_CLASS]); @@ -429,7 +434,6 @@ EOF; [$this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT); // Note -// $note = Support::wrapTag("<div class='qfq-note'>", $formElement[FE_NOTE], true); $note = $formElement[FE_NOTE]; $html .= $this->customWrap($formElement, $note, FE_WRAP_NOTE, $formElement[FE_BS_NOTE_COLUMNS], [$this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START], $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END]], $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE); diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 0fba784aa5251769243b4466f995c02b4f231554..6b959577c7493d46e3c7d239f679c9487f43dde9 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -50,6 +50,7 @@ const SQL_FORM_ELEMENT_SPECIFIC_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' F const SQL_FORM_ELEMENT_ALL_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER = "SELECT fe.id, fe.feIdContainer, fe.name, fe.label, fe.type, fe.checkType, fe.checkPattern, fe.mode, fe.modeSql, fe.parameter, fe.dynamicUpdate FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_CONTAINER_TEMPLATE_GROUP = "SELECT fe.id, fe.name, fe.label, fe.maxLength, fe.parameter FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'container' AND fe.type='templateGroup' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; +const SQL_FORM_ELEMENT_TEMPLATE_GROUP = "SELECT * FROM FormElement AS fe WHERE fe.id = ? AND fe.deleted = 'no' AND fe.class = 'container' AND fe.type='templateGroup' AND fe.enabled='yes' "; // SANITIZE Classifier const SANITIZE_ALLOW_ALNUMX = "alnumx"; @@ -163,7 +164,7 @@ const ERROR_MISSING_SLAVE_ID_DEFINITION = 1074; const ERROR_MISSING_INTL = 1075; const ERROR_HTML_TOKEN_TOO_SHORT = 1076; const ERROR_MISSING_PRINTF_ARGUMENTS = 1077; - +const ERROR_MISSING_DEFINITON = 1078; // Subrecord const ERROR_SUBRECORD_MISSING_COLUMN_ID = 1100; @@ -242,6 +243,7 @@ const STORE_EMPTY = "E"; // value: '', might helpfull if variable is not defined const STORE_SYSTEM = "Y"; // various system values like db connection credentials const STORE_EXTRA = 'X'; // Persistent Store: contains arrays! Not Usefull for user. Used by system. const STORE_ADDITIONAL_FORM_ELEMENTS = 'A'; // Internal Store to collect FormElements. Typically for 'hidden' elements of radio and checkbox. Helps render those elements at the end of the whole form rendering. +const STORE_LDAP = 'L'; const STORE_USE_DEFAULT = "FSRVD"; @@ -381,17 +383,28 @@ const VAR_FILENAME = 'filename'; // Original filename of an uploaded file. const MODE_DB_REGULAR = 'regular'; const MODE_DB_NO_LOG = 'noLog'; -// PHPO class Typeahead +// PHP class Typeahead const TYPEAHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=... - See also FE_TYPE_AHEAD_SQL const TYPEAHEAD_API_SIP = 'sip'; // Name of parameter in API call of typeahead.php?query=...&s=... const TYPEAHEAD_DEFAULT_LIMIT = 20; +const DEFAULT_LDAP_TIME_LIMIT = 3; + const SINGLE_TICK = "'"; const DOUBLE_TICK = '"'; // TOKEN evaluate const TOKEN_ESCAPE_SINGLE_TICK = 's'; const TOKEN_ESCAPE_DOUBLE_TICK = 'd'; +const TOKEN_LDAP_ESCAPE_FILTER = 'l'; +const TOKEN_LDAP_ESCAPE_DN = 'L'; + +// Workaround for PHP < 5.6.0 +if (!function_exists('ldap_escape')) { + define('LDAP_ESCAPE_FILTER', 0x01); + define('LDAP_ESCAPE_DN', 0x02); +} + const TOKEN_FOUND_IN_STORE_QUERY = 'query'; const RANDOM_LENGTH = 32; @@ -427,6 +440,9 @@ const SQL_LOG_MODE_ALL = 'all'; const SQL_LOG_MODE_MODIFY = 'modify'; const SQL_LOG_MODE_ERROR = 'error'; // write log entry, independent of global setting (e.g. broken Query) +const MODE_LDAP_SINGLE = 'ldapSingle'; +const MODE_LDAP_MULTI = 'ldapMulti'; + // api/save.php, api/delete.php, api/load.php const API_DELETE_PHP = 'delete.php'; @@ -460,12 +476,18 @@ const DATA_REQUIRED = 'data-required'; const CLASS_TYPEAHEAD = 'qfq-typeahead'; const DATA_TYPEAHEAD_SIP = 'data-typeahead-sip'; // Used for typeAhead + +const CLASS_NOTE = 'qfq-note'; + //const CLASS_TYPEAHEAD = 'qfq-type-ahead'; //const DATA_TYPEAHEAD_SIP = 'data-sip'; // Used for typeAhead const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit'; const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength'; +const CLASS_CHARACTER_COUNT = 'qfq-character-count'; +const DATA_CHARACTER_COUNT_ID = 'data-character-count-id'; + // BuildForm const SYMBOL_NEW = 'new'; const SYMBOL_EDIT = 'edit'; @@ -525,6 +547,16 @@ const F_FE_DATA_ERROR = 'data-error'; const F_PARAMETER = 'parameter'; // valid for F_ and FE_ +const F_LDAP_SERVER = 'ldapServer'; +const F_LDAP_BASE_DN = 'ldapBaseDn'; +const F_LDAP_SEARCH = 'ldapSearch'; +const F_LDAP_ATTRIBUTES = 'ldapAttributes'; +const F_LDAP_TIME_LIMIT = 'ldapTimeLimit'; +const F_TYPEAHEAD_LIMIT = 'typeAheadLimit'; +const F_TYPEAHEAD_MINLENGTH = 'typeAheadMinLength'; +const F_TYPEAHEAD_LDAP_VALUE_PRINTF = 'typeAheadLdapValuePrintf'; +const F_TYPEAHEAD_LDAP_KEY_PRINTF = 'typeAheadLdapKeyPrintf'; +const F_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch'; // FORM_ELEMENT_STATI const FE_MODE_SHOW = 'show'; @@ -604,15 +636,22 @@ const FE_TEMPLATE_GROUP_REMOVE_TEXT = 'tgRemoveText'; const FE_TEMPLATE_GROUP_CLASS = 'tgClass'; const FE_TEMPLATE_GROUP_DEFAULT_MAX_LENGTH = 5; const FE_TEMPLATE_GROUP_NAME_PATTERN = '%d'; +const FE_TEMPLATE_GROUP_CURRENT_IDX = 'tgCurentIndex'; const FE_BUTTON_CLASS = 'buttonClass'; -const FE_TYPEAHEAD_LIMIT = 'typeaheadLimit'; -const FE_TYPEAHEAD_MINLENGTH = 'typeaheadMinLength'; +const FE_LDAP_SERVER = F_LDAP_SERVER; +const FE_LDAP_BASE_DN = F_LDAP_BASE_DN; +const FE_LDAP_SEARCH = F_LDAP_SEARCH; +const FE_LDAP_ATTRIBUTES = F_LDAP_ATTRIBUTES; +const FE_LDAP_TIME_LIMIT = F_LDAP_TIME_LIMIT; +const FE_TYPEAHEAD_LIMIT = F_TYPEAHEAD_LIMIT; +const FE_TYPEAHEAD_MINLENGTH = F_TYPEAHEAD_MINLENGTH; const FE_TYPEAHEAD_SQL = 'typeAheadSql'; -const FE_TYPEAHEAD_LDAP_SERVER = 'typeAheadLdapServer'; -const FE_TYPEAHEAD_LDAP_BASE_DN = 'typeAheadLdapBaseDn'; -const FE_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch'; -const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = 'typeAheadLdapValuePrintf'; -const FE_TYPEAHEAD_LDAP_KEY_PRINTF = 'typeAheadLdapKeyPrintf'; +const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = F_TYPEAHEAD_LDAP_VALUE_PRINTF; +const FE_TYPEAHEAD_LDAP_KEY_PRINTF = F_TYPEAHEAD_LDAP_KEY_PRINTF; +const FE_TYPEAHEAD_LDAP = 'typeAheadLdap'; +const FE_TYPEAHEAD_LDAP_SEARCH = F_TYPEAHEAD_LDAP_SEARCH; +const FE_FILL_STORE_LDAP = 'fillStoreLdap'; +const FE_CHARACTER_COUNT_WRAP = 'characterCountWrap'; const RETYPE_FE_NAME_EXTENSION = 'RETYPE'; const FE_HTML_ID = 'htmlId'; // Will be dynamically computed during runtime. @@ -647,6 +686,7 @@ const HTML_ID_EXTENSION_INPUT = '-i'; const HTML_ID_EXTENSION_NOTE = '-n'; const HTML_ID_EXTENSION_TOOLTIP = '-t'; const HTML_ID_EXTENSION_ROW = '-r'; +const HTML_ID_EXTENSION_CHARACTER_COUNT = '-cc'; const QUERY_TYPE_SELECT = 'type: select,show,describe,explain'; const QUERY_TYPE_INSERT = 'type: insert'; diff --git a/extension/qfq/qfq/Evaluate.php b/extension/qfq/qfq/Evaluate.php index 2a3097e1702122b26c72e23c88980a547d0a9883..27be2711d087d1aae41518daeb5f15d0522ebed8 100644 --- a/extension/qfq/qfq/Evaluate.php +++ b/extension/qfq/qfq/Evaluate.php @@ -13,6 +13,7 @@ use qfq\Store; require_once(__DIR__ . '/../qfq/store/Store.php'); require_once(__DIR__ . '/../qfq/Database.php'); +require_once(__DIR__ . '/helper/Support.php'); /** * Class Evaluate @@ -70,7 +71,7 @@ class Evaluate { $value = trim($value); // Skip comments. - if(substr($value,0,1)!='#') { + if (substr($value, 0, 1) != '#') { $arr[$key] = $this->parse($value, 0, $debugStack); } } @@ -206,15 +207,25 @@ class Evaluate { // escape ticks if (is_string($value)) { - switch ($arr[3]) { - case TOKEN_ESCAPE_SINGLE_TICK: - $value = str_replace("'", "\\'", $value); - break; - case TOKEN_ESCAPE_DOUBLE_TICK: - $value = str_replace('"', '\\"', $value); - break; - default: - break; + // Process all escape requests in the given order. + for ($ii = 0; $ii < strlen($arr[3]); $ii++) { + $escape = $arr[3][$ii]; + switch ($escape) { + case TOKEN_ESCAPE_SINGLE_TICK: + $value = str_replace("'", "\\'", $value); + break; + case TOKEN_ESCAPE_DOUBLE_TICK: + $value = str_replace('"', '\\"', $value); + break; + case TOKEN_LDAP_ESCAPE_FILTER: + $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER); + break; + case TOKEN_LDAP_ESCAPE_DN: + $value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN); + break; + default: + break; + } } } diff --git a/extension/qfq/qfq/form/FormAction.php b/extension/qfq/qfq/form/FormAction.php index 774a8a4d56e11c03dc26ce03d0ef6795b03283a8..8b66072a8554dffdf9761ad89ed3d67aab2bddac 100644 --- a/extension/qfq/qfq/form/FormAction.php +++ b/extension/qfq/qfq/form/FormAction.php @@ -65,13 +65,16 @@ class FormAction { * @throws DbException * @throws UserFormException */ - public function elements($recordId, array $feSpecAction, $feTypeList) { + public function elements($recordId, array $feSpecAction, $feTypeList, $templateGroupIndex = 0) { $flagModified = false; // Iterate over all Action FormElements foreach ($feSpecAction as $fe) { + // Preparation for Log, Debug + $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); + $fe = HelperFormElement::initActionFormElement($fe); // Only process FE elements of types listed in $feTypeList. Skip all other @@ -79,8 +82,25 @@ class FormAction { continue; } - // Preparation for Log, Debug - $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); + // Process templateGroup action elements + if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) { + + $feTemplateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP, ROW_REGULAR, [$fe[FE_ID_CONTAINER]]); + + if (count($feTemplateGroup) == 1) { + $fe[FE_ID_CONTAINER] = 0; + for ($ii = 1; $ii < $feTemplateGroup[0][FE_MAX_LENGTH]; $ii++) { + $feNew = OnArray::arrayValueReplace($fe, FE_TEMPLATE_GROUP_NAME_PATTERN, $ii); + if ($this->elements($recordId, [$feNew], $feTypeList, $ii)) { + $flagModified = true; + } + } + } else { + // At the moment 'action' elements have to point to a templateGroup - nothing else is defined. Break if there is somethin else + throw new UserFormException("Expect a 'templateGroup' record in FormElement.id=", $fe[FE_ID_CONTAINER], ERROR_RECORD_NOT_FOUND); + } + continue; // skip to next FormElement + } switch ($fe[FE_TYPE]) { case FE_TYPE_BEFORE_LOAD: @@ -96,6 +116,19 @@ class FormAction { continue; } + if (isset($fe[FE_FILL_STORE_LDAP])) { + $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES, F_LDAP_SEARCH, F_LDAP_TIME_LIMIT]; + $fe = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $fe, $keyNames); + + // Extract necessary elements + $config = OnArray::getArrayItems($fe, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]); + $config = $this->evaluate->parseArray($config); + + $ldap = new Ldap(); + $arr = $ldap->process($config, '', MODE_LDAP_SINGLE); + $this->store->setStore($arr, STORE_LDAP, true); + } + if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) { $this->sendMail($fe); //no further processing of current element necessary. @@ -104,7 +137,7 @@ class FormAction { $this->validate($fe); - $this->doSlave($fe, $recordId); + $this->doSlave($fe, $recordId, $templateGroupIndex); $flagModified = true; } @@ -228,14 +261,27 @@ class FormAction { * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or `sqlInsert` * * @param array $fe + * @param int $recordId + * @param int $templateGroupIndex * @return int * @throws CodeException + * @throws DbException * @throws UserFormException */ - private function doSlave(array $fe, $recordId) { + private function doSlave(array $fe, $recordId, $templateGroupIndex) { // Get the slaveId $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]); + if ($templateGroupIndex > 0) { + if (is_array($slaveId)) { + // Select the n'th id of the array + $slaveId = isset($slaveId[$templateGroupIndex]) ? $slaveId[$templateGroupIndex] : 0; + } else { + throw new UserFormException("Result not an arry. SQL Statement for 'slaveId' in TemplateGroup should return an array with all 'slaveId's.", ERROR_EXPECTED_ARRAY); + } + } elseif (is_array($slaveId)) { + throw new UserFormException("Result not a single value. SQL Statement for 'slaveId' in should a single value.", ERROR_UNEXPECTED_TYPE); + } if ($slaveId === '' && $fe[FE_NAME] !== '') { // if the current action element has the same name as a real master record column: take that value as an id diff --git a/extension/qfq/qfq/form/TypeAhead.php b/extension/qfq/qfq/form/TypeAhead.php index 87545b6cab77607f3235ae870973ea98bae844bd..fbfc280bc7da6a93d0815dcd131b67c73389b7c4 100644 --- a/extension/qfq/qfq/form/TypeAhead.php +++ b/extension/qfq/qfq/form/TypeAhead.php @@ -80,7 +80,7 @@ class TypeAhead { if (isset($sipVars[FE_TYPEAHEAD_SQL])) { $arr = $this->typeAheadSql($sipVars, $this->vars[TYPEAHEAD_API_QUERY]); - } elseif (isset($sipVars[FE_TYPEAHEAD_LDAP_SERVER])) { + } elseif (isset($sipVars[FE_LDAP_SERVER])) { $ldap = new Ldap(); $arr = $ldap->process($sipVars, $this->vars[TYPEAHEAD_API_QUERY]); } diff --git a/extension/qfq/qfq/helper/Ldap.php b/extension/qfq/qfq/helper/Ldap.php index ced53b734c2b9110dcce909fe083281469c01a2c..ffbf3a716e50c52b91421fc814845e4cf8cc09a8 100644 --- a/extension/qfq/qfq/helper/Ldap.php +++ b/extension/qfq/qfq/helper/Ldap.php @@ -16,51 +16,128 @@ require_once(__DIR__ . '/OnArray.php'); class Ldap { /** - * @param $query - * @return array + * @param $ldapServer + * @return resource * @throws UserFormException */ - public function process($config, $query) { - $arr = array(); - - $ldapServer = $config[FE_TYPEAHEAD_LDAP_SERVER]; - $ldapBaseDn = $config[FE_TYPEAHEAD_LDAP_BASE_DN]; - $ldapSearch = $config[FE_TYPEAHEAD_LDAP_SEARCH]; - $ldapSearch = str_replace('?', $query, $ldapSearch); - $ldapLimit = $config[FE_TYPEAHEAD_LIMIT]; - + private function ldapConnect($ldapServer) { $ds = ldap_connect($ldapServer); // must be a valid LDAP server! if (!$ds) { throw new UserFormException("Unable to connect to LDAP server: $ldapServer", ERROR_LDAP_CONNECT); } + return $ds; + } - $keyArr = $this->printfPrepare($config[FE_TYPEAHEAD_LDAP_KEY_PRINTF], $keyFormat); - $valueArr = $this->printfPrepare($config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF], $valueFormat); - - $attr = array_values(array_unique(array_merge($keyArr, $valueArr))); - + /** + * @param $ds + * @param array $config + * @param array $attr + * @return resource + */ + private function ldapSearch($ds, array $config, array $attr) { // 'Size Limit errors' are reported, even if it is not a real problem. // Fake all errors at the moment. // TODO: just drop the 'Size Limit errors' and report all others set_error_handler(function () { /* ignore errors */ }); - $sr = ldap_search($ds, $ldapBaseDn, $ldapSearch, $attr, 0, $ldapLimit); + + $sr = ldap_search($ds, $config[FE_LDAP_BASE_DN], $config[FE_LDAP_SEARCH], $attr, 0, $config[FE_TYPEAHEAD_LIMIT] + 1, $config[FE_LDAP_TIME_LIMIT]); restore_error_handler(); - $info = ldap_get_entries($ds, $sr); + return $sr; + } - for ($i = 0; $i < $info["count"]; $i++) { + /** + * @param array $config + * @param string $searchValue + * @param string $mode + * @return array + * @throws UserFormException + */ + private function prepareConfig(array $config, $searchValue, $mode) { + + $config[FE_LDAP_ATTRIBUTES] = Support::setIfNotSet($config, FE_LDAP_ATTRIBUTES, ''); + $config[FE_LDAP_TIME_LIMIT] = Support::setIfNotSet($config, FE_LDAP_TIME_LIMIT, DEFAULT_LDAP_TIME_LIMIT); + + + $config[FE_LDAP_SEARCH] = str_replace('?', $searchValue, $config[FE_LDAP_SEARCH]); - $key = $this->printfResult($keyFormat, $keyArr, $info[$i]); - $value = $this->printfResult($valueFormat, $valueArr, $info[$i]); + $config[FE_TYPEAHEAD_LIMIT] = ($mode == MODE_LDAP_MULTI) ? $config[FE_TYPEAHEAD_LIMIT] : 1; - if ($key == '' || $value == '') { - continue; // if $key or $value is empty: skip + if ($mode == MODE_LDAP_MULTI) { + $config[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($config, FE_TYPEAHEAD_LDAP_KEY_PRINTF, ''); + $config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($config, FE_TYPEAHEAD_LDAP_VALUE_PRINTF, ''); + + if ($config[FE_TYPEAHEAD_LDAP_KEY_PRINTF] == '') { + $config[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = $config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF]; + } + + if ($config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] == '') { + $config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = $config[FE_TYPEAHEAD_LDAP_KEY_PRINTF]; } - $arr[] = [API_TYPEAHEAD_KEY => $key, API_TYPEAHEAD_VALUE => $value]; + if ($mode == MODE_LDAP_MULTI && $config[FE_TYPEAHEAD_LDAP_KEY_PRINTF] == '') { + throw new UserFormException("Missing parameter '" . FE_TYPEAHEAD_LDAP_KEY_PRINTF . "' and/or '" . FE_TYPEAHEAD_LDAP_VALUE_PRINTF); + } + } + + return $config; + } + + /** + * @param array $config [FE_LDAP_SERVER , FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_LDAP_KEY_PRINTF, FE_TYPEAHEAD_LDAP_VALUE_PRINTF] + * @param string $searchValue value to search via $config[FE_LDAP_SEARCH] + * @param string $mode MODE_LDAP_SINGLE | MODE_LDAP_MULTI + * @return array Array: [ [ 'key' => '...', 'value' => '...' ], ] + * @throws UserFormException + */ + public function process(array $config, $searchValue, $mode = MODE_LDAP_MULTI) { + $arr = array(); + + // For TypeAhead, use an optional given F_TYPEAHEAD_LDAP_SEARCH + if ($mode == MODE_LDAP_MULTI && $config[F_TYPEAHEAD_LDAP_SEARCH] != '') { + $config[F_LDAP_SEARCH] = $config[F_TYPEAHEAD_LDAP_SEARCH]; } + $searchValue = Support::ldap_escape($searchValue, null, LDAP_ESCAPE_FILTER); + $config = $this->prepareConfig($config, $searchValue, $mode); + + $ds = $this->ldapConnect($config[FE_LDAP_SERVER]); // must be a valid LDAP server! + + $keyArr = $this->preparePrintf($config, FE_TYPEAHEAD_LDAP_KEY_PRINTF, $keyFormat); + $valueArr = $this->preparePrintf($config, FE_TYPEAHEAD_LDAP_VALUE_PRINTF, $valueFormat); + $specificArr = OnArray::arrayValueToLower(OnArray::trimArray(explode(',', $config[FE_LDAP_ATTRIBUTES]))); + + // merge, trim, toLower, unique, values + $attr = array_values( + array_unique( + OnArray::removeEmptyElementsFromArray( + array_merge($keyArr, $valueArr, $specificArr)))); + + $sr = $this->ldapSearch($ds, $config, $attr); + $info = ldap_get_entries($ds, $sr); + + if ($mode == MODE_LDAP_MULTI) { + + // Iterate over all Elements, per element collect all needed attributes + for ($i = 0; $i < $info["count"]; $i++) { + + $key = $this->printfResult($keyFormat, $keyArr, $info[$i]); + $value = $this->printfResult($valueFormat, $valueArr, $info[$i]); + + if ($key == '' || $value == '') { + continue; // if $key or $value is empty: skip + } + + $arr[] = [API_TYPEAHEAD_KEY => $key, API_TYPEAHEAD_VALUE => $value]; + } + } else { + // Collect all attributes + foreach ($attr as $key) { + $value = isset($info[0][$key][0]) ? $info[0][$key][0] : ''; + $arr[$key] = htmlentities($value); + } + } ldap_close($ds); return $arr; @@ -69,14 +146,22 @@ class Ldap { /** * Very specific function to prepare the later 'printfResult()'. * - * @param $fmtComplete - * @param $fmtFirst - * @return mixed + * @param array $config Check existence and take element $key + * @param string $key FE_TYPEAHEAD_LDAP_KEY_PRINTF, FE_TYPEAHEAD_LDAP_VALUE_PRINTF + * @param string $fmtFirst Returns the first part of $fmtComplete - the printf format string without any args. + * @return array Array with all requested keynames from $fmtComplete * @throws CodeException * @throws UserFormException */ - private function printfPrepare($fmtComplete, &$fmtFirst) { + private function preparePrintf(array $config, $key, &$fmtFirst) { + + $fmtFirst = ''; + + if (!isset($config[$key])) { + return array(); + } + $fmtComplete = $config[$key]; // Typical $fmtComplete: "'%s / %s / %s', cn, mail. telephonenumber" $arr = KeyValueStringParser::explodeWrapped(',', $fmtComplete); @@ -89,11 +174,8 @@ class Ldap { array_shift($arr); // remove first entry: - $arr = OnArray::trimArray($arr); // remove any leading/trailing spaces - - // toLower is important, cause the LDAP attribute names are all lowercase in PHP - if the user specifies in CamelHook , the vars are not found. - return OnArray::arrayValueToLower($arr); - + // toLower & trim are mandatory here: access to LDAP entries are comming soon. + return OnArray::arrayValueToLower(OnArray::trimArray($arr)); } /** @@ -111,7 +193,7 @@ class Ldap { $args = array($format); foreach ($keyArr as $key) { - $args[] = (isset($infoElement[$key][0])) ? $infoElement[$key][0] : ''; + $args[] = (isset($infoElement[$key][0])) ? htmlentities($infoElement[$key][0]) : ''; } return call_user_func_array('sprintf', $args); diff --git a/extension/qfq/qfq/helper/OnArray.php b/extension/qfq/qfq/helper/OnArray.php index 7e2806b920b6df2c01fba8e074917b228f76fd00..b60e6fe32a6276d1ab9d8d05ed4520a853a82c1a 100644 --- a/extension/qfq/qfq/helper/OnArray.php +++ b/extension/qfq/qfq/helper/OnArray.php @@ -140,7 +140,7 @@ class OnArray { } /** - * Iterates over $arr and removes all empty (='') elements. + * Iterates over $arr and removes all empty (='') elements. Preserves keys * * @param array $arr * @return array @@ -169,6 +169,11 @@ class OnArray { return $new; } + /** + * Convert all values of an array to lowercase. + * @param array $arr + * @return array + */ public static function arrayValueToLower(array $arr) { $new = array(); @@ -177,4 +182,67 @@ class OnArray { } return $new; } + + /** + * Search in array $dest for all $keyNames if they exist. If not, check if they exist in $src. If yes, copy. + * + * @param array $src + * @param array $dest + * @param array $keyNames + * @return array $dest filled with new values + */ + public static function copyArrayItemsIfNotAlreadyExist(array $src, array $dest, array $keyNames) { + + foreach ($keyNames as $key) { + + if (!isset($dest[$key])) { + if (isset($src[$key])) { + $dest[$key] = $src[$key]; + } + } + } + + return $dest; + } + + /** + * Copies all items whose $keyNames are listed to $new and return $new + * + * @param array $src + * @param array $keyNames + * @param bool $createMissing + * @return array + */ + public static function getArrayItems(array $src, array $keyNames, $createMissing = false) { + $new = array(); + + // Extract necessary elements + foreach ($keyNames as $key) { + if (isset($src[$key])) { + $new[$key] = $src[$key]; + } elseif ($createMissing == true) { + $new[$key] = ''; + } + } + + return $new; + } + + /** + * Iterates over an array and replaces all occurences of $search with $replace. Returns the new array. + * + * @param array $src + * @param $search + * @param $replace + * @return array + */ + public static function arrayValueReplace(array $src, $search, $replace ) { + $new = array(); + + foreach($src AS $key => $element) { + $new[$key] = str_replace($search, $replace, $element); + } + + return $new; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index 1fc32d719d2c8725828dc71f385da52c417c36f8..1316b06e74c10ca0cc418e6da4a42d9b5cf036ca 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -123,7 +123,7 @@ class Support { switch (strtolower($type)) { case 'size': case 'maxlength': - // empty or '0' for attributes of type 'size' or 'maxlength' result in unsuable input elements: skip this. + // empty or '0' for attributes of type 'size' or 'maxlength' result in unsuable input elements: skip this. if ($value === '' || $value == 0) { return ''; } @@ -703,4 +703,80 @@ class Support { public static function falseEmptyToZero($val) { return ($val == '' || $val == false) ? '0' : $val; } + + /** + * TODO: as soon as we don't support PHP 5.6.0 anymore, this local implemention can be removed. + * Workaround for PHP < 5.6.0: there is no ldap_escape() - use this code instead. + * + * @param string $subject The subject string + * @param string $ignore Set of characters to leave untouched + * @param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the + * set(s) of characters to escape. + * @return string + **/ + public static function ldap_escape($subject, $ignore = '', $flags = 0) { + + if (function_exists('ldap_escape')) { + + return ldap_escape($subject, $ignore, $flags); + + } else { + +// define('LDAP_ESCAPE_FILTER', 0x01); +// define('LDAP_ESCAPE_DN', 0x02); + + static $charMaps = array( + LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"), + LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'), + ); + // Pre-process the char maps on first call + if (!isset($charMaps[0])) { + $charMaps[0] = array(); + for ($i = 0; $i < 256; $i++) { + $charMaps[0][chr($i)] = sprintf('\\%02x', $i);; + } + for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) { + $chr = $charMaps[LDAP_ESCAPE_FILTER][$i]; + unset($charMaps[LDAP_ESCAPE_FILTER][$i]); + $charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr]; + } + for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) { + $chr = $charMaps[LDAP_ESCAPE_DN][$i]; + unset($charMaps[LDAP_ESCAPE_DN][$i]); + $charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr]; + } + } + // Create the base char map to escape + $flags = (int)$flags; + $charMap = array(); + if ($flags & LDAP_ESCAPE_FILTER) { + $charMap += $charMaps[LDAP_ESCAPE_FILTER]; + } + if ($flags & LDAP_ESCAPE_DN) { + $charMap += $charMaps[LDAP_ESCAPE_DN]; + } + if (!$charMap) { + $charMap = $charMaps[0]; + } + // Remove any chars to ignore from the list + $ignore = (string)$ignore; + for ($i = 0, $l = strlen($ignore); $i < $l; $i++) { + unset($charMap[$ignore[$i]]); + } + // Do the main replacement + $result = strtr($subject, $charMap); + // Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed + if ($flags & LDAP_ESCAPE_DN) { + if ($result[0] === ' ') { + $result = '\\20' . substr($result, 1); + } + if ($result[strlen($result) - 1] === ' ') { + $result = substr($result, 0, -1) . '\\20'; + } + } + + return $result; + } + } + } \ No newline at end of file diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index ea95f84f3f5b0faa6ae2310a6bf11d83540f3beb..c4bee36d3ad5a4cb9c01ee36ff4f464852be76dd 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -165,6 +165,7 @@ class Store { STORE_EMPTY => false, STORE_SYSTEM => false, STORE_EXTRA => false, + STORE_LDAP => false, STORE_ADDITIONAL_FORM_ELEMENTS => false ]; @@ -444,19 +445,24 @@ class Store { } while ($useStores !== false) { - $store = substr($useStores, 0, 1); // next store + + $finalKey = $key; + if ($store == STORE_LDAP) { + $finalKey = strtolower($key); // in STORE_LDAP all keys are lowercase + } + $foundInStore = $store; $useStores = substr($useStores, 1); // shift left remaining stores - if (!isset(self::$raw[$store][$key])) { + if (!isset(self::$raw[$store][$finalKey])) { switch ($store) { case STORE_ZERO: return 0; case STORE_EMPTY: return ''; case STORE_VAR: - if ($key === VAR_RANDOM) { + if ($finalKey === VAR_RANDOM) { return Support::randomAlphaNum(RANDOM_LENGTH); } else { continue 2; // no value provided, continue with while loop @@ -468,7 +474,7 @@ class Store { } } - $rawVal = isset(self::$raw[$store][$key]) ? self::$raw[$store][$key] : null; + $rawVal = isset(self::$raw[$store][$finalKey]) ? self::$raw[$store][$finalKey] : null; if (self::$sanitizeStore[$store] && $sanitizeClass != '') { if ($sanitizeClass == SANITIZE_ALLOW_PATTERN || $sanitizeClass == SANITIZE_ALLOW_MIN_MAX || $sanitizeClass == SANITIZE_ALLOW_MIN_MAX_DATE) { // We do not have any pattern or min|max values at this point. For those who be affected, they already checked earlier. So set 'no check' diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index 3269b01c30a02629d5fa79149c0019472b169491..c0bda726f94bc04cd897aa6fc296728fec32fa99 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -136,37 +136,37 @@ INSERT INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEdi 'Form', 'sip', 'sip', 'bootstrap', '', 'maxVisiblePill=5\nclass=container-fluid'); # FormEditor: FormElements for 'form' -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, +INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, note, clientJs, value, sql1, parameter, feIdContainer, subrecordOption, modeSql, placeholder) VALUES - (1, 1, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', 0, '', '', ''), - (2, 1, 'access', 'Access', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', 0, '', '', ''), - (3, 1, 'various', 'Various', 'show', 'pill', 'all', 'container', 30, 0, 0, '', '', '', '', '', 0, '', '', ''), - (4, 1, 'multi', 'Multi', 'show', 'pill', 'all', 'container', 40, 0, 0, '', '', '', '', '', 0, '', '', ''), - (5, 1, 'formelement', 'Formelement', 'show', 'pill', 'all', 'container', 50, 0, 0, '', '', '', '', '', 0, '', '', ''); + (1, 1, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, '', '', '', '', '', 0, '', '', ''), + (2, 1, 'access', 'Access', 'show', 'pill', 'all', 'container', 20, 0, '', '', '', '', '', 0, '', '', ''), + (3, 1, 'various', 'Various', 'show', 'pill', 'all', 'container', 30, 0, '', '', '', '', '', 0, '', '', ''), + (4, 1, 'multi', 'Multi', 'show', 'pill', 'all', 'container', 40, 0, '', '', '', '', '', 0, '', '', ''), + (5, 1, 'formelement', 'Formelement', 'show', 'pill', 'all', 'container', 50, 0, '', '', '', '', '', 0, '', '', ''); # FormEditor: FormElements for 'form' INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, parameter, feIdContainer, subrecordOption, modeSql, placeholder) VALUES (1, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', 1, '', '', ''), - (1, 'name', 'Name', 'required', 'text', 'alnumx', 'native', 120, 0, 255, '', '', '', '', 'autofocus', 1, '', '', ''), - (1, 'title', 'Title', 'show', 'text', 'all', 'native', 130, 0, 255, '', '', '', '', '', 1, '', '', ''), + (1, 'name', 'Name', 'required', 'text', 'alnumx', 'native', 120, 0, 0, '', '', '', '', 'autofocus', 1, '', '', ''), + (1, 'title', 'Title', 'show', 'text', 'all', 'native', 130, 0, 0, '', '', '', '', '', 1, '', '', ''), (1, 'noteInternal', 'Note', 'show', 'text', 'all', 'native', 140, '40,3', 0, '', '', '', '', '', 1, '', '', ''), (1, 'tableName', 'Table', 'required', 'select', 'all', 'native', 150, 0, 0, '', '', '', '{{!SHOW tables}}', 'emptyItemAtStart', 1, '', '', ''), - (1, 'requiredParameter', 'Required Parameter', 'show', 'text', 'all', 'native', 200, 0, 255, '', '', '', '', '', 2, '', '', ''), + (1, 'requiredParameter', 'Required Parameter', 'show', 'text', 'all', 'native', 200, 0, 0, '', '', '', '', '', 2, '', '', ''), (1, 'permitNew', 'Permit New', 'show', 'radio', 'all', 'native', 210, 0, 10, '', '', '', '', '', 2, '', '', ''), (1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'all', 'native', 220, 0, 10, '', '', '', '', '', 2, '', '', ''), (1, 'render', 'Render', 'show', 'radio', 'all', 'native', 230, 0, 3, '', '', '', '', '', 2, '', '', ''), (1, 'showButton', 'Show button', 'show', 'checkbox', 'all', 'native', 240, 0, 5, '', '', '', '', 'checkBoxMode = multi\norientation=vertical', 2, '', '', ''), (1, 'forwardMode', 'Forward', 'show', 'radio', 'all', 'native', 300, 0, 0, '', '', '', '', '', 3, '', '', ''), - (1, 'forwardPage', 'Forward Page', 'show', 'text', 'all', 'native', 310, 0, 255, '', '', '', '', '', 3, '', '', ''), + (1, 'forwardPage', 'Forward Page', 'show', 'text', 'all', 'native', 310, 0, 0, '', '', '', '', '', 3, '', '', ''), (1, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 320, '40,3', 0, '', '', '', '', '', 3, '', '', ''), - (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 330, 0, 250, '', '', '', '', '', 3, '', '', '{{bsLabelColumns:Y}}'), - (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 340, 0, 250, '', '', '', '', '', 3, '', '', '{{bsInputColumns:Y}}'), - (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 350, 0, 250, '', '', '', '', '', 3, '', '', '{{bsNoteColumns:Y}}'), + (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 330, 0, 0, '', '', '', '', '', 3, '', '', '{{bsLabelColumns:Y}}'), + (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 340, 0, 0, '', '', '', '', '', 3, '', '', '{{bsInputColumns:Y}}'), + (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 350, 0, 0, '', '', '', '', '', 3, '', '', '{{bsNoteColumns:Y}}'), (1, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 360, 0, 0, '', '', '', '', '', 3, '', '', ''), (1, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 370, 0, 20, '', '', '', '', '', 3, '', '', ''), (1, 'created', 'Created', 'readonly', 'text', 'all', 'native', 380, 0, 20, '', '', '', '', '', 3, '', '', ''), @@ -174,9 +174,9 @@ VALUES (1, 'multi', 'Multi', 'show', 'fieldset', 'all', 'native', 400, 0, 0, '', '', '', '', '', 4, '', '', ''), (1, 'multiMode', 'Multi Mode', 'show', 'radio', 'all', 'native', 410, 0, 0, '', '', '', '', '', 4, '', '', ''), (1, 'multiSql', 'Multi SQL', 'show', 'text', 'all', 'native', 420, '40,3', 0, '', '', '', '', '', 4, '', '', ''), - (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'all', 'native', 430, 0, 255, '', '', '', '', '', 4, + (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'all', 'native', 430, 0, 0, '', '', '', '', '', 4, '', '', ''), - (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 440, 0, 255, '', '', + (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 440, 0, 0, '', '', '', '', '', 4, '', '', ''), (1, '', 'FormElements', 'show', 'subrecord', 'all', 'native', 500, 0, 0, '', '', '', @@ -193,16 +193,16 @@ VALUES 'FormElement', 'sip', 'sip', 'bootstrap', '', 'maxVisiblePill=5\nclassBody=qfq-color-blue-1', 'formId'); # FormEditor: FormElements for 'formElement' -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, +INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, note, clientJs, value, sql1, parameter, feIdContainer, subrecordOption, modeSql) VALUES - (100, 2, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', 0, '', ''), - (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', 0, '', + (100, 2, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, '', '', '', '', '', 0, '', ''), + (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'all', 'container', 20, 0, '', '', '', '', '', 0, '', ''), - (102, 2, 'layout', 'Layout', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', 0, '', ''), - (103, 2, 'value', 'Value', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', 0, '', ''), - (104, 2, 'info', 'Info', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', 0, '', ''); + (102, 2, 'layout', 'Layout', 'show', 'pill', 'all', 'container', 20, 0, '', '', '', '', '', 0, '', ''), + (103, 2, 'value', 'Value', 'show', 'pill', 'all', 'container', 20, 0, '', '', '', '', '', 0, '', ''), + (104, 2, 'info', 'Info', 'show', 'pill', 'all', 'container', 20, 0, '', '', '', '', '', 0, '', ''); INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, parameter, feIdContainer, subrecordOption, dynamicUpdate, bsLabelColumns, bsInputColumns, bsNoteColumns, modeSql, placeholder) @@ -210,45 +210,45 @@ VALUES (2, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'formId', 'formId', 'readonly', 'text', 'all', 'native', 110, 0, 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), + (2, 'formId', 'formId', 'readonly', 'text', 'all', 'native', 110, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), (2, 'feIdContainer', 'Container', 'show', 'select', 'all', 'native', 120, 0, 0, '', '', '', - '{{!SELECT fe.id, CONCAT(fe.type, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{formId:S0}} AND fe.class="container" ORDER BY fe.type, fe.name }}', + '{{!SELECT fe.id, CONCAT(fe.type, " / ", fe.name) FROM FormElement As fe WHERE fe.formId={{formId:S0}} AND fe.class="container" ORDER BY fe.type, fe.name }}', 'emptyItemAtStart', 100, '', 'no', '', '', '', '', ''), (2, 'enabled', 'Enabled', 'show', 'checkbox', 'all', 'native', 130, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), (2, 'dynamicUpdate', 'Dynamic Update', 'show', 'checkbox', 'all', 'native', 135, 0, 0, 'On change, this element will be updated and trigger other.', '', '', '', '', 100, '', 'no', '3', '2', '7', '', ''), - (2, 'name', 'Name', 'show', 'text', 'all', 'native', 140, 0, 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'label', 'Label', 'show', 'text', 'all', 'native', 150, 0, 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'mode', 'Mode', 'show', 'radio', 'all', 'native', 160, 0, 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'modeSql', 'Mode sql', 'show', 'text', 'all', 'native', 170, '70,2', 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'class', 'Class', 'show', 'select', 'all', 'native', 180, 0, 255, '', '', '{{class:FSRD0:alnumx}}', '', '', 100, '', 'yes', '', '', '', '', ''), - (2, 'type', 'Type', 'show', 'select', 'all', 'native', 190, 0, 255, '', '', '', '', + (2, 'name', 'Name', 'show', 'text', 'all', 'native', 140, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), + (2, 'label', 'Label', 'show', 'text', 'all', 'native', 150, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), + (2, 'mode', 'Mode', 'show', 'radio', 'all', 'native', 160, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), + (2, 'modeSql', 'Mode sql', 'show', 'text', 'all', 'native', 170, '70,2', 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), + (2, 'class', 'Class', 'show', 'select', 'all', 'native', 180, 0, 0, '', '', '{{class:FSRD0:alnumx}}', '', '', 100, '', 'yes', '', '', '', '', ''), + (2, 'type', 'Type', 'show', 'select', 'all', 'native', 190, 0, 0, '', '', '', '', 'itemList={{SELECT IF( "{{class:FRD0:alnumx}}"="native","checkbox,date,time,datetime,dateJQW,datetimeJQW,extra,gridJQW,text,editor,note,password,radio,select,subrecord,upload", IF("{{class:FRD0:alnumx}}"="action","beforeLoad,beforeSave,beforeInsert,beforeUpdate,beforeDelete,afterLoad,afterSave,afterInsert,afterUpdate,afterDelete,sendMail", "fieldset,pill,templateGroup") ) }}', 100, '', 'yes', '', '', '', '', ''), (2, 'subrecordOption', 'Subrecord Option', 'show', 'checkbox', 'all', 'native', 200, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), - (2, 'checkType', 'Check Type', 'show', 'select', 'all', 'native', 300, 0, 255, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), - (2, 'checkPattern', 'Check Pattern', 'show', 'text', 'all', 'native', 310, 0, 255, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), - (2, 'onChange', 'JS onChange', 'show', 'text', 'all', 'native', 320, 0, 255, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), - (2, 'ord', 'Order', 'show', 'text', 'all', 'native', 330, 0, 255, '', '', + (2, 'checkType', 'Check Type', 'show', 'select', 'all', 'native', 300, 0, 0, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), + (2, 'checkPattern', 'Check Pattern', 'show', 'text', 'all', 'native', 310, 0, 0, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), + (2, 'onChange', 'JS onChange', 'show', 'text', 'all', 'native', 320, 0, 0, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), + (2, 'ord', 'Order', 'show', 'text', 'all', 'native', 330, 0, 0, '', '', '{{SELECT IF({{ord:R0}}=0, MAX(IFNULL(fe.ord,0))+10,{{ord:R0}}) FROM (SELECT 1) AS a LEFT JOIN FormElement AS fe ON fe.formId={{formId:S0}} GROUP BY fe.formId}}', '', '', 101, '', 'no', '', '', '', '', ''), - (2, 'tabindex', 'tabindex', 'show', 'text', 'all', 'native', 340, 0, 255, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), - (2, 'size', 'Size', 'show', 'text', 'all', 'native', 400, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), - (2, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 410, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsLabelColumns:Y}}'), - (2, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 420, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsInputColumns:Y}}'), - (2, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 430, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsNoteColumns:Y}}'), + (2, 'tabindex', 'tabindex', 'show', 'text', 'all', 'native', 340, 0, 0, '', '', '', '', '', 101, '', 'no', '', '', '', '', ''), + (2, 'size', 'Size', 'show', 'text', 'all', 'native', 400, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), + (2, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 410, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsLabelColumns:Y}}'), + (2, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 420, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsInputColumns:Y}}'), + (2, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 430, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', '{{bsNoteColumns:Y}}'), (2, 'rowLabelInputNote', 'Label / Input / Note', 'show', 'checkbox', 'alnumx', 'native', 440, 0, 10, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), - (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 450, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), - (2, 'note', 'Note', 'show', 'editor', 'all', 'native', 460, '', 255, '', '', '', '', 'editor-plugins=code link table textcolor textpattern\neditor-toolbar=code | styleselect link table | bullist numlist | forecolor backcolor bold italic\neditor-menubar=false\neditor-statusbar=false', 102, '', 'no', '', '', '', '', ''), - (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 470, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), + (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 450, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), + (2, 'note', 'Note', 'show', 'editor', 'all', 'native', 460, '', 0, '', '', '', '', 'editor-plugins=code link table textcolor textpattern\neditor-toolbar=code | styleselect link table | bullist numlist | forecolor backcolor bold italic\neditor-menubar=false\neditor-statusbar=false', 102, '', 'no', '', '', '', '', ''), + (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 470, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 480, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), - (2, 'value', 'value', 'show', 'text', 'all', 'native', 500, '40,2', 255, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), - (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 510, '40,5', 255, 'MariaDB: <a href="https://mariadb.com/kb/en/mariadb/select/">Select</a>, <a href="https://mariadb.com/kb/en/mariadb/functions-and-operators/">Functions</a>', '', '', '', '', 103, '', 'no', '', '', '', '', ''), - (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 520, '40,4', 255, '', + (2, 'value', 'value', 'show', 'text', 'all', 'native', 500, '40,2', 0, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), + (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 510, '40,5', 0, 'MariaDB: <a href="https://mariadb.com/kb/en/mariadb/select/">Select</a>, <a href="https://mariadb.com/kb/en/mariadb/functions-and-operators/">Functions</a>', '', '', '', '', 103, '', 'no', '', '', '', '', ''), + (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 520, '40,4', 0, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), - (2, 'clientJs', 'ClientJS', 'show', 'text', 'all', 'native', 530, 0, 255, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), - (2, 'feGroup', 'feGroup', 'show', 'text', 'all', 'native', 600, 0, 255, '', '', '', '', '', 104, '', 'no', '', '', '', '', ''), + (2, 'clientJs', 'ClientJS', 'show', 'text', 'all', 'native', 530, 0, 0, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), + (2, 'feGroup', 'feGroup', 'show', 'text', 'all', 'native', 600, 0, 0, '', '', '', '', '', 104, '', 'no', '', '', '', '', ''), (2, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 610, 0, 0, '', '', '', '', '', 104, '', 'no', '', '', '', '', ''), (2, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 620, 0, 20, '', '', '', '', '', 104, '', 'no', diff --git a/extension/qfq/tests/phpunit/BuildFormPlainTest.php b/extension/qfq/tests/phpunit/BuildFormPlainTest.php index 2546d02f120e43f0802fb743a3e7699e334b2bae..c9553ae3f737389c0332349aa68be5c3307de382 100644 --- a/extension/qfq/tests/phpunit/BuildFormPlainTest.php +++ b/extension/qfq/tests/phpunit/BuildFormPlainTest.php @@ -83,32 +83,32 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $label['123-l'][API_ELEMENT_CONTENT] = '<label for="name:1" class="control-label" >Name</label>'; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" maxlength="255" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', API_ELEMENT_UPDATE => $label], $json); // CheckType $formElement['checkType'] = SANITIZE_ALLOW_MIN_MAX; $formElement['checkPattern'] = '1|10'; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" maxlength="255" value="" min="1" max="10" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" value="" min="1" max="10" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); $formElement['checkType'] = SANITIZE_ALLOW_PATTERN; $formElement['checkPattern'] = '^[a-z]*$'; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[a-z]*$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" value="" pattern="^[a-z]*$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); $formElement['checkType'] = SANITIZE_ALLOW_DIGIT; $formElement['checkPattern'] = ''; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[\d]*$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" value="" pattern="^[\d]*$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); $formElement['checkType'] = SANITIZE_ALLOW_EMAIL; $formElement['checkPattern'] = ''; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" value="" pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); $formElement['checkType'] = ''; @@ -119,13 +119,13 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement['size'] = 40; $formElement['maxLength'] = 40; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" size="40" maxlength="40" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="40" type="input" size="40" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); // maxlength bigger than physical spec: $formElement['maxLength'] = 1000; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" size="40" maxlength="255" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" size="40" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); // no size, no maxlength and column not in primary table @@ -139,7 +139,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { // no size, given maxlength and column not in primary table $formElement2['maxLength'] = '10'; $result = $build->buildInput($formElement2, 'specialname:1', '', $json); - $this->assertEquals('<input id="123" name="specialname:1" class="form-control" type="input" maxlength="10" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="specialname:1" class="form-control" maxlength="10" type="input" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); // size given, no maxlength and column not in primary table $formElement2['maxLength'] = ''; @@ -151,25 +151,26 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement2['maxLength'] = '20'; $formElement2['size'] = '10'; $result = $build->buildInput($formElement2, 'specialname:1', '', $json); - $this->assertEquals('<input id="123" name="specialname:1" class="form-control" type="input" size="10" maxlength="20" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="specialname:1" class="form-control" maxlength="20" type="input" size="10" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); // Explicit: further $formElement['tooltip'] = 'Nice Tooltip'; $formElement['placeholder'] = 'Please type a name'; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<input id="123" name="name:1" class="form-control" type="input" size="40" maxlength="255" value="Hello World" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<input id="123" name="name:1" class="form-control" maxlength="255" type="input" size="40" value="Hello World" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); // textarea $formElement['size'] = '40,10'; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<textarea id="123" name="name:1" class="form-control" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" >Hello World</textarea><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<textarea id="123" name="name:1" class="form-control" maxlength="255" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" >Hello World</textarea><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); $formElement['size'] = ' 40 , 10 '; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<textarea id="123" name="name:1" class="form-control" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" >Hello World</textarea><div class="help-block with-errors hidden"></div>', $result); + $this->assertEquals('<textarea id="123" name="name:1" class="form-control" maxlength="255" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" data-hidden="no" data-disabled="no" data-required="no" >Hello World</textarea><div class="help-block with-errors hidden"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, API_ELEMENT_UPDATE => $label], $json); } diff --git a/extension/qfq/tests/phpunit/EvaluateTest.php b/extension/qfq/tests/phpunit/EvaluateTest.php index a5a1e951e74477abe70baf50908985e1636b2a1b..6b96edac3b07f1af181b63e8f11a18afb0ce6e89 100644 --- a/extension/qfq/tests/phpunit/EvaluateTest.php +++ b/extension/qfq/tests/phpunit/EvaluateTest.php @@ -322,6 +322,40 @@ class EvaluateTest extends \AbstractDatabaseTest { $this->assertEquals('h\"e\' \'l\"lo \' ', $eval->substitute('a:F:all:d', $foundInStore)); $this->assertEquals(STORE_FORM, $foundInStore); +//--- + $this->store->setVar('a', ' hello world ', STORE_FORM, true); + $this->assertEquals(' hello world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', ' hel\lo world ', STORE_FORM, true); + $this->assertEquals(' hel\5clo world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', ' hel*lo world ', STORE_FORM, true); + $this->assertEquals(' hel\2alo world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', ' hel(lo world ', STORE_FORM, true); + $this->assertEquals(' hel\28lo world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', ' hel)lo world ', STORE_FORM, true); + $this->assertEquals(' hel\29lo world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', " hel\x00lo world ", STORE_FORM, true); + $this->assertEquals(' hel\00lo world ', $eval->substitute('a:F:all:l', $foundInStore)); + + $this->store->setVar('a', ' h\e*l(l)o world ', STORE_FORM, true); + $this->assertEquals(' h\5ce\2al\28l\29o world ', $eval->substitute('a:F:all:l', $foundInStore)); + + +// LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"), +// LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'), + $this->store->setVar('a', ' hello world ', STORE_FORM, true); + $this->assertEquals('\20hello world\20', $eval->substitute('a:F:all:L', $foundInStore)); + + $this->store->setVar('a', 'h\e,l=l+o< >w;o"r#ld', STORE_FORM, true); + $this->assertEquals('h\5ce\2cl\3dl\2bo\3c \3ew\3bo\22r\23ld', $eval->substitute('a:F:all:L', $foundInStore)); + + + $this->store->setVar('a', ' hel;lo world ', STORE_FORM, true); + $this->assertEquals('\20hel\3blo world\20', $eval->substitute('a:F:all:sL', $foundInStore)); } diff --git a/extension/qfq/tests/phpunit/OnArrayTest.php b/extension/qfq/tests/phpunit/OnArrayTest.php index c60e3c7e6767b2923cc03887c47988f636b73daa..de04a7ec2a532e1425f6544a6588955396f533b4 100644 --- a/extension/qfq/tests/phpunit/OnArrayTest.php +++ b/extension/qfq/tests/phpunit/OnArrayTest.php @@ -89,4 +89,50 @@ class OnArrayTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($expected, OnArray::removeEmptyElementsFromArray($raw2)); } + + public function testCopyValuesIfNotAlreadyExist() { + + $this->assertEquals(array(), OnArray::copyArrayItemsIfNotAlreadyExist(array(), array(), array())); + + $this->assertEquals(array(), OnArray::copyArrayItemsIfNotAlreadyExist(['a' => 'something'], array(), array())); + + $this->assertEquals(array(), OnArray::copyArrayItemsIfNotAlreadyExist(['a' => 'something'], array(), ['b'])); + + $this->assertEquals(['a' => 'base'], OnArray::copyArrayItemsIfNotAlreadyExist(['a' => 'something'], ['a' => 'base'], ['unknown'])); + + $this->assertEquals(['b' => 'base', 'a' => 'something'], OnArray::copyArrayItemsIfNotAlreadyExist(['a' => 'something'], ['b' => 'base'], ['a'])); + + $this->assertEquals(['a' => 'base'], OnArray::copyArrayItemsIfNotAlreadyExist(['a' => 'something'], ['a' => 'base'], ['a'])); + + } + + public function testGetArrayItems() { + $this->assertEquals(array(), OnArray::getArrayItems(array(), array())); + $this->assertEquals(array(), OnArray::getArrayItems(['a' => 'hello'], array())); + $this->assertEquals(array(), OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], array())); + $this->assertEquals(array(), OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['c'])); + $this->assertEquals(['a' => 'hello'], OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['a'])); + $this->assertEquals(['a' => 'hello', 'b' => 'world'], OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['a', 'b'])); + + $this->assertEquals(array(), OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['c'], false)); + $this->assertEquals(['c' => ''], OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['c'], true)); + + $this->assertEquals(['a' => 'hello'], OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['a', 'c'], false)); + $this->assertEquals(['a' => 'hello', 'c' => ''], OnArray::getArrayItems(['a' => 'hello', 'b' => 'world'], ['a', 'c'], true)); + } + + public function testArrayValueReplace() { + $this->assertEquals(array(), OnArray::arrayValueReplace(array(), '', '')); + $this->assertEquals(array(), OnArray::arrayValueReplace(array(), 'a', '')); + $this->assertEquals(array(), OnArray::arrayValueReplace(array(), '', 'b')); + $this->assertEquals(array(), OnArray::arrayValueReplace(array(), 'a', 'b')); + + $src = ['fruit1' => 'apple', 'fruit2' => 'cherry', 'fruit3' => 'banana']; + $this->assertEquals($src, OnArray::arrayValueReplace($src, '', '')); + $this->assertEquals($src, OnArray::arrayValueReplace($src, 'Z', '')); + $this->assertEquals($src, OnArray::arrayValueReplace($src, 'Z', 'Y')); + + $expected = ['fruit1' => 'Apple', 'fruit2' => 'cherry', 'fruit3' => 'bAnAnA']; + $this->assertEquals($expected, OnArray::arrayValueReplace($src, 'a', 'A')); + } } diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index e1f88f2af51d35b9e36f78fa9d726f20448f20e0..bba6fb9e01e9a3f15fec0afce2d058ba886d63e3 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -96,7 +96,6 @@ i.@{spinner_class} { padding-top: 4px; } - .qfq-color-white { background-color: #ffffff; } @@ -135,9 +134,18 @@ i.@{spinner_class} { display: block !important; } +.qfq-cc-style { + font-size: 0.8em; + color: #777; + //position: relative; + //top: -24px; + //right: 10px; + //text-align: right; + //display: block; +} + // TypeAhead Suggestions .tt-menu { - padding: 12px; background-color: #fff; border-left: 1px solid #66afe9; border-right: 1px solid #66afe9; @@ -145,11 +153,31 @@ i.@{spinner_class} { border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); + width: 100%; +} + +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: unset; + } +} + +.tt-suggestion { + cursor: pointer; + cursor: hand; + padding-left: 12px; + padding-right: 12px; + padding-bottom: 6px; + padding-top: 12px; } .tt-suggestion + .tt-suggestion { - margin-top: 12px; + padding-top: 6px; +} + +.tt-suggestion:hover { + background-color: rgba(102, 175, 233, .6); }