diff --git a/CODING.md b/CODING.md index c14b9849e914c2a206d1c8d3d983155e799ae79b..9c969543347893005f867a23215083b3ba5b020c 100644 --- a/CODING.md +++ b/CODING.md @@ -158,8 +158,8 @@ New Button * Client stays on current page. -File Handling -------------- +File Handling: Upload +--------------------- * No previous uploaded file present 1. User presses the Browse button 1. User selects file @@ -173,6 +173,39 @@ File Handling 1. File delete button gets disabled and hidden 1. Browse button gets enabled and displayed +Form Build (load) +................. +* The upload functionality consist of three elements + * 1) A <div> tag with a) an optional filename of an earlier uploaded file plus and b) a trash Button. + * 2) The 'browse' button (<input type='file' name='_upload_<feName>'>). This element will not be send by post. + * 3) A HTML hidden element with name=<feName> containing the <sipUpload>. +* A new uniq SIP (sipUpload) will be created for every upload formElement. These 'sipUpload' will be assigned to the browse button and to the delete button. + * The individual sipUpload is necessary to correctly handle multiple simustaenously forms when using r=0. Also, through this uniq id it's easy to distinguish between asynchron uploaded files. + * The SIP contains the '_FILES' information submitted during the upload. +* Via the hidden element <feName> 'save()' access the form individual upload status informations. + +Upload to server, before 'save' +............................... +* If a user open's a file for upload via the browse button, that file is immediately transmitted to the server. The user will see a turning wheel during the upload time. +* On success the 'Browse; Button disappears and the filename plus the delete button will be displayed (client logic). +* The uploaded file will be checked: maxsize, mime type, check script. +* The uploaded file is still temporary. It has been renamed from $_SESSION['X'][<uploadSip>][FILES_TMP_NAME] to $_SESSION['X'][<uploadSip>][FILES_TMP_NAME].cached +* The upload action will be saved in the user session. + * $_SESSION['X'][<uploadSip>][FILES_TMP_NAME|FILES_NAME|FILES_ERROR|FILE_SIZE] +* Clicks the user on delete button. + * In the usersession a flagDelete will be set: $_SESSION['X'][<uploadSip>]['flagDelete']='1' + * An optional previous upload file (still not saved on the final place) will be deleted. + * An optional existing variable $_SESSION['X'][<uploadSip>][FILES_TMP_NAME] will be deleted. The 'flagDelete' must not be change - it's later needed to detect to delete earlier uploaded files. + +Form save +......... +* Before building the insert/update, process all 'uploads'. +* Get every uniq sipUpload to every upload formElement. Get the corresponding temporary uploaded filename. +* If $_SESSION['X'][<uploadSip>]['flagDelete']='1' is set, delete prefious uploaded file. +* Calculate <destination> +* mv <file>.cached <destination> +* clientvalue[<feName>] = <destination> +* delete $_SESSION['X'][<uploadSip>] Formelement type: DATE / DATETIME / TIME ---------------------------------------- diff --git a/extension/Documentation/UsersManual/Index.rst b/extension/Documentation/UsersManual/Index.rst index e722f638b03abd280402077844e5f7415f2680c8..6e8d5871fb284d41c9b9a3ccb1392411e68467ce 100644 --- a/extension/Documentation/UsersManual/Index.rst +++ b/extension/Documentation/UsersManual/Index.rst @@ -19,11 +19,9 @@ Features not implemented yet ---------------------------- * Multi Forms -* File upload * FormElement: * type=action (especially not *addNupdate*) - * field dependencies (activating a parent element, activates child elements and vice versa) * Checkbox: some combinations not tested. QFQ content element @@ -183,6 +181,7 @@ Sanitize class * **min|max**: only usable in forms. Compares the value against an lower and upper limit (numeric or string). * **min|max date**: only usable in forms. Compares the value against an lower and upper date or datetime. * **pattern**: only usable in forms. Compares the value against a regexp. + * **allbut**: all characters allowed, but not [ ] { } % & \ #. The used regexp: '^[^\[\]{}%&\\#]+$', * **all**: no sanitizing @@ -388,9 +387,9 @@ Form: basic setup +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |forwardPage | string / query | If $forward=="page": page to jump to | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ -|bsLabelColumns | string | title: default number of 'bootstrap 12grid' columns | -+------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ -|bsInputColumns | string | input: default number of 'bootstrap 12grid' columns | +|bsLabelColumns | string | The bootstrap grid system is based on 12 columns. The sum of *bsLabelColumns*, | ++------------------------+----------------------------------------------------------+ *bsInputColumns* and *bsNoteColumns* should be 12. These values here are the base values| +|bsInputColumns | string | for all formelements. Exceptions per formelement can be specified per formelement. | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |bsNoteColumns | string | note: default number of 'bootstrap 12grid' columns | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ @@ -481,81 +480,87 @@ Type: pill Class: Native ------------- -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -| Name | Type | Description | -+==============+=============================+===================================================================================================+ -| id | int | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -| formId | int | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|feIdContainer | int | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|enabled | enum('yes'|'no') | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|dynamicUpdate | enum('yes'|'no') | In the browser, formelements with "dynamicUpdate='yes'" will be updated depending on user input. | -| | | :ref:`dynamic-update` | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|name | string | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|label | string | Label of formelement. Depending on layout model, left or on top of the formelement | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|mode | enum('show', 'readonly', | Default: show - *Show*: regular user input field. *Readonly* : user can't change any data. | -| | 'required', 'lock', | *Important* : user manipulated data won't be saved. *Required* User has to specify a value. | -| | 'disabled' ) | Typically, an <empty string> represents 'no value'. *Lock* form element is read only and grayed | -| | | out, *Disabled*: form element is not visible | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|class | enum('native', 'action', | Details below. | -| | 'container') | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|type | enum('checkbox', 'date', 'time', 'datetime', 'dateJQW', 'datetimeJQW', 'gridJQW', 'hidden', 'text', 'note', 'password', | -| | 'radio', 'select', 'subrecord', 'textarea', 'timeJQW', 'upload', 'fieldset', 'pill', 'before_load', 'before_save', | -| | 'before_insert', 'before_update', 'before_delete', 'after_load', 'after_save', 'after_insert', 'after_update', 'after_delete', | -| | 'feGroup', 'sendmail') | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|checkType | enum('min|max', 'pattern', | | -| | 'number', 'email') | | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|checkPattern | 'regexp' |if $check_type=='pattern': pattern to match | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|onChange | string |list of 'form element names' of current form, separated by ', ', If one of the named form elements | -| | | change, reload own data / status / mode | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|ord | string |display order of form elements ('order' is a reserved keyword) | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|tabindex | string |HTML tabindex attribute | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|size | string |Visible length of input element. Might be ommited, depending on the choosen form layout. | -| | |Format: <width>,<height> (in characters) | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|maxLength | string |Maximum characters for input. | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|note | string |Note of formelement. Depending on layout model, right or below of the formelement | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|tooltip | text |Display this text as tooltip on mouse over | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|placeholder | string |Text, displayed inside the input element in light grey | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|clientJs | text |Javascript called on 'on change' formelements | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|value | text |Default value | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|sql1 | text |SQL query ('sql' is a reserved keyword) | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|sql2 | text |second SQL query | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|parameter | text |might contain misc parameter. Depends on the type of formelement. | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|feGroup | string | Comma-separated list of Typo3 FE Group ID. NOT SURE IF THIS WILL BE IMPLEMENTED. Native | -| | | formElements, fieldsets and pills can be assigned to feGroups. Group status: show, hidden, | -| | | disabled. Group Access: FE-Groups. User will be assigned to FE-Groups and the form defintion | -| | | reference such FE-groups. Easy way of granting permission. | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|deleted | string |'yes'|'no'. | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|modified | timestamp |updated autmatically through stored procedure | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|created | datetime |set once through QFQ | -+--------------+-----------------------------+---------------------------------------------------------------------------------------------------+ ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +| Name | Type | Description | ++===============+=============================+===================================================================================================+ +| id | int | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +| formId | int | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|feIdContainer | int | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|enabled | enum('yes'|'no') | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|dynamicUpdate | enum('yes'|'no') | In the browser, formelements with "dynamicUpdate='yes'" will be updated depending on user input. | +| | | :ref:`dynamic-update` | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|name | string | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|label | string | Label of formelement. Depending on layout model, left or on top of the formelement | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|mode | enum('show', 'readonly', | *Show*: regular user input field. This is the default. | +| | 'required', | *Required*: User has to specify a value. Typically, an <empty string> represents 'no value'. | +| | 'disabled' ) | *Readonly*: user can't change any data. Data not saved. | +| | | *Disabled*: form element is not visible. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|class | enum('native', 'action', | Details below. | +| | 'container') | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|type | enum('checkbox', 'date', 'time', 'datetime', 'dateJQW', 'datetimeJQW', 'gridJQW', 'hidden', 'text', 'note', 'password', | +| | 'radio', 'select', 'subrecord', 'textarea', 'timeJQW', 'upload', 'fieldset', 'pill', 'before_load', 'before_save', | +| | 'before_insert', 'before_update', 'before_delete', 'after_load', 'after_save', 'after_insert', 'after_update', 'after_delete', | +| | 'feGroup', 'sendmail') | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|checkType | enum('min|max', 'pattern', | | +| | 'number', 'email') | | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|checkPattern | 'regexp' |if $check_type=='pattern': pattern to match | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|onChange | string |list of 'form element names' of current form, separated by ', ', If one of the named form elements | +| | | change, reload own data / status / mode | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|ord | string |display order of form elements ('order' is a reserved keyword) | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|tabindex | string |HTML tabindex attribute | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|size | string |Visible length of input element. Might be ommited, depending on the choosen form layout. | +| | |Format: <width>,<height> (in characters) | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|bsLabelColumns | string | Number of bootstrap grid columns for label. By default empty, value inherits from the form. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|bsInputColumns | string | Number of bootstrap grid columns for input. By default empty, value inherits from the form. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|bsNoteColumns | string | Number of bootstrap grid columns for note. By default empty, value inherits from the form. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|maxLength | string |Maximum characters for input. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|note | string |Note of formelement. Depending on layout model, right or below of the formelement | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|tooltip | text |Display this text as tooltip on mouse over | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|placeholder | string |Text, displayed inside the input element in light grey | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|clientJs | text |Javascript called on 'on change' formelements | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|value | text |Default value | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|sql1 | text |SQL query ('sql' is a reserved keyword) | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|sql2 | text |second SQL query | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|parameter | text |might contain misc parameter. Depends on the type of formelement. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|feGroup | string | Comma-separated list of Typo3 FE Group ID. NOT SURE IF THIS WILL BE IMPLEMENTED. Native | +| | | formElements, fieldsets and pills can be assigned to feGroups. Group status: show, hidden, | +| | | disabled. Group Access: FE-Groups. User will be assigned to FE-Groups and the form defintion | +| | | reference such FE-groups. Easy way of granting permission. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|deleted | string |'yes'|'no'. | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|modified | timestamp |updated autmatically through stored procedure | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +|created | datetime |set once through QFQ | ++---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ +------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ @@ -774,37 +779,71 @@ Type: select Type: subrecord ^^^^^^^^^^^^^^^ -'subrecord' will be rendered as HTML table. - -* *parameter* - - * *detail*: e.g. *detail=id:gr_id,#{{a}}:p_id,#12:x_id* - * *form*: Target form, e.g. *form=person* - * *page*: Target page with detail form. If none specified, use the current page - * *title*: Title displayed over the table. +'subrecord' present a list of records (called secondary records), typically to edit, delete or add such records. The list + is defined as a SQL query. The number of records shown is not limited. These formelement will be rendered inside the + form as a HTML table. The list is ordered by 1) formelement.class (action, container, native), 2) formelement.container, + 3) formelement.ord * *sql1*: SQL query to select records. E.g.:: - {{!SELECT a.id AS id, CONCAT(a.street, a.streetnumber) AS a, a.city AS b, a.zip AS c FROM adresse AS a}} + {{!SELECT a.id AS id, CONCAT(a.street, a.streetnumber) AS a, a.city AS b, a.zip AS c FROM Address AS a}} # Notice the **exclamation mark** after '{{' - this is necessary to return an array of elements, instead of a single string. * Exactly one column 'id' has to exist; it specifies the primary record for the target form. + In case the id should no be visible to the user, it has to be named *'_id'*. + + * Columnname: *<title>[|<number>][|width=<number>][|nostrip][|icon][|url][|mailto|_rowClass]* + + * *<number>*: any 'digit only' will be treated as '''width'''. + * *width=<number>*: max. number of chars displayed per cell in the column. + * *nostrip*: by default, html tags will be stripped off the cell content before rendering. This protects the table + layout. 'nostrip' deactivates the cleaning to make links, images, ... possible. + * *icon*: the cell value contains the name of an icon in *typo3conf/ext/qfq/Resources/Public/icons*. Empty cell values + will omit an html image tag (=nothing renderd in the cell). + * *mailto*: value will be rendered as a mailto link. + * *url*: value will be rendered as a link. + * *title=<text>* or '<none of the above>': column '''title'''. + * The parameters are position independet. + * Examples:: -* Columnname: *<title>[|<number>][|width=<number>][|nostrip][|icon][|url][|mailto]* + SELECT note1 AS 'Comment', note2 AS 'Comment\|50' , note3 AS 'title=Comment\|width=100\|nostrip', note4 AS '50\|Comment', + 'checked.png' AS 'Status\|icon', email AS 'mailto', CONCAT(homepage, '\|Homepage') AS 'url' ... - * *<number>*: any 'digit only' will be treated as '''width'''. - * *width=<number>*: max. number of chars displayed per cell in the column. - * *nostrip*: by default, html tags will be stripped off the cell content before rendering. This protects the table layout. 'nostrip' deactivates the cleaning to make links, images, ... possible. - * *icon*: the cell value contains the name of an icon in *typo3conf/ext/qfq/Resources/Public/icons*. Empty cell values will omit an html image tag (=nothing renderd in the cell). - * *mailto*: value will be rendered as a mailto link. - * *url*: value will be rendered as a link. - * *title=<text>* or '<none of the above>': column '''title'''. - * The parameters are position independet. - * Examples:: + * *_rowClass* - SELECT note1 AS 'Comment', note2 AS 'Comment\|50' , note3 AS 'title=Comment\|width=100\|nostrip', note4 AS '50\|Comment', - 'checked.png' AS 'Status\|icon', email AS 'mailto', CONCAT(homepage, '\|Homepage') AS 'url' ... + * The value is a CSS class name(s) which will be rendered in the *<tr class="<_rowClass>">* of the subrecord table. + * The column itself is hidden to the user. + * By using Bootstrap, the following predefined classes are available: + + * Text color: *text-muted|text-primary|text-success|text-info|text-warning|text-danger* (http://getbootstrap.com/css/#helper-classes) + * Row background: *active|success|info|warning|danger* (http://getbootstrap.com/css/#tables-contextual-classes) + + * *_rowTitle* + + * Defines the title attribute of a subrecod table row (tooltip). + +* *parameter* + + * *form*: Target form, e.g. *form=person* + * *page*: Target page with detail form. If none specified, use the current page. + * *title*: Title displayed over the table in the current form. + * *detail*: Mapping of values from the primary form to the target form (defined via `form=...`). + + * Syntax:: + + <source table column name 1|&constant 1>:<target column name 1>[,<source table column name 2|&constant 2>:<target column name 2>][...] + + * Example: *detail=id:personId,&12:xId,&{{a}}:personId* + * By default, the given value will overwrite values on the target record. In most situations, this is the wished behaviour. + * Exceptions of the default behaviour have to be defined on the target form in the corresponding formelement in the + field *value*. E.g. `{{<columnname>:RS0}}` - For existing records, the store `R` will provide a value. For new + records, store `R` is empty and store S will be searched for a value: the value defined in `detail` will be choosen. At + last the store '0' is defined as a fallback. + * *source table column name*: E.g. A person form is opened with person.id=5 (r=5). The definition `detail=id:personId` and `form=address` + maps person.id to address.personId. On the target record, the column personId becomes '5'. + * *Constant '&'*: Indicate a 'constant' value. E.g. `&12:xId` or `{{...}}` (all possibilities, incl. further SELECT + statements) might be used. Type: string @@ -818,10 +857,10 @@ Typically not used. Useful if user wishes an explicit 'Submit' Button. Type: time ^^^^^^^^^^ - * Range time: '00:00:00' to '23:59:59' or '00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) - * Optional: - * *showSeconds*: 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed '1' or not '0'. - * *showZero*: 0|1 - For an empty timestamp, With '0' nothing is displayed. With '1' the string '00:00[:00]' is displayed. +* Range time: '00:00:00' to '23:59:59' or '00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) +* Optional: + * *showSeconds*: 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed '1' or not '0'. + * *showZero*: 0|1 - For an empty timestamp, With '0' nothing is displayed. With '1' the string '00:00[:00]' is displayed. Type: upload ^^^^^^^^^^^^ @@ -829,6 +868,25 @@ Type: upload * See: https://www.w3.org/TR/html5/forms.html#file-upload-state-(type=file) * parameter:accept: *image/*,video/*,audio/*,.doc,.docx,.pdf,<mime type>* +An upload element is based on a file browse button and a delete button. Only one of them is shown at a time. +If there is no file already uploaded, the file browse button is displayed. + +The user can than select a file from the local filesystem. +After choosing the file, the upload starts immediately, shown by a turning wheel. When the server received the whole file +and accepts the file, the browse button dissappears and the filename is shown, followed by a delete button. +Now, the user can delete the uploaded file (and maybe upload another one) or leave it as it is. +Until this point, the file is cached on the server but not copied to the final destination. The user have to save the +current record either to finalize the upload or to delete a previous uploaded file. + +* FormElement. *parameter*: + + * *pathFileName*: Destination where to copy the file. A good practice is to specify a relative pathFileName - such an + installation (filesystem and database) are moveable. If the final pathFileName should contain the original filename, + the variable *{{_filename}}* can be used. Example:: + + pathFileName={{SELECT 'fileadmin/user/pictures/', p.name, '-{{_filename}}' FROM Person AS p WHERE p.id={{r}} }} + + .. _class-action: Class: Action @@ -971,12 +1029,18 @@ Syntax Variables from specific stores: {{<name>[:<store/s>[:<sanitize class>]]}} - Row index and total rows: {{<level>.line.count}} or {{<level>.line.total}} + Current row index: {{<level>.line.count}} + + Total rows (num_rows for SELECT and SHOW, affected_rows for UPDATE and INSERT): {{<level>.line.total}} - 'last_insert_id' and 'affected_rows': {{<level>.line.total}} (*total* may have a different meaning, depending on the type of the SQL command) + Last insert id for INSERT: {{<level>.line.insertId}} See :ref:`variables` for a full list of all available variables. + Be aware that line.count / line.total have to be known before the query is fired. E.g. `10.sql = SELECT {{10.line.count}}, ... WHERE {{10.line.count}} = ...` + won't work as expected. `{{10.line.count}}` can't be replaced before the query is fired, but will be replaced during processing the result! + + Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW Only SELECT and SHOW queries will fire subqueries. @@ -1063,7 +1127,7 @@ Example:: Nesting of levels ^^^^^^^^^^^^^^^^^ -Levels can be nested by using curly brackets:: +Levels can be nested by using curly brackets. Be carefull to write nothing than whitespaces/newline behind open or closing curly braces:: 10.sql = SELECT 'hello world' @@ -1144,18 +1208,19 @@ Processing of columns in the SQL result * The content of all columns of all rows will be printed sequentially, without separator. * Rows with `Special column names`_ will be processed in a special way. -* Column names, which start with a '_', will not be printed. This is useful if you only want to access the content of - some columns during later on via the {{<level>.<column>}} variable but don't want to print the content of those columns. Special column names -------------------- +* Special column names always start with '_'. +* Column names, which start with a '_' and which are not reserved (=special column name), will not be printed. Nevertheless, + access to it via the {{<level>.<column>}} variable (without '_') are possible. * The input parameters for the processing function are stored as column values. * Single parameters are delimited by the '|' character. * Parameters are identified by the function either * by their **order** - * or by a **one character qualifier** followed by the : character, placed in front of the actual parameter value. + * or by a **one character qualifier** followed by the ':' character, placed in front of the actual parameter value. +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |**Reserved column name**| **Purpose** | @@ -1321,8 +1386,8 @@ Link Examples +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -Columns: pageX & PageX -^^^^^^^^^^^^^^^^^^^^^^ +Columns: _pageX & _PageX +^^^^^^^^^^^^^^^^^^^^^^^^ These columns provide a shortcut version of the link interface to use for fast creation of internal links. The colum name is composed of the string *page* and an optional character to specify the type of the link. @@ -1400,8 +1465,8 @@ below. +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -Column: vertical -^^^^^^^^^^^^^^^^ +Column: _vertical +^^^^^^^^^^^^^^^^^ Render text vertically. This is useful for tables with limited column width. The vertical rendering is achieved via CSS tranformations (rotation) defined in the style attribute of the wrapping tag. You can optionally specify the rotation angle. @@ -1455,8 +1520,8 @@ angle. -Column: mailto -^^^^^^^^^^^^^^ +Column: _mailto +^^^^^^^^^^^^^^^ Easily create Email links. @@ -1504,8 +1569,8 @@ Easily create Email links. -Column: sendmail -^^^^^^^^^^^^^^^^ +Column: _sendmail +^^^^^^^^^^^^^^^^^ Send simple plain text emails. Every mail will be logged in the mail log. The logfile can be configured in ext_localconf.php via $TYPO3_CONF_VARS[$_EXTKEY]['log']['mail']. @@ -1559,8 +1624,8 @@ This will send an email with subject *Latest News* from company@example.com to j This will send an email with subject *Latest news* from company@example.com to customer1@example.com and to customer2@example.com. -Column: advancedmail -^^^^^^^^^^^^^^^^^^^^ +Column: _advancedmail +^^^^^^^^^^^^^^^^^^^^^ Send plain text/html emails. This is identical to ?t#Column:_sendmail, but allows to additionaly set the cc:, bcc: and reply-to: -headers. Every mail will be logged in the mail log. The logfile can be configured in ext_localconf.php via $TYPO3_CONF_VARS[$_EXTKEY]['log']['mail']. @@ -1597,8 +1662,8 @@ $TYPO3_CONF_VARS[$_EXTKEY]['log']['mail']. +------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -Column: img -^^^^^^^^^^^ +Column: _img +^^^^^^^^^^^^ Renders images. Allows to define an alternative text and a title attribute for the image. Alternative text and title text are optional. @@ -1656,8 +1721,8 @@ Renders images. Allows to define an alternative text and a title attribute for t -Column: exec -^^^^^^^^^^^^ +Column: _exec +^^^^^^^^^^^^^ Runs batch files or executables on the webserver. In case of an error, returncode and errormessage will be returned. @@ -1691,8 +1756,8 @@ Runs batch files or executables on the webserver. In case of an error, returncod -Column: F -^^^^^^^^^ +Column: _F +^^^^^^^^^^ Challenge 1 ''''''''''' @@ -2204,3 +2269,28 @@ QFQ content record:: fend = </td> } + +Secondary records: compute next free 'ord' automatically +-------------------------------------------------------- + +Requirement: new records should automatically get the highest number plus 10 for their 'ord' value. Existing records +should not be altered. + +Version 1 +^^^^^^^^^ + +Compute the next 'ord' in advance in the subrecord field of the primary form. Submit that value to the new record +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 `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 `{{RS0}}`. + +Version 2 +^^^^^^^^^ + +Compute the next 'ord' as default value direct inside the secondary form. No change is needed for the primary form. + +* Secondary form, `ord` formelement, field `value`: set `{{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}}`. diff --git a/extension/qfq/api/file.php b/extension/qfq/api/file.php index 03dcdddb0f467e27177c8fa934da67ca36b34274..dedd27cfca4cf33689c2074dbb05fd56582b5ca4 100644 --- a/extension/qfq/api/file.php +++ b/extension/qfq/api/file.php @@ -6,6 +6,31 @@ * Time: 8:02 PM */ +namespace qfq; + +use qfq; + +require_once(__DIR__ . '/../qfq/store/Store.php'); +require_once(__DIR__ . '/../qfq/Constants.php'); +require_once(__DIR__ . '/../qfq/File.php'); + +/** + * Return JSON encoded answer + * + * status: success|error + * message: <message> + * + * Description: + * + * Upload successfull & File accepted by server. + * status = 'success' + * message = <message> + * + * Upload failed: + * status = 'error' + * message = <message> + */ + $answer = array(); $answer[API_MESSAGE] = ''; @@ -13,12 +38,12 @@ $answer[API_STATUS] = API_ANSWER_STATUS_ERROR; try { - $upload = new \qfq\File(); + $fileUpload = new \qfq\File(); - $upload->process(); + $fileUpload->process(); $answer[API_MESSAGE] = 'upload: success'; - $answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO; +// $answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO; $answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS; } catch (qfq\UserFormException $e) { diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index bdbe52ec2c8b0d784b5276e144f75610a6ab8c4c..b5682ba8ef977e683bf921b803fc296858b4737d 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -42,8 +42,16 @@ abstract class AbstractBuildForm { // protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]"> + /** + * @var string + */ private $formId = null; + /** + * @var Sip + */ + private $sip = null; + /** * AbstractBuildForm constructor. * @@ -60,7 +68,7 @@ abstract class AbstractBuildForm { $this->evaluate = new Evaluate($this->store, $this->db); $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'); -// $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT); + $this->sip = $this->store->getSipInstance(); // render mode specific $this->fillWrap(); @@ -328,8 +336,8 @@ abstract class AbstractBuildForm { // Iterate over all FormElements foreach ($this->feSpecNative as $fe) { - if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord') - || ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord') + if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord') + || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord') // || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no') ) { continue; // skip this FE @@ -347,6 +355,11 @@ abstract class AbstractBuildForm { // Some Defaults $formElement = Support::setFeDefaults($formElement); + $label = ($formElement[F_BS_LABEL_COLUMNS] == '') ? $this->formSpec[F_BS_LABEL_COLUMNS] : $formElement[F_BS_LABEL_COLUMNS]; + $input = ($formElement[F_BS_INPUT_COLUMNS] == '') ? $this->formSpec[F_BS_INPUT_COLUMNS] : $formElement[F_BS_INPUT_COLUMNS]; + $note = ($formElement[F_BS_NOTE_COLUMNS] == '') ? $this->formSpec[F_BS_NOTE_COLUMNS] : $formElement[F_BS_NOTE_COLUMNS]; + $this->fillWrapLabelInputNote($label, $input, $note); + // Get default value $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], $storeUse, $formElement['checkType']) : $formElement['value']; @@ -356,7 +369,7 @@ abstract class AbstractBuildForm { $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId); // Construct Marshaller Name: buildElement - $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']]; + $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]]; $jsonElement = array(); // Render pure element @@ -384,7 +397,7 @@ abstract class AbstractBuildForm { } // Construct Marshaller Name: buildRow - $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']]; + $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]]; $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId); // break; @@ -396,6 +409,8 @@ abstract class AbstractBuildForm { return $html; } + abstract public function fillWrapLabelInputNote($label, $input, $note); + /** * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element. * @@ -551,7 +566,7 @@ abstract class AbstractBuildForm { $attribute .= $this->getAttributeMode($formElement); - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); return "$htmlTag $attribute>$textarea" . $this->getHelpBlock(); @@ -565,7 +580,7 @@ abstract class AbstractBuildForm { // MIN( $formElement['maxLength'], tabledefinition) $maxLength = $this->getColumnSize($formElement['name']); - switch ($formElement['type']) { + switch ($formElement[FE_TYPE]) { case 'date': $feMaxLength = 10; break; @@ -632,7 +647,7 @@ abstract class AbstractBuildForm { * Builds a HTML attribute list, based on $attributeList. * * E.g.: attributeList: [ 'type', 'autofocus' ] - * generates: 'type="$formElement['type']" autofocus="$formElement['autofocus']" ' + * generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement['autofocus']" ' * * @param array $formElement * @param array $attributeList @@ -699,7 +714,7 @@ abstract class AbstractBuildForm { } /** - * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode']. + * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE]. * * @param array $formElement * @return string @@ -708,7 +723,7 @@ abstract class AbstractBuildForm { private function getAttributeMode(array $formElement) { $attribute = ''; - switch ($formElement['mode']) { + switch ($formElement[FE_MODE]) { case 'show': break; case 'readonly': @@ -725,8 +740,8 @@ abstract class AbstractBuildForm { default: // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); - $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM); - throw new UserFormException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE); + $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_MODE, STORE_SYSTEM); + throw new UserFormException("Unknown mode '" . $formElement[FE_MODE] . "'", ERROR_UNKNOWN_MODE); break; } return $attribute; @@ -775,7 +790,7 @@ abstract class AbstractBuildForm { } $attributeBase = $this->getAttributeMode($formElement); - $attributeBase .= Support::doAttribute('type', $formElement['type']); + $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]); switch ($formElement['checkBoxMode']) { case 'single': @@ -788,7 +803,7 @@ abstract class AbstractBuildForm { throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN); } - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); // return Support::wrapTag('<div class="checkbox">', $html, true); return $html; } @@ -1084,7 +1099,7 @@ abstract class AbstractBuildForm { $attributeBase = $this->getAttributeMode($formElement); $attributeBase .= Support::doAttribute('name', $htmlFormElementId); - $attributeBase .= Support::doAttribute('type', $formElement['type']); + $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]); $attributeBase .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); $jj = 0; @@ -1111,8 +1126,8 @@ abstract class AbstractBuildForm { // $element = Support::wrapTag('<label>',$element); -// if(isset($this->feDivClass[$formElement['type']]) && $this->feDivClass[$formElement['type']] != '') { -// $element = Support::wrapTag('<div class="' . $this->feDivClass[$formElement['type']] .'">', $element); +// if(isset($this->feDivClass[$formElement[FE_TYPE]]) && $this->feDivClass[$formElement[FE_TYPE]] != '') { +// $element = Support::wrapTag('<div class="' . $this->feDivClass[$formElement[FE_TYPE]] .'">', $element); // } $html .= $element; @@ -1123,7 +1138,7 @@ abstract class AbstractBuildForm { } } - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); return $html; } @@ -1177,7 +1192,7 @@ abstract class AbstractBuildForm { $option .= '>' . $itemValue[$ii] . '</option>'; } - $json = $this->getJsonElementUpdate($htmlFormElementId, $jsonValues, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $jsonValues, $formElement[FE_MODE]); return '<select ' . $attribute . '>' . $option . '</select>' . $this->getHelpBlock(); } @@ -1200,6 +1215,7 @@ abstract class AbstractBuildForm { $flagEdit = false; $flagDelete = false; $linkNew = ''; + $control = array(); $primaryRecord = $this->store->getStore(STORE_RECORD); @@ -1219,12 +1235,16 @@ abstract class AbstractBuildForm { } } - // construct column attributes - $control = $this->getSubrecordColumnControl(array_keys($formElement['sql1'][0])); - - // Subrecord: Column titles $columns = $linkNew; - $columns .= '<th>' . implode('</th><th>', $control['title']) . '</th>'; + + if (isset($formElement['sql1'][0])) { + // construct column attributes + $control = $this->getSubrecordColumnControl(array_keys($formElement['sql1'][0])); + + // Subrecord: Column titles + $columns .= '<th>' . implode('</th><th>', $control['title']) . '</th>'; + } + if ($flagDelete) $columns .= '<th></th>'; @@ -1241,25 +1261,35 @@ abstract class AbstractBuildForm { // All columns foreach ($row as $columnName => $value) { - $rowHtml .= Support::wrapTag('<td>', $this->renderCell($control, $columnName, $value)); + if (isset($control['title'][$columnName])) + $rowHtml .= Support::wrapTag('<td>', $this->renderCell($control, $columnName, $value)); } if ($flagDelete) { -// $rowHtml .= Support::wrapTag('<td>', $this->createDeleteLink($targetTableName, $row['id'], $this->symbol[SYMBOL_DELETE], 'Delete', $this->showDebugInfo)); -// $this->createDeleteLink($targetTableName, $row['id'], $this->symbol[SYMBOL_DELETE], 'Delete', $this->showDebugInfo) - - // <button type="button" class="record-delete" data-sip={{SIP}}><span class="glyphicon glyphicon-trash"></span></button> $s = $this->createDeleteUrl($targetTableName, $row['id'], RETURN_SIP); $rowHtml .= Support::wrapTag('<td>', Support::wrapTag("<button type='button' class='record-delete' data-sip='$s'>", '<span class="glyphicon glyphicon-trash"></span>')); } - $html .= Support::wrapTag('<tr class="record">', $rowHtml, true); + Support::setIfNotSet($row, FE_SUBRECORD_ROW_CLASS); + $rowClass = 'record '; + $rowClass .= $row[FE_SUBRECORD_ROW_CLASS]; + + Support::setIfNotSet($row, FE_SUBRECORD_ROW_TITLE); + $rowTitle = $row[FE_SUBRECORD_ROW_TITLE]; + + $rowAttribute = Support::doAttribute('class', $rowClass); + $rowAttribute .= Support::doAttribute('title', $rowTitle); + $html .= Support::wrapTag("<tr $rowAttribute>", $rowHtml, true); } - return Support::wrapTag('<table class="table">', $html, true); + return Support::wrapTag('<table class="table table-hover">', $html, true); } /** + * Prepare Subrecord: + * - check if the current record has an recordId>0. If not, subrecord can't be edited. Return a message. + * - check if there is an SELECT statement for the subrecords. + * * @param $formElement * @param $primaryRecord * @param $rcText @@ -1281,14 +1311,14 @@ abstract class AbstractBuildForm { // No records? if (count($formElement['sql1']) == 0) { $rcText = ''; - return false; + return true; } if (!isset($formElement['sql1'][0][$nameColumnId])) $nameColumnId = '_id'; if (!isset($formElement['sql1'][0][$nameColumnId])) { - throw new UserFormException('Missing column \'id\' (or "@_id") in \'sql1\' Query', ERROR_DB_MISSING_COLUMN_ID); + throw new UserFormException('Missing column \'id\' (or "_id") in \'sql1\' Query', ERROR_DB_MISSING_COLUMN_ID); } return true; @@ -1326,7 +1356,7 @@ abstract class AbstractBuildForm { $detailParam = KeyValueStringParser::parse($formElement[SUBRECORD_PARAMETER_DETAIL]); foreach ($detailParam as $src => $dest) { // Constants - if ($src[0] == '#') { + if ($src[0] == '&') { $queryStringArray[$dest] = substr($src, 1); continue; } @@ -1396,6 +1426,15 @@ abstract class AbstractBuildForm { $control = array(); foreach ($titleRaw AS $columnName) { + + switch ($columnName) { + case FE_SUBRECORD_ROW_CLASS: + case FE_SUBRECORD_ROW_TITLE: + continue 2; + default: + break; + } + $flagWidthLimit = true; $control['width'][$columnName] = SUBRECORD_COLUMN_WIDTH; @@ -1450,6 +1489,14 @@ abstract class AbstractBuildForm { */ private function renderCell(array $control, $columnName, $value) { + switch ($columnName) { + case FE_SUBRECORD_ROW_CLASS: + case FE_SUBRECORD_ROW_TITLE: + return ''; + default: + break; + } + $arr = explode('|', $value); if (count($arr) == 1) $arr[1] = $arr[0]; @@ -1505,8 +1552,16 @@ abstract class AbstractBuildForm { public function buildFile(array $formElement, $htmlFormElementId, $value, &$json) { $attribute = ''; - $sip = $this->store->getVar(SIP_SIP, STORE_SIP); - $value = basename($value); // Strip directories + $arr = array(); + $arr['fake_uniq_never_use_this'] = uniqid(); // make sure we get a new SIP. This is needed for multiple forms (same user) with r=0 + $arr[CLIENT_SIP_FOR_FORM] = $this->store->getVar(SIP_SIP, STORE_SIP); + $arr[CLIENT_FE_NAME] = $formElement['name']; + $arr[CLIENT_FORM] = $this->formSpec['name']; + $arr[CLIENT_RECORD_ID] = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); + $arr[CLIENT_PAGE_ID] = 'fake'; + $sipUpload = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP); + + $hiddenSipUpload = $this->buildNativeHidden($htmlFormElementId, $sipUpload); $attribute .= Support::doAttribute('name', $htmlFormElementId); // $attribute .= Support::doAttribute('class', 'form-control'); @@ -1514,6 +1569,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute('title', $formElement['tooltip']); $attribute .= $this->getAttributeList($formElement, ['autofocus', 'accept']); $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); + $attribute .= Support::doAttribute('data-sip', $sipUpload); if ($value === '') { $textDeleteClass = 'hidden'; @@ -1521,23 +1577,23 @@ abstract class AbstractBuildForm { } else { $textDeleteClass = ''; $uploadClass = 'hidden'; - $formElement['mode'] = 'disabled'; + $formElement[FE_MODE] = 'disabled'; } $attribute .= $this->getAttributeMode($formElement); $attribute .= Support::doAttribute('class', $uploadClass, true); $htmlInputFile = '<input ' . $attribute . '>' . $this->getHelpBlock(); - $deleteButton = Support::wrapTag("<button class='delete-file' data-sip='$sip' name='trash-$htmlFormElementId'>", $this->symbol[SYMBOL_DELETE]); + $deleteButton = Support::wrapTag("<button class='delete-file' data-sip='$sipUpload' name='delete-$htmlFormElementId'>", $this->symbol[SYMBOL_DELETE]); $htmlFilename = Support::wrapTag("<span class='uploaded-file-name'>", $value, false); $htmlTextDelete = Support::wrapTag("<div class='uploaded-file $textDeleteClass'>", $htmlFilename . ' ' . $deleteButton); // <button type="button" class="file-delete" data-sip="571d1fc9e6974"><span class="glyphicon glyphicon-trash"></span></button> - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); - return $htmlTextDelete . $htmlInputFile; + return $htmlTextDelete . $htmlInputFile . $hiddenSipUpload; } /** @@ -1587,7 +1643,7 @@ abstract class AbstractBuildForm { $attribute .= $this->getAttributeMode($formElement); - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); return "$htmlTag $attribute>$textarea"; @@ -1615,11 +1671,11 @@ abstract class AbstractBuildForm { $arrMinMax = null; $this->adjustMaxLength($formElement); - $showTime = ($formElement['type'] == 'time' || $formElement['type'] == 'datetime') ? 1 : 0; + $showTime = ($formElement[FE_TYPE] == 'time' || $formElement[FE_TYPE] == 'datetime') ? 1 : 0; $value = Support::convertDateTime($value, $formElement[FE_DATE_FORMAT], $formElement[FE_SHOW_ZERO], $showTime, $formElement[FE_SHOW_SECONDS]); $tmpPattern = $formElement['checkPattern']; - $formElement['checkPattern'] = Support::dateTimeRegexp($formElement['type'], $formElement['dateFormat']); + $formElement['checkPattern'] = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement['dateFormat']); switch ($formElement['checkType']) { @@ -1642,11 +1698,11 @@ abstract class AbstractBuildForm { } // truncate if necessary - if ($value != '') { + if ($value != '' && $formElement['maxLength'] > 0) { $value = substr($value, 0, $formElement['maxLength']); } - $type = $formElement['type']; + $type = $formElement[FE_TYPE]; if ($type === 'datetime') $type = 'datetime-local'; @@ -1659,7 +1715,7 @@ abstract class AbstractBuildForm { if ($formElement['placeholder'] == '') { $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm'; - switch ($formElement['type']) { + switch ($formElement[FE_TYPE]) { case 'date': $placeholder = $formElement['dateFormat']; break; @@ -1670,7 +1726,7 @@ abstract class AbstractBuildForm { $placeholder = $timePattern; break; default: - throw new UserFormException("Unexpected Formelement type: '" . $formElement['type'] . "'", ERROR_FORMELEMENT_TYPE); + throw new UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE); } $formElement['placeholder'] = $placeholder; } @@ -1687,7 +1743,7 @@ abstract class AbstractBuildForm { $attribute .= $this->getAttributeMode($formElement); - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); return "<input $attribute>" . $this->getHelpBlock(); @@ -1771,7 +1827,7 @@ abstract class AbstractBuildForm { // restore parent processed FE's $this->feSpecNative = $tmpStore; - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']); + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); return $html; } diff --git a/extension/qfq/qfq/BodytextParser.php b/extension/qfq/qfq/BodytextParser.php index fd9899d07aa5f5a332d8af2a1160548e769123b1..706e69d2f9c889ed658085ff84c2600c07abe8a1 100644 --- a/extension/qfq/qfq/BodytextParser.php +++ b/extension/qfq/qfq/BodytextParser.php @@ -8,19 +8,30 @@ namespace qfq; +const NESTING_TOKEN_OPEN = '#&nesting-open-&#'; +const NESTING_TOKEN_CLOSE = '#&nesting-close&#'; +const NESTING_TOKEN_LENGTH = 17; class BodytextParser { + /** * @param $bodytext * @return mixed */ public function process($bodytext) { - $bodytext = $this->stripAndRemoveComment($bodytext); + $bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext); + // Encrypt double curly braces to prevent false positives with nesting: form = {{form}}\n + $bodytext = Support::encryptDoubleCurlyBraces($bodytext); + $bodytext = $this->encryptNestingDelimeter($bodytext); $bodytext = $this->joinLine($bodytext); $bodytext = $this->unNest($bodytext); - $bodytext = $this->stripAndRemoveComment($bodytext); + $bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext); + $bodytext = Support::decryptDoubleCurlyBraces($bodytext); + if (strpos($bodytext, NESTING_TOKEN_OPEN) !== false) { + throw new \qfq\UserFormException("Missing close delimiter: $bodytext", ERROR_MISSING_CLOSE_DELIMITER); + } return $bodytext; } @@ -31,7 +42,7 @@ class BodytextParser { * @return string */ - private function stripAndRemoveComment($bodytext) { + private function trimAndRemoveCommentAndEmptyLine($bodytext) { $data = array(); $src = explode(PHP_EOL, $bodytext); @@ -46,7 +57,22 @@ class BodytextParser { return implode(PHP_EOL, $data); } -//PREG_SPLIT_DELIM_CAPTURE + /** + * Encrypt '{\n' and '}\n' by more complex token. + * + * @param $bodytext + * @return mixed + */ + private function encryptNestingDelimeter($bodytext) { + // Take care that a trailing '}' will be recognised: add '\n' + if (substr($bodytext, -1) === '}') { + $bodytext .= "\n"; + } + + $bodytext = str_replace("{\n", NESTING_TOKEN_OPEN, $bodytext); + $bodytext = str_replace("}\n", NESTING_TOKEN_CLOSE, $bodytext); + return $bodytext; + } /** * Join lines, which do not begin with '<level>.<keyword>[ ]=' @@ -88,24 +114,28 @@ class BodytextParser { return implode(PHP_EOL, $data); } +//PREG_SPLIT_DELIM_CAPTURE + /** * @param $bodytext * @return mixed|string * @throws UserFormException */ private function unNest($bodytext) { + // Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested' - $bodytext = str_replace('\{', '#&[_#', $bodytext); - $bodytext = str_replace('\}', '#&]_#', $bodytext); - $bodytext = Support::encryptDoubleCurlyBraces($bodytext); +// $bodytext = str_replace('\{', '#&[_#', $bodytext); +// $bodytext = str_replace('\}', '#&]_#', $bodytext); +// $bodytext = Support::encryptDoubleCurlyBraces($bodytext); $result = $bodytext; - $posFirstClose = strpos($result, '}'); + $posFirstClose = strpos($result, NESTING_TOKEN_CLOSE); while ($posFirstClose !== false) { - $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), '{'); + $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN); if ($posMatchOpen === false) { + $result = $this->decryptNestingDelimeter($result); throw new \qfq\UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER); } @@ -113,12 +143,12 @@ class BodytextParser { if ($pre === false) $pre = ''; - $post = substr($result, $posFirstClose + 1); + $post = substr($result, $posFirstClose + NESTING_TOKEN_LENGTH); if ($post === false) $post = ''; // trim also removes '\n' - $match = trim(substr($result, $posMatchOpen + 1, $posFirstClose - $posMatchOpen - 1)); + $match = trim(substr($result, $posMatchOpen + NESTING_TOKEN_LENGTH, $posFirstClose - $posMatchOpen - NESTING_TOKEN_LENGTH)); // "10.sql = SELECT...\n20 {\n $levelStartPos = strrpos(trim($pre), PHP_EOL); @@ -138,14 +168,27 @@ class BodytextParser { } $result = $pre . $post; - $posFirstClose = strpos($result, '}'); + $posFirstClose = strpos($result, NESTING_TOKEN_CLOSE); } - $result = str_replace('#&[_#', '{', $result); - $result = str_replace('#&]_#', '}', $result); - $result = Support::decryptDoubleCurlyBraces($result); +// $result = str_replace('#&[_#', '{', $result); +// $result = str_replace('#&]_#', '}', $result); +// $result = Support::decryptDoubleCurlyBraces($result); return $result; } + /** + * Decrypt complex token by '{\n' and '}\n' + * + * @param $bodytext + * @return mixed + */ + private function decryptNestingDelimeter($bodytext) { + + $bodytext = str_replace(NESTING_TOKEN_OPEN, "{\n", $bodytext); + $bodytext = str_replace(NESTING_TOKEN_CLOSE, "}\n", $bodytext); + return $bodytext; + } + } \ No newline at end of file diff --git a/extension/qfq/qfq/BuildFormBootstrap.php b/extension/qfq/qfq/BuildFormBootstrap.php index 8b451db4f9b0ee9b90a8f9af5c01aca37bca33a4..e60a293388e51bd74a5e2082eec0f4670e968cd5 100644 --- a/extension/qfq/qfq/BuildFormBootstrap.php +++ b/extension/qfq/qfq/BuildFormBootstrap.php @@ -46,34 +46,41 @@ class BuildFormBootstrap extends AbstractBuildForm { */ public function fillWrap() { - // $this->wrap[WRAP_SETUP_OUTER][WRAP_SETUP_START] = '<div class="tab-content">'; // $this->wrap[WRAP_SETUP_OUTER][WRAP_SETUP_END] = '</div>'; - $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_START] = '<div class="row hidden-xs"><div class="col-md-12"><h1>'; - $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_END] = '</h1></div></div>'; + $this->wrap[WRAP_SETUP_TITLE][WRAP_SETUP_START] = "<div class='row hidden-xs'><div class='col-md-12'><h1>"; + $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_END] = '</div>'; - - $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START] = '<div class="col-md-2">'; - $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END] = '</div>'; - $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START] = '<div class="col-md-6">'; - $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END] = '</div>'; - $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START] = '<div class="col-md-4">'; - $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END] = '</div>'; + $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_START] = "<div class='form-group'>"; + $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_END] = "</div>"; - $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_START] = '<div class="col-md-12">'; - $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_END] = '</div>'; + $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_START] = "<div class='col-md-12'>"; + $this->wrap[WRAP_SETUP_SUBRECORD][WRAP_SETUP_END] = "</div>"; - $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START] = '<p>'; - $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END] = '</p>'; + $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START] = "<p>"; + $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END] = "</p>"; // $this->feDivClass['radio'] = 'radio'; // $this->feDivClass['checkbox'] = 'checkbox'; } + /** + * @param $label + * @param $input + * @param $note + */ + public function fillWrapLabelInputNote($label, $input, $note) { + $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_START] = "<div class='col-md-$label'>"; + $this->wrap[WRAP_SETUP_LABEL][WRAP_SETUP_END] = "</div>"; + $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_START] = "<div class='col-md-$input'>"; + $this->wrap[WRAP_SETUP_INPUT][WRAP_SETUP_END] = "</div>"; + $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_START] = "<div class='col-md-$note'>"; + $this->wrap[WRAP_SETUP_NOTE][WRAP_SETUP_END] = "</div>"; + + } + /** * @return string */ @@ -311,6 +318,9 @@ class BuildFormBootstrap extends AbstractBuildForm { $deleteUrl = $this->createDeleteUrl($this->formSpec['tableName'], $recordId); } + $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD; + $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE; + $html .= '</form>'; // <form class="form-horizontal" ... $html .= <<<EOF <script type="text/javascript"> @@ -318,14 +328,14 @@ class BuildFormBootstrap extends AbstractBuildForm { 'use strict'; QfqNS.Log.level = 0; - var qfqPage = new QfqNS.QfqPage({ tabsId: '$tabId', formId: '$formId', submitTo: 'typo3conf/ext/qfq/qfq/api/save.php', deleteUrl: '$deleteUrl', refreshUrl: "typo3conf/ext/qfq/qfq/api/load.php", - fileUploadTo: 'typo3conf/ext/qfq/qfq/api/file.php' + fileUploadTo: 'typo3conf/ext/qfq/qfq/api/file.php?$actionUpload', + fileDeleteUrl: 'typo3conf/ext/qfq/qfq/api/file.php?$actionDelete' }); var qfqRecordList = new QfqNS.QfqRecordList('typo3conf/ext/qfq/qfq/api/delete.php'); diff --git a/extension/qfq/qfq/BuildFormPlain.php b/extension/qfq/qfq/BuildFormPlain.php index 78b76d0da88a80cdd7c61abe10a509a3b410a878..5558eea537978c06e6e6b30b8590d1adccfae82d 100644 --- a/extension/qfq/qfq/BuildFormPlain.php +++ b/extension/qfq/qfq/BuildFormPlain.php @@ -41,6 +41,9 @@ class BuildFormPlain extends AbstractBuildForm { } + public function fillWrapLabelInputNote($label, $input, $note) { + + } /** * @return string */ @@ -67,7 +70,7 @@ class BuildFormPlain extends AbstractBuildForm { $html = ''; // Construct Marshaller Name -// $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']]; +// $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]]; if($formElement['nestedInFieldSet']==='no') diff --git a/extension/qfq/qfq/BuildFormTable.php b/extension/qfq/qfq/BuildFormTable.php index c3977fc63396a3e0431064783a6d324b52ae244a..6964eb9a8863ab26f2b6225956cee80c11bc7b9d 100644 --- a/extension/qfq/qfq/BuildFormTable.php +++ b/extension/qfq/qfq/BuildFormTable.php @@ -45,6 +45,9 @@ class BuildFormTable extends AbstractBuildForm { } + public function fillWrapLabelInputNote($label, $input, $note) { + + } /** * @return string */ @@ -94,9 +97,9 @@ class BuildFormTable extends AbstractBuildForm { $html = ''; // Construct Marshaller Name - $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']]; + $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]]; - if ($formElement['type'] === 'subrecord') { + if ($formElement[FE_TYPE] === 'subrecord') { // subrecord in render='table' are outside the table $html .= $this->wrapItem(WRAP_SETUP_SUBRECORD, $formElement['label']); $html .= $this->wrapItem(WRAP_SETUP_SUBRECORD, $htmlElement); diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 285ab1b7cd9803895bc52078c4d1c4726c0644ed..c5eb3e0ba27d5ee5c97a7a9faaff0518a3674327 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -33,6 +33,10 @@ const FORM_FORWARD_MODE_NO = 'no'; const FORM_FORWARD_MODE_AUTO = 'auto'; const FORM_FORWARD_MODE_PAGE = 'page'; +const F_BS_LABEL_COLUMNS = 'bsLabelColumns'; +const F_BS_INPUT_COLUMNS = 'bsInputColumns'; +const F_BS_NOTE_COLUMNS = 'bsNoteColumns'; + const SESSION_FE_USER_UID = 'fe_user_uid'; const RETURN_URL = 'return_url'; @@ -101,7 +105,6 @@ const ERROR_USER_LOGGED_IN = 1018; const ERROR_FORM_FORBIDDEN = 1019; const ERROR_FORM_UNKNOWN_PERMISSION_MODE = 10120; const ERROR_MULTI_SQL_MISSING = 1021; -const ERROR_MISSING_OPEN_DELIMITER = 1022; const ERROR_RECURSION_TOO_DEEP = 1023; const ERROR_CHECKBOXMODE_UNKNOWN = 1024; const ERROR_MISSING_SQL1 = 1025; @@ -134,6 +137,8 @@ const ERROR_REQUIRED_VALUE_EMPTY = 1055; const ERROR_DATE_UNEXPECTED_FORMAT = 1056; const ERROR_NOT_APPLICABLE = 1057; const ERROR_FORMELEMENT_TYPE = 1058; +const ERROR_MISSING_OPEN_DELIMITER = 1059; +const ERROR_MISSING_CLOSE_DELIMITER = 1060; // Store const ERROR_STORE_VALUE_ALREADY_CODPIED = 1100; @@ -143,6 +148,12 @@ const ERROR_STORE_KEY_EXIST = 1101; const ERROR_IO_READ_FILE = 1200; const ERROR_IO_WRITE = 1203; const ERROR_IO_OPEN = 1204; +const ERROR_IO_UNLINK = 1205; +const ERROR_IO_FILE_EXIST = 1206; +const ERROR_IO_RENAME = 1207; +const ERROR_IO_INVALID_LINK = 1208; +const ERROR_IO_DIR_EXIST_AS_FILE = 1209; +const ERROR_IO_CHDIR = 1210; //Report const ERROR_UNKNOWN_LINK_QUALIFIER = 1300; @@ -151,6 +162,11 @@ const ERROR_MISSING_VALUE = 1302; const ERROR_MULTIPLE_DEFINITION = 1303; const ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION = 1304; +// Upload +const ERROR_UPLOAD = 1400; +const ERROR_UNKNOWN_ACTION = 1402; +const ERROR_NO_TARGET_PATH_FILE_NAME = 1403; + // KeyValueParser const ERROR_KVP_VALUE_HAS_NO_KEY = 1900; @@ -201,8 +217,11 @@ const CLIENT_KEY_SEM_ID_USER = 'keySemIdUser'; const CLIENT_PAGE_ID = 'id'; const CLIENT_PAGE_TYPE = 'type'; const CLIENT_PAGE_LANGUAGE = 'L'; -const CLIENT_UPLOAD_FE_NAME = 'uploadFeName'; -const CLIENT_UPLOAD_DELETE = 'uploadDeleteOld'; +const CLIENT_UPLOAD_FE_NAME = 'name'; + +const CLIENT_SIP_FOR_FORM = '_sipForForm'; +const CLIENT_FE_NAME = '_feName'; +const CLIENT_UPLOAD_FILENAME = '_filename'; // ALL $_SERVER variables: http://php.net/manual/en/reserved.variables.server.php // The following exist and might be the most used ones. @@ -221,9 +240,6 @@ const CLIENT_REQUEST_URI = 'REQUEST_URI'; const CLIENT_SCRIPT_NAME = 'SCRIPT_NAME'; const CLIENT_PHP_SELF = 'PHP_SELF'; -// Extra: -const EXTRA_UPLOAD_DELETE = CLIENT_UPLOAD_DELETE; - // T3 Bodytext Keywords const TYPO3_FORM = CLIENT_FORM; const TYPO3_RECORD_ID = CLIENT_RECORD_ID; @@ -254,6 +270,7 @@ const SYSTEM_CSS_CLASS_QFQ_CONTAINER = 'CSS_CLASS_QFQ_CONTAINER'; // computed automatically during runtime const SYSTEM_PATH_EXT = 'EXT_PATH'; +const SYSTEM_SITE_PATH = 'SITE_PATH'; // Information for: Log / Debug / Exception const SYSTEM_SQL_RAW = 'sqlRaw'; // Type: SANITIZE_ALL / String. SQL Query (before substitute). Useful for error reporting. @@ -348,16 +365,20 @@ const GLYPH_ICON_CHECK = 'glyphicon glyphicon-ok'; const FE_MODE_SHOW = 'show'; const FE_MODE_READONLY = 'readonly'; const FE_MODE_REQUIRED = 'required'; -const FE_MODE_LOCK = 'lock'; const FE_MODE_DISABLED = 'disabled'; +const FE_SUBRECORD_ROW_CLASS = '_rowClass'; +const FE_SUBRECORD_ROW_TITLE = '_rowTitle'; + // FormElement columns: real const FE_TYPE = 'type'; +const FE_MODE = 'mode'; // FormElement columns: via parameter field const FE_DATE_FORMAT = 'dateFormat'; // value: FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN const FE_SHOW_SECONDS = 'showSeconds'; // value: 0|1 const FE_SHOW_ZERO = 'showZero'; // value: 0|1 +const FE_PATH_FILE_NAME = 'pathFileName'; // Target pathFilename for an uploaded file. // SUPPORT const PARAM_T3_ALL = 't3 all'; @@ -380,3 +401,21 @@ const QUERY_TYPE_UPDATE = 'type: update,replace,delete'; // Date/ DateTime formats const FORMAT_DATE_INTERNATIONAL = 'yyyy-mm-dd'; const FORMAT_DATE_GERMAN = 'dd.mm.yyyy'; + +// $_FILES +const FILES_NAME = 'name'; +const FILES_TMP_NAME = 'tmp_name'; +const FILES_ERROR = 'error'; +const FILES_SIZE = 'size'; +const FILES_FLAG_DELETE = 'flagDelete'; + +const UPLOAD_CACHED = '.cached'; +const FILE_ACTION = 'action'; +const FILE_ACTION_UPLOAD = 'upload'; +const FILE_ACTION_DELETE = 'delete'; + +// DATABASE +const DB_NUM_ROWS = 'numRows'; +const DB_AFFECTED_ROWS = 'affectedRows'; +const DB_INSERT_ID = 'insertId'; + diff --git a/extension/qfq/qfq/Database.php b/extension/qfq/qfq/Database.php index 326245503ac164be7a3e51218f14b4babc88fdac..9d2fb57f37daacaeebe029c20b1d69d6cc52e1f4 100644 --- a/extension/qfq/qfq/Database.php +++ b/extension/qfq/qfq/Database.php @@ -208,7 +208,7 @@ class Database { * @throws \qfq\CodeException * @throws \qfq\DbException */ - public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '', array &$keys = array()) { + public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '', array &$keys = array(), array &$stat = array()) { $queryType = ''; $result = array(); $this->closeMysqliStmt(); @@ -221,7 +221,8 @@ class Database { if ($specificMessage) $specificMessage .= " "; - $count = $this->prepareExecute($sql, $parameterArray, $queryType); + $count = $this->prepareExecute($sql, $parameterArray, $queryType, $stat); + if ($count === false) { throw new DbException($specificMessage . "No idea why this error happens - please take some time and check this: $sql", ERROR_DB_GENERIC_CHECK); } @@ -297,18 +298,20 @@ class Database { /** * Execute a prepared SQL statement like SELECT, INSERT, UPDATE, DELETE, SHOW, ... * - * Returns the number of selected rows (SELECT, SHOW, ..) or the affected rows (UPDATE) or the last insert id (INSERT) + * Returns the number of selected rows (SELECT, SHOW, ..) or the affected rows (UPDATE, INSERT). $stat contains appropriate num_rows, insert_id or rows_affected. * * @param string $sql SQL statement with prepared statement variable. * @param array $parameterArray parameter array for prepared statement execution. * @param string $queryType returns QUERY_TYPE_SELECT | QUERY_TYPE_UPDATE | QUERY_TYPE_INSERT, depending on the query. + * @param array $stat * @return int|mixed * @throws \qfq\CodeException * @throws \qfq\DbException * @throws \qfq\UserFormException */ - private function prepareExecute($sql, array $parameterArray = array(), &$queryType = '') { + private function prepareExecute($sql, array $parameterArray = array(), &$queryType, array &$stat) { $result = 0; + $stat = array(); $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); $this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM); @@ -343,19 +346,23 @@ class Database { } $queryType = QUERY_TYPE_SELECT; $this->mysqli_result = $result; - $count = $this->mysqli_result->num_rows; - $msg = 'Read rows: ' . $count; + $stat[DB_NUM_ROWS] = $this->mysqli_result->num_rows; + $count = $stat[DB_NUM_ROWS]; + $msg = 'Read rows: ' . $stat[DB_NUM_ROWS]; break; case 'INSERT': $queryType = QUERY_TYPE_INSERT; - $count = $this->mysqli->insert_id; + $stat[DB_INSERT_ID] = $this->mysqli->insert_id; + $stat[DB_AFFECTED_ROWS] = $this->mysqli->affected_rows; + $count = $stat[DB_AFFECTED_ROWS]; $msg = 'ID: ' . $count; break; case 'UPDATE': case 'REPLACE': case 'DELETE': $queryType = QUERY_TYPE_UPDATE; - $count = $this->mysqli->affected_rows; + $stat[DB_AFFECTED_ROWS] = $this->mysqli->affected_rows; + $count = $stat[DB_AFFECTED_ROWS]; $msg = 'Affected rows: ' . $count; break; default: @@ -508,12 +515,14 @@ class Database { * * @param $sql * @param array $keys + * @param array $stat * @return array|bool + * @throws \qfq\CodeException * @throws \qfq\DbException */ - public function sqlKeys($sql, array &$keys) { + public function sqlKeys($sql, array &$keys, array &$stat = array()) { - return $this->sql($sql, ROW_KEYS, array(), '', $keys); + return $this->sql($sql, ROW_KEYS, array(), '', $keys, $stat); } /** diff --git a/extension/qfq/qfq/Evaluate.php b/extension/qfq/qfq/Evaluate.php index 2b230d6e34382f6b2626b6be15c88b7d86b78a00..3d815149acb93376d65be14b2c9e9c3e41ee278f 100644 --- a/extension/qfq/qfq/Evaluate.php +++ b/extension/qfq/qfq/Evaluate.php @@ -60,7 +60,7 @@ class Evaluate { } /** - * Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them. + * Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them. All queries will be fired. * * Token to replace have to be enclosed by '{{' and '}}' * diff --git a/extension/qfq/qfq/File.php b/extension/qfq/qfq/File.php index adfc5c9d39a3d26f44206e7073b5d958a47878dc..bab7b520f5adf42da951fc3b7a5d42da2a02951a 100644 --- a/extension/qfq/qfq/File.php +++ b/extension/qfq/qfq/File.php @@ -13,35 +13,98 @@ require_once(__DIR__ . '/Constants.php'); class File { + private $uploadErrMsg = array(); + /** - * @var null|Store + * @var Store */ private $store = null; public function __construct($phpUnit = false) { $this->store = Store::getInstance('', $phpUnit); + +// $sessionName = $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM); +// $this->sip = new Sip($sessionName); + + $this->uploadErrMsg = [ + UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini", + UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form", + UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded", + UPLOAD_ERR_NO_FILE => "No file was uploaded", + UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder", + UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk", + UPLOAD_ERR_EXTENSION => "File upload stopped by extension" + ]; } + /** + * @throws UserFormException + */ public function process() { - $sClient = $this->store->getVar(CLIENT_SIP, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); - $sStore = $this->store->getVar(SIP_SIP, STORE_SIP); - if ($sClient === false || $sStore === false || $sClient !== $sStore) { - throw new UserFormException('SIP invalid', ERROR_SIP_INVALID); + $sipUpload = $this->store->getVar(SIP_SIP, STORE_SIP); + if ($sipUpload === false) { + throw new UserFormException('SIP invalid: ' . $sipUpload, ERROR_SIP_INVALID); } + $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA, SANITIZE_ALLOW_ALL); + if ($statusUpload === false) { + $statusUpload = array(); + } + + $action = $this->store->getVar(FILE_ACTION, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); + switch ($action) { + case FILE_ACTION_UPLOAD: + $this->doUpload($sipUpload, $statusUpload); + break; + case FILE_ACTION_DELETE: + $this->doDelete($sipUpload, $statusUpload); + break; + default: + throw new UserFormException("Unknown FILE_ACTION: $action", ERROR_UNKNOWN_ACTION); + } + } + + /** + * @param string $keyStoreExtra + * @throws CodeException + * @throws UserFormException + */ + private function doUpload($sipUpload, $statusUpload) { - $uploadFeName = $this->store->getVar(CLIENT_UPLOAD_FE_NAME, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); - $uploadDelete = $this->store->getVar(CLIENT_UPLOAD_DELETE, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); + list($dummy, $newArr) = each($_FILES); + $statusUpload = array_merge($statusUpload, $newArr); - $newArr = $_FILES; - $oldArr = $this->store->getVar($uploadFeName . STORE_EXTRA, SANITIZE_ALLOW_ALL); - if (isset($oldArr[EXTRA_UPLOAD_DELETE]) && $oldArr[EXTRA_UPLOAD_DELETE] === 'yes') { - $uploadDelete = 'yes'; + if ($statusUpload[FILES_ERROR] !== UPLOAD_ERR_OK) { + throw new UserFormException($this->uploadErrMsg[$newArr[FILES_ERROR]], ERROR_UPLOAD); } - if ($uploadDelete === 'yes') - $newArr[EXTRA_UPLOAD_DELETE] = 'yes'; + //TODO: do necessary checks with uploaded file HERE!!! + + + // rename uploaded file: ?.cached + $filenameCached = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); + move_uploaded_file($newArr[FILES_TMP_NAME], $filenameCached); + + $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA); + } - $this->store->setVar($uploadFeName, $newArr, STORE_EXTRA); + /** + * @param string $keyStoreExtra + * @throws CodeException + * @throws UserFormException + */ + private function doDelete($sipUpload, $statusUpload) { + + if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') { + $file = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); + if (file_exists($file)) { + if (!unlink($file)) { + throw new UserFormException('unlink file: ' . $file, ERROR_IO_UNLINK); + } + } + $statusUpload[FILES_TMP_NAME] = ''; + } + $statusUpload[FILES_FLAG_DELETE] = '1'; + $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA); } } \ No newline at end of file diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index 378d5f47d050b7ca358c9584ac15a63543a66baf..38f81fc0a04e32326b93d45223b0a74e2c6f74b7 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -225,12 +225,14 @@ class QuickFormQuery { break; case FORM_SAVE: + // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten. + $this->fillStoreRecord($this->formSpec['tableName'], $this->store->getVar(SIP_RECORD_ID, STORE_SIP)); + $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative); $rc = $save->process(); // Reload fresh saved record and fill STORE_RECORD with it - $record = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, [$rc]); - $this->store->setVarArray($record, STORE_RECORD, true); + $this->fillStoreRecord($this->formSpec['tableName'], $rc); $htmlElementNameIdZero = false; // Retrieve current STORE_SIP. @@ -292,8 +294,10 @@ class QuickFormQuery { HelperFormElement::explodeParameter($this->formSpec); # Set defaults: - if (!isset($this->formSpec['class'])) - $this->formSpec['class'] = ''; + Support::setIfNotSet($this->formSpec, 'class', ''); + Support::setIfNotSet($this->formSpec, F_BS_LABEL_COLUMNS, 3, ''); + Support::setIfNotSet($this->formSpec, F_BS_INPUT_COLUMNS, 8, ''); + Support::setIfNotSet($this->formSpec, F_BS_NOTE_COLUMNS, 1, ''); // Clear $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM); @@ -435,6 +439,21 @@ class QuickFormQuery { return $sipFound; } + /** + * @param string $table + * @param string $recordId + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + private function fillStoreRecord($table, $recordId) { + if ($recordId !== false && $recordId > 0) { + $record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]); + $this->store->setVarArray($record, STORE_RECORD, true); + } + + } + /** * @param $sipArray * @param $recordId diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index 033917a74ec0b0bccc38c01f421822c7219ac727..d2bcbb5f3c257d429b6ae6d9cc179d7af800ccaf 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -62,7 +62,8 @@ class Save { $rc = $this->elements($row['_id']); } } else { - $rc = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO)); + $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO); + $rc = $this->elements($recordId); } return $rc; @@ -82,8 +83,11 @@ class Save { $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES)); $formValues = $this->store->getStore(STORE_FORM); + $this->processAllUploads($formValues); + // Iterate over all table.columns. Built an assoc array $newValues. foreach ($tableColumns AS $column) { + // Never save a predefined 'id': autoincrement values will be given by database.. if ($column === 'id') continue; @@ -93,14 +97,21 @@ class Save { if ($formElement === false) continue; + // Some modes means: do not save this column. + switch ($formElement[FE_MODE]) { + case FE_MODE_READONLY: + case FE_MODE_DISABLED: + continue 2; // 1 for switch, 2 for continue foreach. + default: + break; + } + + // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); - if (isset($formValues[$column])) { - $newValues[$column] = $formValues[$column]; - } else { - $newValues[$column] = ''; - } + Support::setIfNotSet($formValues, $column); + $newValues[$column] = $formValues[$column]; } if ($recordId == 0) { @@ -113,6 +124,114 @@ class Save { return $rc; } + /** + * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the final filenames. + * + * @param array $formValues + */ + private function processAllUploads(array &$formValues) { + + foreach ($this->feSpecNative AS $formElement) { + // skip non upload formElements + if ($formElement[FE_TYPE] != 'upload') { + continue; + } + + // Preparation for Log, Debug + $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); + + $column = $formElement['name']; + $file = $this->doUpload($formElement, $formValues[$column]); + if ($file !== false) { + $formValues[$column] = $file; + } + } + } + + /** + * Process upload for the given Formelement. If necessary, delete a previous uploaded file. + * Calculate the final path/filename and move the file to the new location. + * + * @param $formElement + * @param $sipUpload + * @return string|false New filename or false on error + * @throws CodeException + * @throws UserFormException + * @internal param $recordId + */ + private function doUpload($formElement, $sipUpload) { + + + // Status information about upload file + $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); + if ($statusUpload === false) { + return false; + } + + // Take care the necessary target directories exist. + $cwd = getcwd(); + $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM); + if ($cwd === false || $sitePath === false || !chdir($sitePath)) { + throw new UserFormException("getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed.", ERROR_IO_CHDIR); + } + + // Delete existing old file. + if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') { + $oldFile = $this->store->getVar($formElement['name'], STORE_RECORD); + if (file_exists($oldFile)) { + if (!unlink($oldFile)) { + throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK); + } + } + } + + $pathFileName = $this->copyUploadFile($formElement, $statusUpload); + + chdir($cwd); + + // Delete current used uniq SIP + $this->store->setVar($sipUpload, array(), STORE_EXTRA); + + return $pathFileName; + + } + + /** + * @param array $formElement + * @param array $statusUpload + * @return array|mixed|null|string + * @throws CodeException + * @throws UserFormException + */ + private function copyUploadFile(array $formElement, array $statusUpload) { + $pathFileName = ''; + + if (isset($formElement[FE_PATH_FILE_NAME])) { + + // Provide variable '_filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME]. + $origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]); + $this->store->setVar(CLIENT_UPLOAD_FILENAME, $origFilename, STORE_FORM); + + $pathFileName = $this->evaluate->parse($formElement[FE_PATH_FILE_NAME]); + } + + if ($pathFileName === '') { + throw new UserFormException("Upload failed, no target '" . FE_PATH_FILE_NAME . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME); + } + + if (file_exists($pathFileName)) { + throw new UserFormException('Copy upload failed - file already exist: ' . $pathFileName, ERROR_IO_FILE_EXIST); + } + + Support::mkDirParent($pathFileName); + + $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); + if (!rename($srcFile, $pathFileName)) { + throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME); + } + + return $pathFileName; + } /** * Get the complete FormElement for $name * diff --git a/extension/qfq/qfq/helper/Sanitize.php b/extension/qfq/qfq/helper/Sanitize.php index aefdfb766f7ac486b2f45e01df9bb60d3d7968f3..67e48f248daf370e6048defade3d8ae1b0493ebf 100644 --- a/extension/qfq/qfq/helper/Sanitize.php +++ b/extension/qfq/qfq/helper/Sanitize.php @@ -101,6 +101,7 @@ class Sanitize { case SANITIZE_ALLOW_DIGIT: case SANITIZE_ALLOW_EMAIL: case SANITIZE_ALLOW_ALNUMX: + case SANITIZE_ALLOW_ALLBUT: $arr = self::inputCheckPatternArray(); $pattern = $arr[$sanatizeClass]; break; @@ -133,8 +134,7 @@ class Sanitize { /** * @return array */ - public - static function inputCheckPatternArray() { + public static function inputCheckPatternArray() { //EMail Regex: http://www.regular-expressions.info/email.html return [ SANITIZE_ALLOW_ALNUMX => '^[@\-_\.,;: \/\(\)a-zA-Z0-9]*$', // ':alnum:' does not work here in FF @@ -148,4 +148,34 @@ class Sanitize { ]; } + + /** + * Sanatizes a filename. Copied from http://www.phpit.net/code/filename-safe/ + * + * @param $filename + * @return mixed + */ + public static function safeFilename($filename) { + $search = array( + // Definition of German Umlauts START + '/ß/', + '/ä/', '/Ä/', + '/ö/', '/Ö/', + '/ü/', '/Ü/', + // Definition of German Umlauts ENDE + '([^[:alnum:]._])' // Disallow: Not alphanumeric, dot or underscore + ); + + $replace = array( + 'ss', + 'ae', 'Ae', + 'oe', 'Oe', + 'ue', 'Ue', + '_' + ); + + return preg_replace($search, $replace, $filename); + } // safeFilename() + + } \ No newline at end of file diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index 89126a27a9de0abfc23cebbce2c914827aab67bb..f3c1446a2976fcc11196a1d9a3f6f1dc977a0f97 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -63,8 +63,8 @@ class Support { /** * Extract Tag from $tag (eg: <input class="form-control">, might contain further attributes) and wrap it around $value. If $flagOmitEmpty==true && $value=='': return ''. * - * @param $tag - * @param $value + * @param string $tag + * @param string $value * @param bool|false $omitIfValueEmpty * @return string */ @@ -84,8 +84,8 @@ class Support { /** * Format's an attribute: $type=$value. If $flagOmitEmpty==true && $value=='': return ''. * - * @param $type - * @param $value + * @param string $type + * @param string $value * @param bool $flagOmitEmpty * @return string */ @@ -101,8 +101,8 @@ class Support { * * Based on: http://www.w3schools.com/howto/howto_css_tooltip.asp * - * @param $before - * @param $tooltipText + * @param string $before + * @param string $tooltipText * @return string */ public static function appendTooltip($before, $tooltipText) { @@ -118,8 +118,8 @@ class Support { * * Returns the false if not found or index of found place. Be carefull: use unary operator to compare for 'false' * - * @param $needle - * @param $haystack + * @param string $needle + * @param string $haystack * @return boolean true if found, else false */ public static function findInSet($needle, $haystack) { @@ -135,7 +135,7 @@ class Support { * 01.02.13 3:24 > 1979-02-01 03:24:00 * 1.2.1979 14:21:5 > 1979-02-01 14:21:05 * - * @param $dateTimeString + * @param string $dateTimeString * @return string * @throws UserFormException */ @@ -222,7 +222,7 @@ class Support { /** * @param string $type date | datetime | time * @param string $format FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN - * @param $showSeconds + * @param string $showSeconds * @return string * @throws UserFormException */ @@ -262,10 +262,10 @@ class Support { * Returned value will be 'date only', 'datetime' oder 'time only', depending on the input value. * * @param string $dateTimeString - * @param $dateFormat - * @param $showZero - * @param $showTime - * @param $showSeconds + * @param string $dateFormat + * @param string $showZero + * @param string $showTime + * @param string $showSeconds * @return string * @throws UserFormException */ @@ -383,7 +383,7 @@ class Support { /** * Split date FORMAT_DATE_GERMAN | FORMAT_DATE_INTERNATIONAL to array with arr[0]=yyyy, arr[1]=mm, arr[2]=dd. * - * @param $dateString + * @param string $dateString * @return array * @throws UserFormException */ @@ -425,7 +425,7 @@ class Support { public static function getDateTimePlaceholder(array $formElement) { $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm'; - switch ($formElement['type']) { + switch ($formElement[FE_TYPE]) { case 'date': $placeholder = $formElement['dateFormat']; break; @@ -436,7 +436,7 @@ class Support { $placeholder = $timePattern; break; default: - throw new UserFormException("Unexpected Formelement type: '" . $formElement['type'] . "'", ERROR_FORMELEMENT_TYPE); + throw new UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE); } return $placeholder; @@ -446,7 +446,7 @@ class Support { /** * Encrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces. * - * @param $text + * @param string $text * @return mixed */ public @@ -460,7 +460,7 @@ class Support { /** * Decrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces * - * @param $text + * @param string $text * @return mixed */ public static function decryptDoubleCurlyBraces($text) { @@ -485,8 +485,8 @@ class Support { } /** - * @param $url - * @param $param + * @param string $url + * @param string $param * @return string */ public static function concatUrlParam($url, $param) { @@ -516,14 +516,66 @@ class Support { } /** - * @param $arr - * @param $index + * @param array $arr + * @param string $index * @param string $value + * @param string|bool $overwriteThis If there is already something which is equal to $overwrite: take new default. */ - public static function setIfNotSet(&$arr, $index, $value = '') { + public static function setIfNotSet(array &$arr, $index, $value = '', $overwriteThis = false) { - if (!isset($arr[$index])) + if (!isset($arr[$index])) { $arr[$index] = $value; + } + + if ($overwriteThis !== false && $arr[$index] === $overwriteThis) { + $arr[$index] = $value; + } } + /** + * @param string $filename + * @param string $extend + * @return string + */ + public static function extendFilename($filename, $extend) { + return $filename . $extend; + } + + /** + * Creates all necessary directories in $pathFileName, but not the last part, the filename. A filename has to be specified. + * + * @param string $pathFileName Path with Filename + * @throws UserFormException + */ + public static function mkDirParent($pathFileName) { + $path = ""; + + // Teile "Directory/File.Extension" auf + $pathParts = pathinfo($pathFileName); + + // Zerlege Pfad in einzelne Directories + $arr = explode("/", $pathParts["dirname"]); + + // Durchlaufe die einzelnen Dirs und überprüfe ob sie angelegt sind. + // Wenn nicht, lege sie an. + foreach ($arr as $part) { + $path .= $part; + + // Check ob der Pfad ein Link ist + if ("link" == @filetype($path)) { + if ("0" == ($path1 = readlink($path))) { + throw new UserFormException("Can't create '$pathFileName': '$path' contains an invalid link.", ERROR_IO_INVALID_LINK); + } + } else { + if (file_exists($path)) { + if ("dir" != filetype($path)) { + throw new UserFormException("Can't create '$pathFileName': There is already a file with the same name as '$path'", ERROR_IO_DIR_EXIST_AS_FILE); + } + } else + mkdir($path, 0700); + } + + $path .= "/"; + } + } } \ No newline at end of file diff --git a/extension/qfq/qfq/report/Report.php b/extension/qfq/qfq/report/Report.php index f38d55f62ff40274d37c40ff8eb2901881498179..4f73ad1d2413fc4b01d6a17a08a7ed7f36aa9e24 100644 --- a/extension/qfq/qfq/report/Report.php +++ b/extension/qfq/qfq/report/Report.php @@ -316,6 +316,7 @@ class Report { private function triggerReport($cur_level = 1, array $super_level_array = array(), $counter = 0) { $keys = array(); + $stat = array(); $lineDebug = 0; $content = ""; @@ -370,15 +371,17 @@ class Report { //Execute SQL. All errors have been already catched. unset($result); - $result = $this->db->sqlKeys($sql, $keys); + $result = $this->db->sqlKeys($sql, $keys, $stat); // If an array is returned, $sql was a query, otherwise an 'insert', 'update', 'delete', ... // Query: total nummber of rows - // insert: last_insert_id - // delete, update: number of affected rows - $rowTotal = is_array($result) ? count($result) : $result; + // insert, delete, update: number of affected rows + $rowTotal = isset($stat[DB_NUM_ROWS]) ? $stat[DB_NUM_ROWS] : $stat[DB_AFFECTED_ROWS]; $this->variables->resultArray[$full_level . ".line."]["total"] = $rowTotal; + if (isset($stat[DB_INSERT_ID])) { + $this->variables->resultArray[$full_level . ".line."]["insertId"] = $stat[DB_INSERT_ID]; + } // HEAD: If there is at least one record, do 'head'. if ($rowTotal > 0) @@ -527,7 +530,7 @@ class Report { $flagControl = true; $columnName = substr($columnName, 1); } - + //TODO: reserved names,not starting with '_' will be still accepted - stop this! switch ($columnName) { case "link": $link = new Link($this->fr_error, $this->sip); @@ -741,7 +744,8 @@ class Report { break; } - $this->variables->resultArray[$full_level . "."][$columnName] = $content; + // Always save column values, even if they are hidden. + $this->variables->resultArray[$full_level . "."][$columnName] = ($content == '' && $flagControl) ? $columnValue : $content; return $content; } diff --git a/extension/qfq/qfq/store/FillStoreForm.php b/extension/qfq/qfq/store/FillStoreForm.php index 24d3ab92de93098e6459fd93445bd2792ce46dd6..72f3f70927e60ae06f48497b118f5b3da796e659 100644 --- a/extension/qfq/qfq/store/FillStoreForm.php +++ b/extension/qfq/qfq/store/FillStoreForm.php @@ -120,7 +120,7 @@ class FillStoreForm { // Preparation for Log, Debug // $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM); - if ($formElement['type'] == 'hidden') { + if ($formElement[FE_TYPE] == 'hidden') { // Hidden elements will be transferred by SIP if (!isset($sipValues[$formElement['name']])) { throw new CodeException("Missing the hidden field '" . $formElement['name'] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP); @@ -130,13 +130,13 @@ class FillStoreForm { continue; } - if ($formElement['mode'] === FE_MODE_REQUIRED) { + if ($formElement[FE_MODE] === FE_MODE_REQUIRED) { if (!isset($clientValues[$clientFieldName]) || ($clientValues[$clientFieldName] === '')) { throw new UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY); } } - switch ($formElement['mode']) { + switch ($formElement[FE_MODE]) { case FE_MODE_REQUIRED: case FE_MODE_SHOW: if (isset($clientValues[$clientFieldName])) { @@ -151,7 +151,7 @@ class FillStoreForm { $clientValues[$clientFieldName] = implode(',', $clientValues[$clientFieldName]); } - switch ($formElement['type']) { + switch ($formElement[FE_TYPE]) { case 'date': case 'datetime': case 'time': @@ -167,11 +167,10 @@ class FillStoreForm { break; case FE_MODE_READONLY: - case FE_MODE_LOCK: case FE_MODE_DISABLED: continue; default: - throw new CodeException("Unknown mode: " . $formElement['mode'], ERROR_UNKNOWN_MODE); + throw new CodeException("Unknown mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE); } } @@ -188,14 +187,14 @@ class FillStoreForm { */ private function doDateTime(array &$formElement, $value) { - $regexp = Support::dateTimeRegexp($formElement['type'], $formElement['dateFormat']); + $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement['dateFormat']); if (1 !== preg_match('/' . $regexp . '/', $value, $matches)) { $placeholder = Support::getDateTimePlaceholder($formElement); throw new UserFormException("DateTime format not recognized: $placeholder / $value ", ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED); } - $showTime = $formElement['type'] == 'date' ? '0' : '1'; + $showTime = $formElement[FE_TYPE] == 'date' ? '0' : '1'; $value = Support::convertDateTime($value, FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]); return $value; diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index e820d28e36ffc60959ddc1657222a540c16e6faa..f206903edfb3ba080ea395bc64c96e2a1aca922b 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -110,6 +110,7 @@ class Store { CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL, CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX, CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX, + CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT, // SYSTEM_DBUSER => SANITIZE_ALLOW_ALNUMX, // SYSTEM_DBSERVER => SANITIZE_ALLOW_ALNUMX, @@ -141,7 +142,8 @@ class Store { STORE_CLIENT => true, STORE_TYPO3 => false, STORE_ZERO => false, - STORE_SYSTEM => false + STORE_SYSTEM => false, + STORE_EXTRA => false ]; self::fillSystemStore(); @@ -182,10 +184,13 @@ class Store { $pos = strpos($_SERVER['SCRIPT_FILENAME'], $relExtDir); if ($pos === false && isset($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php'])) { - // probably: index.php - THERE should be a TYPO3 environment. + // Typo3 extension: probably index.php $config[SYSTEM_PATH_EXT] = dirname($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php']); + $config[SYSTEM_SITE_PATH] = dirname($_SERVER['SCRIPT_FILENAME']); } else { + // API $config[SYSTEM_PATH_EXT] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos + strlen($relExtDir)); + $config[SYSTEM_SITE_PATH] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos); } } } @@ -365,6 +370,7 @@ class Store { * @throws \qfq\CodeException */ private static function fillStoreExtra() { + if (isset($_SESSION[STORE_EXTRA])) self::setVarArray($_SESSION[STORE_EXTRA], STORE_EXTRA, true); else @@ -453,9 +459,8 @@ class Store { // The STORE_EXTRA saves arrays and is persistent if ($store === STORE_EXTRA) { - foreach ($value as $k1 => $v1) { - $_SESSION[STORE_EXTRA][$key][$k1] = $v1; - } + + $_SESSION[STORE_EXTRA][$key] = $value; } } diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index f82aa4acc169763aa853bbb5a3e34dceca2ea798..d796a85b5e19737002065876f22f014dcb79f737 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -63,7 +63,7 @@ CREATE TABLE IF NOT EXISTS `FormElement` ( `name` VARCHAR(255) NOT NULL DEFAULT '', `label` VARCHAR(255) NOT NULL DEFAULT '', - `mode` ENUM('show', 'readonly', 'required', 'lock', 'disabled') NOT NULL DEFAULT 'show', + `mode` ENUM('show', 'required', 'readonly', 'disabled') NOT NULL DEFAULT 'show', `class` ENUM('native', 'action', 'container') NOT NULL DEFAULT 'native', `type` ENUM('checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'gridJQW', 'hidden', 'text', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', @@ -81,6 +81,9 @@ CREATE TABLE IF NOT EXISTS `FormElement` ( `size` VARCHAR(255) NOT NULL DEFAULT '', `maxLength` VARCHAR(255) NOT NULL DEFAULT '', + `bsLabelColumns` VARCHAR(255) NOT NULL DEFAULT '', + `bsInputColumns` VARCHAR(255) NOT NULL DEFAULT '', + `bsNoteColumns` VARCHAR(255) NOT NULL DEFAULT '', `note` TEXT NOT NULL, `tooltip` VARCHAR(255) NOT NULL DEFAULT '', `placeholder` VARCHAR(255) NOT NULL DEFAULT '', @@ -131,9 +134,9 @@ VALUES (1, 'multi', 'Multi', 'show', 'pill', 'all', 'container', 40, 0, 0, '', '', '', '', '', '', 0, ''), (1, 'formelement', 'Formelement', 'show', 'pill', 'all', 'container', 50, 0, 0, '', '', '', '', '', '', 0, ''), - (1, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 10, 11, '', '', '', '', '', '', 1, ''), - (1, 'name', 'Name', 'required', 'text', 'all', 'native', 120, 40, 255, '', '', '', '', '', 'autofocus', 1, ''), - (1, 'title', 'Title', 'show', 'text', 'all', 'native', 130, 40, 255, '', '', '', '', '', '', 1, ''), + (1, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', '', 1, ''), + (1, 'name', 'Name', 'required', 'text', 'all', 'native', 120, 0, 255, '', '', '', '', '', 'autofocus', 1, ''), + (1, 'title', 'Title', 'show', 'text', 'all', 'native', 130, 0, 255, '', '', '', '', '', '', 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, ''), @@ -143,28 +146,28 @@ VALUES (1, 'showButton', 'Show button', 'show', 'checkbox', 'all', 'native', 200, 0, 0, '', '', '', '', '', 'checkBoxMode = multi\norientation=vertical', 2, ''), (1, 'forwardMode', 'Forward', 'show', 'radio', 'all', 'native', 260, 0, 0, '', '', '', '', '', '', 3, ''), - (1, 'forwardPage', 'Forward Page', 'show', 'text', 'all', 'native', 270, 40, 255, '', '', '', '', '', '', 3, ''), + (1, 'forwardPage', 'Forward Page', 'show', 'text', 'all', 'native', 270, 0, 255, '', '', '', '', '', '', 3, ''), (1, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 275, '40,3', 0, '', '', '', '', '', '', 3, ''), - (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 280, 40, 250, '', '', '', '', '', '', 3, ''), - (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 290, 40, 250, '', '', '', '', '', '', 3, ''), - (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 300, 40, 250, '', '', '', '', '', '', 3, ''), + (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 280, 0, 250, '', '', '', '', '', '', 3, ''), + (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 290, 0, 250, '', '', '', '', '', '', 3, ''), + (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 300, 0, 250, '', '', '', '', '', '', 3, ''), (1, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 405, 0, 0, '', '', '', '', '', '', 3, ''), - (1, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 410, 40, 20, '', '', '', '', '', '', 3, ''), - (1, 'created', 'Created', 'readonly', 'text', 'all', 'native', 420, 40, 20, '', '', '', '', '', '', 3, ''), + (1, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 410, 0, 20, '', '', '', '', '', '', 3, ''), + (1, 'created', 'Created', 'readonly', 'text', 'all', 'native', 420, 0, 20, '', '', '', '', '', '', 3, ''), (1, 'multi', 'Multi', 'show', 'fieldset', 'all', 'native', 210, 0, 0, '', '', '', '', '', '', 4, ''), (1, 'multiMode', 'Multi Mode', 'show', 'radio', 'all', 'native', 220, 0, 0, '', '', '', '', '', '', 4, ''), (1, 'multiSql', 'Multi SQL', 'show', 'text', 'all', 'native', 230, '40,3', 0, '', '', '', '', '', '', 4, ''), - (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'all', 'native', 240, 40, 255, '', '', '', '', '', '', 4, + (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'all', 'native', 240, 0, 255, '', '', '', '', '', '', 4, ''), - (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 250, 40, 255, '', '', + (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 250, 0, 255, '', '', '', '', '', '', 4, ''), (1, '', 'FormElements', 'show', 'subrecord', 'all', 'native', 500, 0, 0, '', '', '', - '{{!SELECT id, feIdContainer, name, label, mode, class, type, ord, size, sql1, parameter FROM FormElement WHERE formId={{id:R0}} ORDER BY ord, id}}', + '{{!SELECT IF( fe.enabled="yes", IF( fe.enabled="yes" AND fe.feIdContainer=0 AND !ISNULL(feCX.id) AND fe.class="native", "danger", IF( fe.class="container", "text-info", IF( fe.class="action", "text-success", ""))), "text-muted") AS _rowClass, IF( fe.enabled="yes", IF(fe.feIdContainer=0 AND !ISNULL(feCX.id) AND fe.class="native", "Please choose a container for this formelement", fe.class), "Disabled") AS _rowTitle, fe.id, CONCAT( IFNULL( CONCAT( feC.name, " (", fe.feIdContainer, ")"),"")) AS Container, fe.name, fe.label, fe.mode, fe.class, fe.type, fe.ord, fe.size, fe.sql1, fe.parameter FROM FormElement AS fe LEFT JOIN FormElement AS feC ON feC.id=fe.feIdContainer AND feC.formId=fe.formId LEFT JOIN FormElement AS feCX ON feCX.class="container" AND feCX.enabled="yes" AND feCX.formId=fe.formId WHERE fe.formId={{id:R0}} GROUP BY fe.id ORDER BY fe.class DESC, fe.feIdContainer, fe.ord, fe.id}}', '', 'form=formElement\ndetail=id:formId', 5, 'new,edit,delete'); # @@ -176,7 +179,8 @@ INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, r 'FormElement', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=5'); # FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) +INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, + sql1, sql2, parameter, feIdContainer, subrecordOption) VALUES (100, 2, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', '', 0, ''), @@ -185,77 +189,45 @@ VALUES (103, 2, 'value', 'Value', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), (104, 2, 'info', 'Info', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''); -INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, dynamicUpdate) +INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, dynamicUpdate, bsLabelColumns, bsInputColumns, bsNoteColumns) VALUES - (2, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 10, 11, '', '', '', '', '', '', 100, '', 'no'), - (2, 'formId', 'formId', 'readonly', 'text', 'all', 'native', 110, 40, 255, '', '', '', '', '', '', 100, '', 'no'), + (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, 'feIdContainer', 'Container', 'show', 'select', 'all', 'native', 120, 0, 0, '', '', '', '{{!SELECT fe.id, CONCAT(fe.class, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{formId}} AND fe.class="container" ORDER BY fe.ord }}', - '', '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, 'This element will be updated on change and trigger other.', '', '', '', '', '', 100, '', 'no'), - (2, 'name', 'Name', 'required', 'text', 'all', 'native', 140, 40, 255, '', '', '', '', '', '', 100, '', 'no'), - (2, 'label', 'Label', 'show', 'text', 'all', 'native', 150, 40, 255, '', '', '', '', '', '', 100, '', 'no'), - (2, 'mode', 'Mode', 'show', 'select', 'all', 'native', 160, 0, 255, '', '', '', '', '', '', 100, '', 'no'), - (2, 'class', 'Class', 'show', 'select', 'all', 'native', 170, 0, 255, '', '', '{{class:FSRD0:alnumx}}', '', '', '', 100, '', 'yes'), + '', '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', 'select', 'all', 'native', 160, 0, 255, '', '', '', '', '', '', 100, '', 'no', '', '', ''), + (2, 'class', 'Class', 'show', 'select', 'all', 'native', 170, 0, 255, '', '', '{{class:FSRD0:alnumx}}', '', '', '', 100, '', 'yes', '', '', ''), (2, 'type', 'Type', 'show', 'select', 'all', 'native', 180, 0, 255, '', '', '', '', '', 'itemList={{SELECT IF( "{{class:FRD0:alnumx}}"="native","checkbox,date,time,datetime,dateJQW,datetimeJQW,gridJQW,hidden,text,note,password,radio,select,subrecord,upload", IF("{{class:FRD0:alnumx}}"="action","before_load,before_save,before_insert,before_update,before_delete,after_load,after_save,after_insert,after_update,after_delete,feGroup,sendmail", "fieldset,pill") ) }}', - 100, '', 'yes'), - (2, 'subrecordOption', 'Subrecord Option', 'show', 'checkbox', 'all', 'native', 190, 0, 0, '', '', '', '', '', '', 100, '', 'no'), - (2, 'checkType', 'Check Type', 'show', 'select', 'all', 'native', 200, 0, 255, '', '', '', '', '', '', 101, '', 'no'), - (2, 'checkPattern', 'Check Pattern', 'show', 'text', 'all', 'native', 210, 40, 255, '', '', '', '', '', '', 101, '', 'no'), - (2, 'onChange', 'JS onChange', 'show', 'text', 'all', 'native', 220, 40, 255, '', '', '', '', '', '', 101, '', 'no'), - (2, 'ord', 'Order', 'show', 'text', 'all', 'native', 230, 40, 255, '', '', '', '', '', '', 101, '', 'no'), - (2, 'tabindex', 'tabindex', 'show', 'text', 'all', 'native', 240, 40, 255, '', '', '', '', '', '', 101, '', 'no'), - (2, 'size', 'Size', 'show', 'text', 'all', 'native', 250, 40, 255, '', '', '', '', '', '', 102, '', 'no'), - (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 260, 40, 255, '', '', '', '', '', '', 102, '', 'no'), - (2, 'note', 'note', 'show', 'text', 'all', 'native', 270, 40, 255, '', '', '', '', '', '', 102, '', 'no'), - (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 280, 40, 255, '', '', '', '', '', '', 102, '', 'no'), - (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 290, 40, 255, '', '', '', '', '', '', 102, '', 'no'), - (2, 'value', 'value', 'show', 'text', 'all', 'native', 300, 40, 255, '', '', '', '', '', '', 103, '', 'no'), - (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 310, '70,5', 255, '', '', '', '', '', '', 103, '', 'no'), - (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 320, '40,4', 255, '', '', '', '', '', '', 103, '', - 'no'), - (2, 'clientJs', 'ClientJS', 'show', 'text', 'all', 'native', 330, 40, 255, '', '', '', '', '', '', 103, '', 'no'), - (2, 'feGroup', 'feGroup', 'show', 'text', 'all', 'native', 340, 40, 255, '', '', '', '', '', '', 104, '', 'no'), - (2, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 350, 0, 0, '', '', '', '', '', '', 104, '', 'no'), - (2, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 360, 40, 20, '', '', '', '', '', '', 104, '', 'no'), - (2, 'created', 'Created', 'readonly', 'text', 'all', 'native', 370, 40, 20, '', '', '', '', '', '', 104, '', 'no'); - -# FormEditor: Small -INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES - ('person', 'Person {{SELECT ": ", firstName, " ", name, " (", id, ")" FROM Person WHERE id = {{r:S0}}}}', - 'Please secure the form', - 'Person', 'always', 'always', 'bootstrap', '', ''); - -# FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) -VALUES + 100, '', 'yes', '', '', ''), + (2, 'subrecordOption', 'Subrecord Option', 'show', 'checkbox', 'all', 'native', 190, 0, 0, '', '', '', '', '', '', 100, '', 'no', '', '', ''), + (2, 'checkType', 'Check Type', 'show', 'select', 'all', 'native', 200, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', ''), + (2, 'checkPattern', 'Check Pattern', 'show', 'text', 'all', 'native', 210, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', ''), + (2, 'onChange', 'JS onChange', 'show', 'text', 'all', 'native', 220, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', ''), + (2, 'ord', 'Order', 'show', 'text', 'all', 'native', 230, 0, 255, '', '', '{{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', 240, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', ''), + (2, 'size', 'Size', 'show', 'text', 'all', 'native', 250, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 260, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 270, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 280, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 290, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'note', 'note', 'show', 'text', 'all', 'native', 300, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 310, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 320, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', ''), + (2, 'value', 'value', 'show', 'text', 'all', 'native', 330, 0, 255, '', '', '', '', '', '', 103, '', 'no', '', '', ''), + (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 340, '70,5', 255, '', '', '', '', '', '', 103, '', 'no', '', '', ''), + (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 350, '40,4', 255, '', '', '', '', '', '', 103, '', + 'no', '', '', ''), + (2, 'clientJs', 'ClientJS', 'show', 'text', 'all', 'native', 360, 0, 255, '', '', '', '', '', '', 103, '', 'no', '', '', ''), + (2, 'feGroup', 'feGroup', 'show', 'text', 'all', 'native', 370, 0, 255, '', '', '', '', '', '', 104, '', 'no', '', '', ''), + (2, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 380, 0, 0, '', '', '', '', '', '', 104, '', 'no', '', '', ''), + (2, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 390, 0, 20, '', '', '', '', '', '', 104, '', 'no', '', '', ''), + (2, 'created', 'Created', 'readonly', 'text', 'all', 'native', 400, 0, 20, '', '', '', '', '', '', 104, '', 'no', '', + '', ''); - (200, 3, 'name', 'Name', 'show', 'text', 'all', 'native', 10, 50, 255, '', '', '', '', '', '', 0, ''), - (201, 3, 'firstname', 'Firstname', 'show', 'text', 'all', 'native', 20, 50, 255, '', '', '', '', '', '', 0, ''), - (202, 3, 'birthday', 'Birthday', 'show', 'date', 'all', 'native', 30, 50, 255, '', '', '', '', '', '', 0, ''); - -# ---------------------------------------------------------------------- -# - -# Form: plain -INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES - ('formplain', 'Form: Plain', '', 'Form', 'always', 'always', 'plain', '', ''); - -# FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) -VALUES - (300, 4, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 10, 11, '', '', '', '', '', '', 0, ''), - (310, 4, 'name', 'Name', 'show', 'text', 'all', 'native', 120, 40, 255, '', '', '', '', '', '', 0, ''); - -# Form: table -INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES - ('formtable', 'Form: Table', '', 'Form', 'always', 'always', 'table', '', ''); - -# FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) -VALUES - (400, 5, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 10, 11, '', '', '', '', '', '', 0, ''), - (410, 5, 'name', 'Name', 'show', 'text', 'all', 'native', 120, 40, 255, '', '', '', '', '', '', 0, ''); diff --git a/extension/qfq/sql/testtables.sql b/extension/qfq/sql/testtables.sql index 9ee45f9526efdec8c7417831083f0092ea23039f..5d8c224b8c99182c24b5c365854962e708201c14 100644 --- a/extension/qfq/sql/testtables.sql +++ b/extension/qfq/sql/testtables.sql @@ -1,11 +1,11 @@ DROP TABLE IF EXISTS Person; CREATE TABLE Person ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, + id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128), firstname VARCHAR(128), - gender ENUM('', 'male', 'female') NOT NULL DEFAULT 'male', - groups SET('', 'a', 'b', 'c') NOT NULL DEFAULT '', - birthday DATE NOT NULL DEFAULT '0000-00-00' + gender ENUM('', 'male', 'female') NOT NULL DEFAULT 'male', + groups SET('', 'a', 'b', 'c') NOT NULL DEFAULT '', + birthday DATE NOT NULL DEFAULT '0000-00-00' ); # @@ -15,24 +15,73 @@ INSERT INTO Person (id, name, firstname, gender, groups) VALUES (NULL, 'Smith', 'Jane', 'female', 'a,c'); +DROP TABLE IF EXISTS PersFunction; +CREATE TABLE PersFunction ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + personId BIGINT, + type ENUM('Student', 'Assistant', 'Professor', 'Administration'), + start DATE NOT NULL DEFAULT '0000-00-00', + end DATE NOT NULL DEFAULT '0000-00-00', + note VARCHAR(255) +); + +# --------------------------- +# Form: plain +REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES + (3, 'formplain', 'Form: Plain', '', 'Form', 'always', 'always', 'plain', '', ''); + +# FormEditor: FormElements +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) +VALUES + (300, 3, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', '', 0, ''), + (310, 3, 'name', 'Name', 'show', 'text', 'all', 'native', 120, 0, 255, '', '', '', '', '', '', 0, ''); + +# Form: table +REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES + (4, 'formtable', 'Form: Table', '', 'Form', 'always', 'always', 'table', '', ''); + +# FormEditor: FormElements +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) +VALUES + (400, 4, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', '', 0, ''), + (410, 4, 'name', 'Name', 'show', 'text', 'all', 'native', 120, 0, 255, '', '', '', '', '', '', 0, ''); + + +# Form: Person +REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES + (5, 'person', 'Person {{SELECT ": ", firstName, " ", name, " (", id, ")" FROM Person WHERE id = {{r:S0}}}}', + 'Please secure the form', + 'Person', 'always', 'always', 'bootstrap', '', ''); + +# FormEditor: FormElements +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) +VALUES + + (500, 5, 'name', 'Name', 'show', 'text', 'all', 'native', 10, 0, 255, '', '', '', '', '', '', 0, ''), + (501, 5, 'firstname', 'Firstname', 'show', 'text', 'all', 'native', 20, 0, 255, '', '', '', '', '', '', 0, ''), + (502, 5, 'birthday', 'Birthday', 'show', 'date', 'all', 'native', 30, 0, 255, '', '', '', '', '', '', 0, ''), + (506, 5, 'gender', 'Sex', 'show', 'select', 'alnumx', 'native', 40, 0, 0, '', '', '', '', '', '', 0, ''), + (503, 5, 'datumZeit', 'Datum & Zeit', 'show', 'datetime', 'alnumx', 'native', 50, 0, 0, '', '', '', '', '', '', 0, + ''), + (504, 5, 'zeit', 'Zeit', 'show', 'time', 'alnumx', 'native', 60, 0, 0, '', '', '', '', '', '', 0, ''), + (505, 5, 'picture', 'Picture', 'show', 'upload', 'allbut', 'native', 70, 0, 0, '', '', '', '', '', '', 0, ''); + +# ---------------------------------------------------------------------- +# DROP TABLE IF EXISTS Address; CREATE TABLE Address ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - person_id BIGINT, + personId BIGINT, street VARCHAR(128), city VARCHAR(128), country ENUM('Switzerland', 'Austria', 'France', 'Germany'), gr_id_typ BIGINT ); -DROP TABLE IF EXISTS Gruppe; -CREATE TABLE Gruppe ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255), - value VARCHAR(255), - typ VARCHAR(255), - t1 TEXT -); +INSERT INTO Address (personId, street, city) VALUES + (1, 'Side Street', 'Zurich'), + (1, 'Park Street', 'Zurich'), + (1, 'Winter Street', 'Zurich'), + (2, 'Summer Street', 'Zurich'); -# --------------------------- diff --git a/extension/qfq/tests/phpunit/BodytextParserTest.php b/extension/qfq/tests/phpunit/BodytextParserTest.php index f6e00a69f83e605a1d11f9926f84976793179ed4..b398d1f07217c6ef1e74a3fe09853f9dd0cd3b45 100644 --- a/extension/qfq/tests/phpunit/BodytextParserTest.php +++ b/extension/qfq/tests/phpunit/BodytextParserTest.php @@ -66,13 +66,13 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($expected, $result); // Nested expression: complex - $given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 {\n sql=SELECT 'Hello World3'\n40 { sql = SELECT 'Hello World4'\n} \n}\n}"; + $given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 { \n sql=SELECT 'Hello World3'\n40 { \n sql = SELECT 'Hello World4'\n } \n } \n } "; $expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n20.30.sql=SELECT 'Hello World3'\n20.30.40.sql = SELECT 'Hello World4'"; $result = $btp->process($given); $this->assertEquals($expected, $result); // form=...., {{ }} - $given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 { \nsql=SELECT 'Hello World'\n}\n form=Person\n"; + $given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 {\nsql=SELECT 'Hello World'\n}\n form=Person\n"; $expected = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30.sql=SELECT 'Hello World'\nform=Person"; $result = $btp->process($given); $this->assertEquals($expected, $result); @@ -83,9 +83,15 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase { $result = $btp->process($given); $this->assertEquals($expected, $result); - // Nested: unclosed open bracket - $given = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'\n"; - $expected = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'"; + // Single open bracket inside a string. + $given = "10.sql = SELECT 'Hello { World'"; + $expected = "10.sql = SELECT 'Hello { World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Complex test + $given = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20 {\n 10 {\n 5 {\n sql = SELECT 'hello world<br>'\n }\n }\n}\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT '20.10<br>'"; + $expected = "10.sql = SELECT '[\*]{7} [0-9]{5}<br>'\n20.10.5.sql = SELECT 'hello world<br>'\n20.10.5.head = Terific\n20.sql = SELECT 20, '<br>'\n20.10.sql = SELECT '20.10<br>'"; $result = $btp->process($given); $this->assertEquals($expected, $result); @@ -95,12 +101,24 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase { * @expectedException \qfq\UserFormException * */ - public function testProcessException() { + public function testProcessExceptionClose() { $btp = new BodytextParser(); - // Nested: unclosed open bracket + // Nested: unclosed close bracket $btp->process("10.sql = SELECT 'Hello World'\n } \n30.sql = SELECT 'Hello World'\n"); } + /** + * @expectedException \qfq\UserFormException + * + */ + public function testProcessExceptionOpen() { + $btp = new BodytextParser(); + + // Nested: unclosed open bracket + $btp->process("10.sql = SELECT 'Hello World'\n20 { \n30.sql = SELECT 'Hello World'\n"); + + } + } diff --git a/extension/qfq/tests/phpunit/BuildFormPlainTest.php b/extension/qfq/tests/phpunit/BuildFormPlainTest.php index 081d024ac072b849ca28c3e165ef55813928a9b6..fea1762a85394e672e4659762d04fff1800d3353 100644 --- a/extension/qfq/tests/phpunit/BuildFormPlainTest.php +++ b/extension/qfq/tests/phpunit/BuildFormPlainTest.php @@ -53,7 +53,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $build = new \qfq\BuildFormPlain(array(), array(), array()); $result = $build->buildLabel('myLabel:123', "Hello World"); - $this->assertEquals('<label for="myLabel:123">Hello World</label>', $result); + $this->assertEquals('<label for="myLabel:123" class="control-label" >Hello World</label>', $result); } public function testBuildInput() { @@ -97,32 +97,32 @@ class BuildFormPlainTest extends AbstractDatabaseTest { // Defaults $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" maxlength="255" value="" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); // CheckType $formElement['checkType'] = SANITIZE_ALLOW_MIN_MAX; $formElement['checkPattern'] = '1|10'; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" maxlength="255" value="" min="1" max="10" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" min="1" max="10" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); $formElement['checkType'] = SANITIZE_ALLOW_PATTERN; $formElement['checkPattern'] = '^[a-z]*$'; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" maxlength="255" value="" pattern="^[a-z]*$" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[a-z]*$" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); $formElement['checkType'] = SANITIZE_ALLOW_DIGIT; $formElement['checkPattern'] = ''; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" maxlength="255" value="" pattern="^[\d]*$" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[\d]*$" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); $formElement['checkType'] = SANITIZE_ALLOW_EMAIL; $formElement['checkPattern'] = ''; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" maxlength="255" value="" pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); $formElement['checkType'] = ''; @@ -133,31 +133,31 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement['size'] = 40; $formElement['maxLength'] = 40; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" size="40" maxlength="40" value="" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" size="40" maxlength="40" value="" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); // maxlength bigger than physical spec: $formElement['maxLength'] = 1000; $result = $build->buildInput($formElement, 'name:1', '', $json); - $this->assertEquals('<input name="name:1" type="input" size="40" maxlength="255" value="" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" size="40" maxlength="255" value="" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => '', 'disabled' => false, 'readonly' => false], $json); // Explicit: further $formElement['tooltip'] = 'Nice Tooltip'; $formElement['placeholder'] = 'Please type a name'; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<input name="name:1" type="input" size="40" maxlength="255" value="Hello World" placeholder="Please type a name" title="Nice Tooltip" >', $result); + $this->assertEquals('<input name="name:1" class="form-control" type="input" size="40" maxlength="255" value="Hello World" placeholder="Please type a name" title="Nice Tooltip" ><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, 'readonly' => false], $json); // textarea $formElement['size'] = '40,10'; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<textarea name="name:1" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" >Hello World</textarea>', $result); + $this->assertEquals('<textarea name="name:1" class="form-control" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" >Hello World</textarea><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, 'readonly' => false], $json); $formElement['size'] = ' 40 , 10 '; $result = $build->buildInput($formElement, 'name:1', 'Hello World', $json); - $this->assertEquals('<textarea name="name:1" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" >Hello World</textarea>', $result); + $this->assertEquals('<textarea name="name:1" class="form-control" cols="40" rows="10" placeholder="Please type a name" title="Nice Tooltip" >Hello World</textarea><div class="help-block with-errors"></div>', $result); $this->assertEquals(['form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false, 'readonly' => false], $json); } @@ -193,9 +193,9 @@ class BuildFormPlainTest extends AbstractDatabaseTest { 'enabled' => 'yes', 'name' => 'name', 'label' => 'Name', - 'mode' => 'show', + FE_MODE => 'show', 'class' => 'native', - 'type' => 'input', + FE_TYPE => 'input', 'value' => '', 'sql1' => '', 'parameter' => '', diff --git a/extension/qfq/tests/phpunit/DatabaseTest.php b/extension/qfq/tests/phpunit/DatabaseTest.php index f0594a27541b1ca629883da94cc4af6e2fceb2fc..ef5b36bcfbbd2d3deedada29601514f5d13df72c 100644 --- a/extension/qfq/tests/phpunit/DatabaseTest.php +++ b/extension/qfq/tests/phpunit/DatabaseTest.php @@ -137,6 +137,9 @@ class DatabaseTest extends AbstractDatabaseTest { * @throws \qfq\DbException */ public function testQuerySimpleParameter() { + $stat = array(); + $dummy = array(); + // Parameter Susbstitution by '?' $dataArray = $this->db->sql('SELECT * FROM Person WHERE name LIKE ? ORDER BY id', ROW_REGULAR, ['Smith']); // Check count @@ -145,11 +148,13 @@ class DatabaseTest extends AbstractDatabaseTest { $dataArray = $this->db->sql('UPDATE Person SET groups = ?', ROW_REGULAR, ['a,b,c']); $this->assertEquals(2, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); - $dataArray = $this->db->sql('INSERT INTO Person (`name`, `firstname`, `groups`) VALUES ( ?, ? ,? )', ROW_REGULAR, ['Meier', 'John', 'a']); - $this->assertEquals(3, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); + $dataArray = $this->db->sql('INSERT INTO Person (`name`, `firstname`, `groups`) VALUES ( ?, ? ,? )', ROW_REGULAR, ['Meier', 'John', 'a'], 'fake', $dummy, $stat); + $this->assertEquals(1, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); + $this->assertEquals(3, $stat[DB_INSERT_ID]); - $dataArray = $this->db->sql('INSERT INTO Person (`name`, `firstname`, `groups`) VALUES ( ?, ? ,? )', ROW_REGULAR, ['Meier', 'Jan', 'b']); - $this->assertEquals(4, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); + $dataArray = $this->db->sql('INSERT INTO Person (`name`, `firstname`, `groups`) VALUES ( ?, ? ,? )', ROW_REGULAR, ['Meier', 'Jan', 'b'], 'fake', $dummy, $stat); + $this->assertEquals(1, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); + $this->assertEquals(4, $stat[DB_INSERT_ID]); $dataArray = $this->db->sql('DELETE FROM Person WHERE name = ?', ROW_REGULAR, ['Meier']); $this->assertEquals(2, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); @@ -224,11 +229,13 @@ class DatabaseTest extends AbstractDatabaseTest { * @throws \qfq\DbException */ public function testGetLastInsertId() { + $dummy = array(); + $stat = array(); $sql = "INSERT INTO Person (id, name, firstname, gender, groups) VALUES (NULL, 'Doe', 'Jonni', 'male','')"; - $this->db->sql($sql); - $this->assertEquals(3, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM)); + $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + $this->assertEquals(3, $stat[DB_INSERT_ID]); } /** @@ -255,7 +262,8 @@ class DatabaseTest extends AbstractDatabaseTest { ['0' => '2', '1' => 'Smith', '2' => '0'], ]; // Same as above, but specify 'ROW_REGULAR' - $dataArray = $this->db->sqlKeys('SELECT id AS "id", name, "0" AS "id" FROM Person ORDER BY id LIMIT 3', $keys); + $stat = array(); + $dataArray = $this->db->sqlKeys('SELECT id AS "id", name, "0" AS "id" FROM Person ORDER BY id LIMIT 3', $keys, $stat); // Check rows $this->assertEquals($expected, $dataArray); @@ -263,6 +271,7 @@ class DatabaseTest extends AbstractDatabaseTest { // Check keys $this->assertEquals(['id', 'name', 'id'], $keys); } + /** * @throws Exception */ diff --git a/extension/qfq/tests/phpunit/fixtures/Generic.sql b/extension/qfq/tests/phpunit/fixtures/Generic.sql index a8d17684f0da13f73b95ef0bf0bca6c35ed11bc7..fb4a96802750aa0595c54535039cea57457d23fe 100644 --- a/extension/qfq/tests/phpunit/fixtures/Generic.sql +++ b/extension/qfq/tests/phpunit/fixtures/Generic.sql @@ -16,6 +16,7 @@ DROP TABLE IF EXISTS Note; CREATE TABLE Note ( id BIGINT AUTO_INCREMENT PRIMARY KEY, gr_id INT(11) NOT NULL DEFAULT 0, + person_id BIGINT(20) DEFAULT NULL, value VARCHAR(128), t1 TEXT, t2 TEXT @@ -25,3 +26,15 @@ INSERT INTO Note (gr_id, value, t1, t2) VALUES (1, 'First note - constants', 'Latest Information', 'Dear User\nt\n you\'re invited to to our Christmas party'), (2, 'Second note - some variables', 'Latest Information: {{party:C:all}}', 'Dear {{Firstname:C:all}} {{Lastname:C:all}}\n\n you\'re invited to to our Christmas party'); + + +DROP TABLE IF EXISTS Address; +CREATE TABLE Address ( + id BIGINT(20) NOT NULL AUTO_INCREMENT, + person_id BIGINT(20) NOT NULL DEFAULT 0, + street VARCHAR(128) NOT NULL DEFAULT '', + city VARCHAR(128) NOT NULL DEFAULT '', + country ENUM('', 'Switzerland', 'Austria', 'France', 'Germany') NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +); + diff --git a/javascript/src/Element/NameSpaceFunctions.js b/javascript/src/Element/NameSpaceFunctions.js index 4598c1af03f69d5facb62331144b755198b03124..c6f0343835bcfc24412be9dda10e1050ea5e1c37 100644 --- a/javascript/src/Element/NameSpaceFunctions.js +++ b/javascript/src/Element/NameSpaceFunctions.js @@ -34,7 +34,7 @@ QfqNS.Element = QfqNS.Element || {}; case 'number': case "email": case "url": - case "passoword": + case "password": case "datetime": case "datetime-local": case "date": diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js index 0c95e9f2872a8dda27c2f9e0c394873327ae3fd6..3cc8a806e7ed78abea9cf9cdb21b2d66bbbeed5a 100644 --- a/javascript/src/QfqForm.js +++ b/javascript/src/QfqForm.js @@ -717,6 +717,7 @@ var QfqNS = QfqNS || {}; /** * Retrieve SIP as stored in hidden input field. * + * @deprecated SIP should be passed via url or data attribute. * @returns {string} sip */ n.QfqForm.prototype.getSip = function () {