diff --git a/.gitignore b/.gitignore index d7bb865cb34775dc08bf9caa6caa8519008c9d7f..1503198894a2228e7b286b043815772d560cb92d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ +/.plantuml_install +/doc/*.pdf /.doc_plantuml /.support /.support_plantuml /.plantuml /doc/plantuml /extension/Documentation/_make/build -/qfq.ini /doc/phpdoc /.idea /node_modules @@ -18,7 +19,7 @@ /css /fonts /qfq.flowchart.dia.autosave -/extension/config.ini +/qfq*.zip /support /extension/Resources/Public/fonts /extension/Resources/Public/JavaScript diff --git a/API.md b/API.md deleted file mode 100644 index 7d0002656343a5b1a505fe5ca7eafeb687043440..0000000000000000000000000000000000000000 --- a/API.md +++ /dev/null @@ -1,201 +0,0 @@ -API: Client / Server -==================== - -Form initial call ------------------ - -Request: index.php (QuickFormQuery.php, included by Typo3 extension) - -Response: -Form attributes: - -data-hidden: 'yes'|'no' - yes: The element is not visible yet, maybe later. -data-disabled: 'yes'|'no' - yes: The element is visible, but the user can't interact with it. -data-required: 'yes'|'no' - yes: The element is required. The form can't be submitted if any required element is empty. - -General -------- - -Asynchronous request (read AJAX) initiated by the client receive a JSON Response from the server containing at least: - - { - "status": "success"|"error", - "message": "<message>" - } - -`status` indicates whether or not the request has been fullfiled by the server (`"success"`) or encountered an error (`"error"`). -On `"error"` the client must display `"<message>"` to the user. On `"success"`, the client may display `"<message>"` to the user. - - - -Form load (update) ------------------- - -### Trigger - -Form Element with attribute `data-load="data-load"`. - -The client side JavaScript installs on change handlers for all HTML Form Elements having the `data-load` attribute. - -### Request: api/load.php - -#### Type -POST - -#### Parameters - -##### URL -none - -##### POST -HTML Form without `<input>` elements of type `file`. The HTML Form is required to have a HTML Form Element named `s`, which must contain the SIP. - -### Response - -JSON Stream - - { - "status": "success"|"error", - "message": "<message>", - "redirect": "client"|"url"|"no", - "field-name": "<field name>", - "field-message": "<message>", - "form-update": [ - { - "form-element": "<element_name>", - "hidden": true | false, - "disabled": true | false, - "required": true | false, - "value": <value> - } - ] - } - -Name | Description -------- | ----------- -status | see General -message | see General -redirect | not used -field-name | HTML Form Element Name which raised error on server side. Requires status to be `"error"` -field-message | reason of error. Requires status to be `"error"`. -form-update | Array of Objects. Each object describes the state and value of a HTML Form Element identfied by its `name` attribute. - - -Form save ---------- - -### Trigger -none - -### Request: api/save.php - -#### Type -POST - -#### Parameters - -##### URL -none - -##### POST -HTML Form without `<input>` elements of type `file`. The HTML Form is required to have a HTML Form Element named `s`, which must contain the SIP. - -### Response - -JSON Stream - - { - "status": "success"|"error", - "message": "<message>", - "redirect": "client"|"url"|"no", - "field-name": "<field name>", - "field-message": "<message>", - "form-update": [ - { - "form-element": "<element_name>", - "hidden": true | false, - "disabled": true | false, - "required": true | false, - "value": <value> - } - ] - } - -Name | Description -------- | ----------- -status | see General -message | see General -redirect | not used -field-name | HTML Form Element Name which raised error on server side. Requires status to be `"error"` -field-message | reason of error. Requires status to be `"error"`. -form-update | Array of Objects. Each object describes the state and value of a HTML Form Element identfied by its `name` attribute. - - -File (upload) -------------- - -### Trigger -none - -### Request: api/file.php - -#### Type -POST - -#### Parameters - -##### URL -`action=upload` - -##### POST -Multi part form with file content, parameter `s` containing SIP, and parameter `name` containing the name of the HTML Form Element. - -### Response - -JSON Stream - - { - "status": "success"|"error", - "message": "<message>" - } - - -Name | Description -------- | ----------- -status | see General -message | see General - - -Record delete -------------- - -Request: api/delete.php - - -Return JSON encoded answer - -status: success|error -message: <message> -redirect: client|url|no -redirect-url: <url> -field-name:<field name> -field-message: <message> - -Description: - -Delete successfull. - status = 'success' - message = <message> - redirect = 'client' - -Delete successfull. - status = 'success' - message = <message> - redirect = 'url' - redirect-url = <URL> - -Delete failed: Show message. - status = 'error' - message = <message> - redirect = 'no' - diff --git a/Gruntfile.js b/Gruntfile.js index d627afb60b2c96f1159e42d81030d69abc280855..332d0474a865dbe0ff55fa0ae7991501e8f1ee58 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,7 @@ var path = require('path'); module.exports = function (grunt) { + 'use strict'; + var typo3_css = 'extension/Resources/Public/Css/'; var typo3_js = 'extension/Resources/Public/JavaScript/'; var typo3_fonts = 'extension/Resources/Public/fonts/'; @@ -147,12 +149,35 @@ module.exports = function (grunt) { } ] }, + ChartJS: { + files: [ + { + cwd: 'bower_components/Chart.js/dist/', + src: [ + 'Chart.min.js' + ], + expand: true, + dest: typo3_js, + flatten: true + }, + { + cwd: 'bower_components/Chart.js/dist/', + src: [ + 'Chart.min.js' + ], + expand: true, + dest: "js/", + flatten: true + } + ] + }, jqwidgets: { files: [ { cwd: 'bower_components/jqwidgets/jqwidgets/', src: [ - 'jqx-all.js' + 'jqx-all.js', + 'globalization/globalize.js' ], expand: true, dest: typo3_js, @@ -162,7 +187,7 @@ module.exports = function (grunt) { cwd: 'bower_components/jqwidgets/jqwidgets/styles/', src: [ 'jqx.base.css', - 'jqx.darkblue.css' + 'jqx.bootstrap.css' ], expand: true, dest: typo3_css, @@ -183,7 +208,8 @@ module.exports = function (grunt) { { cwd: 'bower_components/jqwidgets/jqwidgets/', src: [ - 'jqx-all.js' + 'jqx-all.js', + 'globalization/globalize.js' ], expand: true, dest: 'js/', @@ -193,7 +219,7 @@ module.exports = function (grunt) { cwd: 'bower_components/jqwidgets/jqwidgets/styles/', src: [ 'jqx.base.css', - 'jqx.darkblue.css' + 'jqx.bootstrap.css' ], expand: true, dest: 'css/', @@ -209,6 +235,54 @@ module.exports = function (grunt) { } ] }, + tinymce: { + files: [ + { + cwd: 'bower_components/tinymce/', + src: [ + 'tinymce.min.js' + ], + expand: true, + dest: typo3_js, + flatten: true + }, + { + cwd: 'bower_components/tinymce/', + src: [ + 'themes/*/theme.min.js', + 'plugins/*/plugin.min.js', + 'skins/**' + ], + dest: typo3_js, + expand: true, + flatten: false + } + ] + }, + tinymce_devel: { + files: [ + { + cwd: 'bower_components/tinymce/', + src: [ + 'tinymce.min.js' + ], + expand: true, + dest: 'js/', + flatten: true + }, + { + cwd: 'bower_components/tinymce/', + src: [ + 'themes/*/theme.min.js', + 'plugins/*/plugin.min.js', + 'skins/**' + ], + dest: 'js/', + expand: true, + flatten: false + } + ] + }, eventEmitter: { files: [ { @@ -242,9 +316,7 @@ module.exports = function (grunt) { } }, jshint: { - all: [ - 'javascript/src/*.js' - ] + all: js_sources }, concat_in_order: { debug_standalone: { @@ -315,8 +387,8 @@ module.exports = function (grunt) { } }, jasmine: { - frontend: { - src: ['tests/jasmine/spec/*Spec.js'], + unit: { + src: ['tests/jasmine/unit/spec/*Spec.js'], options: { vendor: [ 'js/jquery.min.js', @@ -326,18 +398,13 @@ module.exports = function (grunt) { 'js/qfq.debug.js' ], helpers: ['tests/jasmine/helper/mock-ajax.js'], - template: 'tests/jasmine/SpecRunner.tmpl' + template: 'tests/jasmine/unit/SpecRunner.tmpl' } } }, watch: { scripts: { - files: [ - 'javascript/src/*.js', - 'javascript/src/Helper/*.js', - 'javascript/src/Element/*.js', - 'less/*.less' - ], + files: js_sources.concat(['less/*.less']), tasks: ['default'], options: { spawn: true diff --git a/Makefile b/Makefile index c6ed615c18c0549bc96b695d38b5ac308d141073..a3fd4e6215d69fcd38815f00a82f408c40abbdc1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PHPDOC ?= support/pear/phpdoc JSDOC ?= jsdoc PKG_VERSION = $(shell awk '/version/ { print $$3 }' extension/ext_emconf.php | sed "s/'//g") NIGHTLY_DATE = $(shell date '+%Y%m%d') -EXTENSION_CONTENT = Classes Configuration Documentation qfq Resources ext_emconf.php ext_localconf.php ext_tables.php config.example.ini +EXTENSION_CONTENT = Classes Configuration Documentation qfq Resources ext_emconf.php ext_localconf.php ext_tables.php ext_icon.png config.example.ini all: archive t3sphinx @@ -12,10 +12,10 @@ maintainer-clean: rm -f .bowerpackages .doc_plantuml .npmpackages .phpdocinstall .plantuml_install .support .support_plantuml rm -rf doc support -archive: clean qfq_$(PKG_VERSION).zip +archive: clean qfq.zip -qfq_$(PKG_VERSION).zip: - cd extension; zip -r ../$@ $(EXTENSION_CONTENT) -x config.ini +qfq.zip: + cd extension; zip -r ../$@ $(EXTENSION_CONTENT) clean: rm -f qfq_$(PKG_VERSION).zip diff --git a/README.md b/README.md index 72bb88ca8f5c31b715da21e87d87d851ab0fa733..626848a3c5be32f2ead10a3bf14bbf7ada510b5e 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,23 @@ Version: see `extension/ext_emconf.php` Installation ------------ -* Take care that the `php5-mysqlnd` driver is installed: -* The following functions are used and are only available with the native driver: +* Ubuntu < 16.04: Take care that the `php5-mysqlnd` driver is installed: + * See also: http://dev.mysql.com/downloads/connector/php-mysqlnd/ + * If there is a error message "Call to undefined method mysqli_stmt::get_result()", `php5-mysqlnd` is not installed or not active. + * The following functions are used and are only available with the native driver: + + ```bash mysqli::get_result (important), mysqli::fetch_all (nice to have) -* See also: http://dev.mysql.com/downloads/connector/php-mysqlnd/ -* If there is a error message "Call to undefined method mysqli_stmt::get_result()", `php5-mysqlnd` is not installed or not active. + ``` -* Ubuntu: + * Ubuntu: - sudo apt-get install php5-mysqlnd - sudo php5enmod mysqlnd - sudo service apache2 restart + ```bash + sudo apt-get install php5-mysqlnd + sudo php5enmod mysqlnd + sudo service apache2 restart + ``` * Install extension as regular. * In `typo3conf/ext/qfq` rename `config.examle.ini` to `config.ini`. @@ -26,23 +31,28 @@ Installation Bootstrap: include by TypoScript --------- + +```script page.includeCSS { - file1 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css - file2 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css - file3 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css - file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.darkblue.css - file5 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css + file1 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css + file2 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css + file3 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css + file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.bootstrap.css + file5 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css } page.includeJS { - file1 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.min.js - file2 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js - file3 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js - file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq-min.js + file1 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.min.js + file2 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js + file3 = typo3conf/ext/qfq/Resources/Public/JavaScript/validator.min.js + file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js + file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/tinymce.min.js + file5 = typo3conf/ext/qfq/Resources/Public/JavaScript/EventEmitter.min.js + file6 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.min.js } - +``` Usage ----- diff --git a/bower.json b/bower.json index 4cac6561adf6cdf1929e65520f65910de81d80fe..249fe0462c3892ae6d52ab1427bccd2f7ec6c0c2 100644 --- a/bower.json +++ b/bower.json @@ -18,9 +18,11 @@ ], "dependencies": { "bootstrap": "~3.3.6", - "jqwidgets": "*", + "jqwidgets": "4.2.1", "tablesorter": "jquery.tablesorter#^2.25.6", "eventEmitter": "^4.3.0", - "bootstrap-validator": "^0.10.2" + "bootstrap-validator": "^0.11.5", + "Chart.js": "^2.1.2", + "tinymce": "tinymce-dist#^4.4.3" } } diff --git a/CODING.md b/doc/CODING.md similarity index 56% rename from CODING.md rename to doc/CODING.md index 8b499af70ad075105aa96ec083f09219060b46a0..620d93aa851d466a799acc5c64aed95998096b7d 100644 --- a/CODING.md +++ b/doc/CODING.md @@ -23,7 +23,7 @@ LOAD * When qfq starts, * (Form) Looking for a formname at: 1. Typo3 Bodytext Element, - 2. For the 'SIP' ($_GET['s']) + 2. For the 'SIP' ($_GET['s'] => $S_SESSION['qfq'][$_GET['s']]="form=person&r=123") 3. $_GET variables 'form' and 'r' (=recordId) - the parameter 'form' has to be allowed in 'Permit URL Parameter' of the specified form. This means: load the form to check, if it is allowed to load the form!? * If a formname is found, the search stops and the specified form will be processed. @@ -36,12 +36,20 @@ LOAD * All parameters from active SIP: [$this->store->getStore(STORE_SIP)] * Check Contstants.php for known Store members -* In QuickFormQuery.php the whole Form will be copied to $this->formSpec and depending on further processing, the elements are -available in $this->feNative and $this->feAction. +* In QuickFormQuery.php the whole Form will be copied to `$this->formSpec` and depending on further processing, the + elements are available in `$this->feNative` and `$this->feAction`. * The Form specificaton (table form) will be evaluated direct after loading. * The FormElement specification will be evaluated later on in BuildForm*.php - +* If a form is called without a SIP (form.permitNew='always'), than a SIP is created on the fly (as a + parameter in the form). +* Depending on `r=0` or `r>0` a form submit will do an MySQL `insert` or `update` later during save. +* For new records (r=0), clicking on 'save' without closing the form is a tricky situation. Additionally the user might have open multiple + tabs (same form, all r=0) and after saving the record (wihtout closing the form) the user expects that it's ok to edit + the record again and again. Unfortunately, the initial created SIP (before 'form load') is not uniqe anymore (multiple + tabs might contain a saved 'new record'). To guarantee correct saving of r=0 records, a unique on the fly generated SIP + is creatd during form load - individually per browser tab. + SAVE ---- * Via wrapper api/save.php @@ -51,6 +59,18 @@ SAVE * Client will handle the response of save.php. * Optional redirection initiated by client. +New records +........... +* r=0 (missing 'r' means r=0) +* After saving the SIP content will be updated with the new record. + Remember that the SIP in the URL is *not* the SIP used in the form to identify the form/record. The form use a + individual 'new record' SIP. + +Existing records +................ +* r>0 ('r' have to exist) + + DELETE ------ * Via wrapper api/delete.php @@ -60,153 +80,67 @@ DELETE * class=record-delete * Button: data-sip={{SIP}} + * SIP values: + + * SIP_RECORD_ID: Mandatory. + * SIP_TABLE: Either SIP_TABLE or SIP_FORM has to be given. + * SIP_FORM: Either SIP_TABLE or SIP_FORM has to be given. Not implemented now. + * SIP_TARGET_URL: Only with SIP_MODE_ANSWER=MODE_HTML - Url to redirect browser to. + * SIP_MODE_ANSWER: MODE_JSON / MODE_HTML. If not given, this means MODE_JSON. + * Three possible variants with delete links: - * Form: main record + * (1) Form: main record * HTML Code: <button id="delete-button" type="button" class="btn btn-default navbar-btn" ><span class="glyphicon glyphicon-trash"></span></button> - * Form: subrecord, one delete button per record - * Report: typially inside a table, but maybe different. + * (2) Form: subrecord, one delete button per record * HTML Code: <button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button> + + * (3) Report: typially inside a table, but maybe different. -USER INTERFACE -============== + <button type="button" class="record-delete" data-sip={{SIP}} ><span class="glyphicon glyphicon-trash"></span></button> -Button status -------------- -* Form modified: - * Buttons enabled: Save, Close, New, Delete - * Button disable: - - -* Form not modified: - * Buttons enabled: Close, New, Delete - * Button disabled: Save - -Save Button ------------ - -* User presses the button - * Reset all validation states - * Client validates HTML Form - * Form is submitted to server - * Success: - * Show message provided by server - * Current formelements and data will be reloaded. - * Process server reponse 'redirect': - * 'client': No redirect. - * 'no': No redirect. - * 'url': Redirect to URL - * Failure: Happens on communication errors, if data validation fails, form actions fails or saving data fails. - * Show error message. - * Client: Ignore server reponse 'redirect'. Client stays on current page. - -Close Button ------------- -* User presses the button - * Form not modified by user: Client redirects to previous page. - * Form modified by user: Ask User 'You have unsaved changes. Do you want to close?' - * Yes: Client redirects to previous page. - * No: Client stays on current page. - * Save & Close: - * Client reset all validation states - * Client validates HTML Form - * Client submits form to server. - * Success: Process server response 'redirect': - * 'client': Client shows previous page. - * 'no': No redirect. - * 'url': Redirect to URL - * Failure: Happens on communication errors, if data validation fails, form actions fails or saving data fails. - * Show error message. - * Client: No redirect. Ignore server reponse 'redirect'. - -Delete Button: Main record --------------------------- -* User presses the button. Ask User 'Do you really want to delete the record? - * Yes: - * Delete record on server. - * Process server reponse 'redirect': - * 'client': Client redirects to previous page. - * 'no': Error message. - * 'url': Redirect to URL - * No: - * Client does not delete record on server. - * No redirect. Client stays on current page. - -New Button ----------- -* User presses the button - * Form not modified by user: Client redirects to href url. - * Form modified by user: Ask User 'You have unsaved changes. Do you want to save first?' - * Yes: - * Client reset all validation states - * Client validates HTML Form - * Form is submitted to server - * Success: - * Client: Ignore server reponse 'redirect'. Client redirects to href url. - * Failure: Happens on communication errors, if data validation fails, form actions fails or saving data fails. - * Show error message. - * Client: Ignore server reponse 'redirect'. Client stays on current page. - * No: - * Client does not save the modified record. - * Client redirects to href url. - * Cancel: - * Client does not save the modified record. - * Client stays on current page. - +Upload +----------------- -File Handling: Upload ---------------------- -* No previous uploaded file present - 1. User presses the Browse button - 1. User selects file - 1. File is uploaded to qfq immediately - 1. Browse button gets disabled and hidden - 1. File delete button is shown - 1. User cancels file selection - 1. no action -* Previous uploaded file present - 1. User deletes file - 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. +* The upload UI consist of three elements + * 1) A <div> tag with a) an optional filename of an earlier uploaded file and b) a trash Button. + * 2) The 'browse' button (<input type='file' name='<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. +* A new uniq SIP (sipUpload) will be created for every upload formElement. These 'sipUpload' will be assigned to the upload browse button and to the upload delete button. + * The individual sipUpload is necessary to correctly handle multiple simultaenously forms when using r=0. Also, through this uniq id it's easy to distinguish between asynchron uploaded files. + * The SIP on ther server contains the individual '_FILES' information submitted during the upload. +* Via the hidden element <feName> 'save()', access to the form individual upload status informations is given. 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). +* 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 until the upload finished. +* After successfull upload 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. + * $_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>] +* Step 1: insert /update the record. +* Step 2: 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>] +* Step 3: update record with final `FileDestination' Formelement type: DATE / DATETIME / TIME ---------------------------------------- @@ -232,8 +166,8 @@ Formelement type: DATE / DATETIME / TIME * datetime format: 'DATE TIME' -Debug / Log / Errormessages -=========================== +Debug / Log +=========== * Before firing a SQL or doing processing of an FormElement, set some debugging / error variables: @@ -257,6 +191,20 @@ Debug / Log / Errormessages $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes' +Errormessages & Eceptions +========================= + +* Exception types: + * Code + * Db + * User Form + * user Report + * plus an Errorhandler which throws exceptions + +* Exceptions inside of an API call delivers the error code and msg as JSON to the client. +* Typo3 suppress E_NOTICE (e.g. undefined index). To catch E_NOTICE in QFQ, it will be temporaly enabled in QfqCongroller.php. + + Stores ====== @@ -298,8 +246,8 @@ SIP === Page loaded: www.example.com?index.php&id=start&s=badcaffee1234&type=2&L=3, with $_SESSION['badcaffee1234'] => 'form=Person&r=1' -* $_SESSION[$sip] => <urlparam> >> $_SESSION['badcaffee1234'] => 'form=Person&r=1' -* $_SESSION[$urlparam] => <sip> >> $_SESSION['form=Person&r=1'] => 'badcaffee1234' +* $_SESSION['qfq'][$sip] => <urlparam> >> $_SESSION['qfq']['badcaffee1234'] => 'form=Person&r=1' +* $_SESSION['qfq'][$urlparam] => <sip> >> $_SESSION['qfq']['form=Person&r=1'] => 'badcaffee1234' FormElement @@ -314,4 +262,4 @@ Checkbox </label> </div> - \ No newline at end of file + diff --git a/doc/HTML.md b/doc/HTML.md new file mode 100644 index 0000000000000000000000000000000000000000..d7d3266f03759c60187b40e35e44d6675f1b4863 --- /dev/null +++ b/doc/HTML.md @@ -0,0 +1,39 @@ +# HTML + +This document explains the HTML markup used by QFQ. + +## Hooks + +Hooks are used on by the Client to gather information required for +asynchronous requests and to add predefined event handlers to HTML Elements. + + +### form.data-toggle="validator" + +Adding the attribute `data-toggle="validator"` to a `<form>` element, +enables the Bootstrap Validator on that HTML Form. + + +### .data-sip + +Asynchronous requests require to pass a SID to the Server. Elements +triggering an asynchronous request, may gather the SIP from the +`data-sip` attribute assigned to the HTML Form Element. + + +### .class="record-delete" + +HTML Form Buttons having the class `record-delete` set, will get an +`onclick` handler attached by `QfqNS.QfqRecordList`. Each `<button>` +also requires an `data-sip` attribute. + + +### .data-load="" + +HTML Form Elements having the attribute `data-load`, will trigger a +call to `api/load.php` upon change. + +### id="save-button" +### id="close-button" +### id="delete-button" +### id="form-new-button" diff --git a/LAYOUT.md b/doc/LAYOUT.md similarity index 100% rename from LAYOUT.md rename to doc/LAYOUT.md diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..b9d13ff192becbd391749e0c62edcdffb0d78717 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,9 @@ +SOURCES = CODING.md HTML.md PROTOCOL.md UI.md LAYOUT.md +PDFDOCS = $(patsubst %.md,%.pdf,$(SOURCES)) + +all: $(PDFDOCS) + +.md.pdf: + pandoc -o $@ $< + +.SUFFIXES: .md .pdf diff --git a/doc/NewVersion.md b/doc/NewVersion.md new file mode 100644 index 0000000000000000000000000000000000000000..923c7d9f34bfc3a70be012321290e30ebd033851 --- /dev/null +++ b/doc/NewVersion.md @@ -0,0 +1,32 @@ + +Neue Versionsnummer +=================== + +1) In folgenden Files anpassen: + * extension/Documentation/_make/conf.py: release, version + * extension/Documentation/Settings.yml: version + * extension/ext_emconf.php: version + * extension/RELEASE.txt + +2) Im Projektverzeichnis: + + make t3sphinx (dadurch fallen Fehler in RESTdoc Syntax auf) + + +3) Merge auf master Branch + + git checkout master + git merge crose_work + +4) Neuen Tag vergeben: git tag 0.10 + +5) Alle Files, inkl. Tags, in GIT einchecken. + +6) Per PhpStorm Sync aller Files auf VM qfq + +7) In T3 Instanz Dokumentation rendern lassen. + + T3 6.2: Admin Tools > Extension Manager > QFQ > Doku HTML: rechts oben 'Render Documentation' + + + diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md new file mode 100644 index 0000000000000000000000000000000000000000..9af56bfb66b60e9aa25446301a2f5b46c7034a85 --- /dev/null +++ b/doc/PROTOCOL.md @@ -0,0 +1,301 @@ +<!-- -*- markdown -*- --> + +# Client/Server Protocol + + +## General Protocol + +The Client may asynchronously send requests to the Server. The Server +is expected to return responses as outlined below. + +The response must contain at least a [Minimal Response]. Depending on +the request, it may provide additional responses as outlined in this +section. + + +### Minimal Response + +Asynchronous request (read AJAX) initiated by the Client receive a +JSON Response from the server containing at least: + + { + "status": "success"|"error", + "message": "<message>" + } + +`status` indicates whether or not the request has been fullfiled by +the server (`"success"`) or encountered an error (`"error"`). On +`"error"` the Client must display `"<message>"` to the user. On +`"success"`, the Client may display `"<message>"` to the user. + +Depending on the request, the server may provide additional +information in the response, as outlined below. + + +### HTML Form Element Validation Response + +The Server may perform a serverside validation of values submitted as +part of a HTML Form submission. If the validation fails, it may notify +the Client by adding following name/value pairs to the response JSON +Stream + + { + "status": "error", + ... + "field-name": "<field name>", + "field-message": "<message>", + ... + } + +Only one validation failure per request can be reported to Client. + +The Server is expected to set the status `"status"` to `"error"`, and +the Client is expected to treat the error as explained in [Minimal Response] +and must obey the rules of redirection as explained in [Redirection Response]. + +The Client must visibly highlight the HTML Form Element that caused the +validation failure. + +`"field-name"` +: The value of the `name` attribute of the HTML Form Element that + caused the validation failure. + +`"field-message"` +: Message to the User, indicating the nature of the failure. + + +### Form Group Configuration Response + +As part of the server response, the JSON stream may contain a key +`form-update`. This response is used to reconfigure HTML Form Elements +and Form Groups on the clientside, based on conditions evaluated on +the serverside. It contains an array of objects +having the following structure + + { + ... + "form-update" : [ + { + "form-element": "<element_name>", + "hidden": true | false, + "disabled": true | false, + "required": true | false, + "value": <value> + }, + ... + ], + ... + } + +`"form-element"` +: the name of the HTML Form Element as it appears in the `name` attribute. + +`"hidden"` +: whether the Form Group is visible (value: `false`) or invisible (value: `true`). + +`"disabled"` +: whether or not the Form Element is disabled HTML-wise. + +`"required"` +: whether or not the Form Element receives the HTML5 `required` attribute. + +`"value"` +: For textual HTML Form Input elements, it is supposed to be a scalar + value, which is set on the element. + + When `"form-element"` references a `<select>` element, a scalar + value selects the corresponding value from the option list. In + order to replace the entire option list, use an array of objects + as value to `"value"`, having this format + + [ + ... + { + "value": 100, + "text": "a", + "selected": true + }, + { + "value": 200, + "text": "b", + "selected": false + } + ... + ] + + `"select"` is optional, as is `"text"`. If `"text"` is omitted, it + will be derived from value. + + HTML checkboxes are ticked with a `"value"` of `true`. They are + unchecked with `false`. + + HTML radio buttons are activated by providing the value of the + radio button `value`-attribute to be activated in `"value"`. + + +### Redirection Response + +Depending on the request, the server may return redirection +information to the Client. It is up to the Client to respect the +redirection information. + +The Client must not perform a redirect in case the status in +`"status"` is `"error"`. + +The format of redirect information is outlined below + + { + ... + "redirect": "no" | "url" | "client" + "redirect-url": "<url>" + ... + } + + +`"redirect"` +: type of redirection. `"no"` advises the Client to stay on the + Current Page. `"client"` advises the Client to decide where to + redirect to. `"url"` advices the Client to redirect to the URL + provided in `"redirect-url"`. + +`"redirect-url"` +: Used to provide an URL when `"redirect"` is set to `"url"`. It + should be disregarded unless `"redirect"` is set to `"url"`. + + +## API Endpoints + + +### Form Update + +The Client may request an updated set of Form Group Configuration and +HTLM Element states. In order for the Server to compile the set of +Form Group Configuration and HTML Element states, it requires the +entire HTML Form in the POST body, without any HTML Input Elements of +type `file`. + +The Client must include the SIP using an HTML Input Element (most +likely of `type` `hidden`). + +Request URL +: api/load.php + +Request Method +: POST + +URL Parameters +: none + +Server Response +: The response contains at least a [Minimal Response]. In addition, + a [Form Group Configuration Response] may be included. + + +### Form Save + +The Client submits the HTML Form for persitent storage to the +Server. The submission should not contain `<input>` HTML Elements of +type `file`. + +The Client must include the SIP using an HTML Input Element (most +likely of `type` `hidden`). + +Request URL +: api/save.php + +Request Method +: POST + +URL Parameters +: none + +Server Response +: The response contains at least a [Minimal Response]. In addition, a + [Form Group Configuration Response], + [HTML Form Element Validation Response] and/or + [Redirection Response] may be included. + + +### File Upload + +Files are uploaded asynchronously. Each file to be uploaded requires +one request to the Server, using a Multi part form with file content, +parameter `s` containing SIP, and parameter `name` containing the name +of the HTML Form Element. + +Request +: api/file.php + +Request Method +: POST + +URL Parameters +: `action=upload` + +Server Response +: The response contains a [Minimal Response]. + + +### File Delete + +Files are delete asynchronously. Each file to be delete on the +serverside requires on request to the Server. The parameters +identifying the file to be deleted are sent as part of the POST +body. The SIP of the request is included in the parameter name +`s`. The value of the `name` attribute of the HTML Form Element is +provided in `name`. + +Request +: api/file.php + +Request Method +: POST + +URL Parameters +: `action=delete` + +Server Response +: The response contains a [Minimal Response]. + + +### Record(s) delete + +Request the deletion of the record identified by the SIP. The SIP might contain a SIP_TABLE and/or a SIP_FORM. +If both are specified, SIP_FORM will be taken. With SIP_FORM, the tableName is derived from the form. + +Request +: api/delete.php + +Request Method +: POST + +URL Parameters +: `s=<SIP>` + +Server Response +: The response contains a [Minimal Response]. + [Redirection Response] may be included. + + +## Glossary + +SIP +: tbd + +HTML Form Element +: Any `<input>` or `<select>` HTML tag. Synonymous to *Form Element*. + +Form Group +: The sourrounding `<div>` containing the `.control-label`, + `.form-control` `<div>`s, and `.help-block` `<p>`. + +Client +: Application that enables a user to interact with QFQ, i.e. a Web Browser. + + +Current Page +: The currently displayed page in the Client. + +Redirect +: Issued by the Server. It is a command prompting the Client to + navigate away from the Current Page. diff --git a/doc/UI.md b/doc/UI.md new file mode 100644 index 0000000000000000000000000000000000000000..599a6a2489bda0eeba01a088dabd25aa094b62d6 --- /dev/null +++ b/doc/UI.md @@ -0,0 +1,102 @@ +USER INTERFACE +============== + +Button states +------------- +If the HTML Form has no modifications, the `Close`, `New` and `Delete` +buttons are enabled. The `Save` button is disabled. + +If the HTML Form has modifications, the `Save`, `Close`, `New`, and +`Delete` button is enabled. No button is disabled. + + +Save Button +----------- + +* User presses the Save button + 1. Reset all validation states + 1. Client validates HTML Form + 1. Form is submitted to server + * Success: + 1. Show message provided by server + 1. Current formelements and data will be reloaded. + 1. Process server reponse 'redirect': + * 'client': No redirect. + * 'no': No redirect. + * 'url': Redirect to URL + * Failure: Happens on communication errors, if data validation + fails, form actions fails or saving data fails. + 1. Show error message. + 1. Client: Ignore server reponse 'redirect'. Client stays on current page. + + +Close Button +------------ +* User presses the Close button + 1. Form not modified by user: Client redirects to previous page. + 1. Form modified by user: Ask User 'You have unsaved changes. Do you want to save first?' + * Yes: Client redirects to previous page. + * No: Client stays on current page. + * Save & Close: + 1. Client reset all validation states + 1. Client validates HTML Form + 1. Client submits form to server. + * Success: Process server response 'redirect': + * 'client': Client shows previous page. + * 'no': No redirect. + * 'url': Redirect to URL + * Failure: Happens on communication errors, if data validation + fails, form actions fails or saving data fails. + * Show error message. + * Client: No redirect. Ignore server reponse 'redirect'. + +Delete Button: Main record +-------------------------- +* User presses the button. Ask User 'Do you really want to delete the record? + * Yes: + * Delete record on server. + * Process server reponse 'redirect': + * 'client': Client redirects to previous page. + * 'no': Error message. + * 'url': Redirect to URL + * No: + * Client does not delete record on server. + * No redirect. Client stays on current page. + +New Button +---------- +* User presses the button + * Form not modified by user: Client redirects to href url. + * Form modified by user: Ask User 'You have unsaved changes. Do you want to save first?' + * Yes: + * Client reset all validation states + * Client validates HTML Form + * Form is submitted to server + * Success: + * Client: Ignore server reponse 'redirect'. Client redirects to href url. + * Failure: Happens on communication errors, if data validation fails, form actions fails or saving data fails. + * Show error message. + * Client: Ignore server reponse 'redirect'. Client stays on current page. + * No: + * Client does not save the modified record. + * Client redirects to href url. + * Cancel: + * Client does not save the modified record. + * Client stays on current page. + + +File Handling: Upload +--------------------- +* No previous uploaded file present + 1. User presses the Browse button + 1. User selects file + 1. File is uploaded to qfq immediately + 1. Browse button gets disabled and hidden + 1. File delete button is shown + 1. User cancels file selection + 1. no action +* Previous uploaded file present + 1. User deletes file + 1. File delete button gets disabled and hidden + 1. Browse button gets enabled and displayed + diff --git a/extension/Classes/Controller/QfqController.php b/extension/Classes/Controller/QfqController.php index e9d5583327103f34b6227cef337701f74c4eccdf..2601a46e3389e1452000823040c945c4b786495c 100644 --- a/extension/Classes/Controller/QfqController.php +++ b/extension/Classes/Controller/QfqController.php @@ -14,11 +14,19 @@ require_once(__DIR__ . '/../../qfq/qfq/exceptions/CodeException.php'); require_once(__DIR__ . '/../../qfq/qfq/exceptions/DbException.php'); class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController { + public function showAction() { + $origErrorReporting = ''; try { $contentObject = $this->configurationManager->getContentObject(); + + // By T3 default 'E_NOTICE' is unset. E.g. 'Undefined Index' will throw an exception. + // QFQ like to see those 'E_NOTICE' + $origErrorReporting = error_reporting(); + error_reporting($origErrorReporting | E_NOTICE); + $qfq = new \qfq\QuickFormQuery($contentObject->data); $html = $qfq->process(); @@ -34,6 +42,9 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController { $html = "Generic Exception: " . $e->getMessage(); } + // Restore has to be outside of try/catch - E_NOTICE needs to unset for further T3 handling after an QFQ Exception. + error_reporting($origErrorReporting); + $this->view->assign('qfqOutput', $html); return $this->view->render(); } diff --git a/extension/Configuration/PageTSconfig/PageTSconfig.ts b/extension/Configuration/PageTSconfig/PageTSconfig.ts index 2383aefc58f91b9aa2a17d9f042b67ffbb400d83..9da2d2f6b7777844aa4a84094e499927c637d534 100644 --- a/extension/Configuration/PageTSconfig/PageTSconfig.ts +++ b/extension/Configuration/PageTSconfig/PageTSconfig.ts @@ -4,27 +4,26 @@ mod.wizards.newContentElement { special.elements { - sampleextension_element + qfq_element { icon = icon goes here - title = Sample + title = QFQ Content Element Content Element - description = More + description = Quick Form Query (QFQ) offers a Form Editor and a SQL based Report Language. info goes here tt_content_defValues { - CType = sampleextension_samplepluginname + CType = qfq_qfq } } } special.show : - = addToList(sampleextension_element) + = addToList(qfq_element) } } - diff --git a/extension/Documentation/AdministratorManual/Index.rst b/extension/Documentation/AdministratorManual/Index.rst index 8b25d60907ea91100089710d72ee93aebe2f71af..25c343fe52478c4ce0b12b335495b375697aa2d9 100644 --- a/extension/Documentation/AdministratorManual/Index.rst +++ b/extension/Documentation/AdministratorManual/Index.rst @@ -20,19 +20,32 @@ native driver (see also: http://dev.mysql.com/downloads/connector/php-mysqlnd/): * mysqli::get_result (important), * mysqli::fetch_all (nice to use) -Installation for Ubuntu:: +Preparation for Ubuntu 14.04:: sudo apt-get install php5-mysqlnd sudo php5enmod mysqlnd sudo service apache2 restart +Preparation steps for Ubuntu 16.04:: + + none + Setup ----- * Install the extension via the Extensionmanager. -* Rename the file *<ext_dir>/config.example.ini* to *<ext_dir>/config.ini* and configure the necessary values: `<ext_dir>/config.ini`_ + + * If you install the extension by manual download/upload and get an error message + "can't activate extension": rename the downloaded zip file to `qfq.zip` or `qfq_<version>.zip` (e.g. version: 0.9.1). + + * If the Extensionmanager stops after importing: check your memory limit in php.ini. + +* Enable the online Documentation_. +* Copy/rename the file *<Documentroot>/typo3conf/ext/<ext_dir>/config.example.qfq.ini* to + *<Documentroot>/typo3conf/config.qfq.ini* and configure the necessary values: `config.qfq.ini`_ + The configuration file is outside the extension directory to not loose it during updates. * Play the SQL File *<ext_dir>/qfq/sql/formEditor.sql* to fill the database with the *FormEditor* records. -* Configure Typoscript to include Bootstrap, jQuery and QFQ javascript and CSS files. +* Configure Typoscript to include Bootstrap, jQuery, QFQ javascript and CSS files. :: @@ -41,7 +54,7 @@ Setup file1 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css file2 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css file3 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css - file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.darkblue.css + file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.bootstrap.css file5 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css } @@ -51,8 +64,9 @@ Setup file2 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js file3 = typo3conf/ext/qfq/Resources/Public/JavaScript/validator.min.js file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js + file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/tinymce.min.js file5 = typo3conf/ext/qfq/Resources/Public/JavaScript/EventEmitter.min.js - file6 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.debug.js + file6 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.min.js } FormEditor @@ -61,9 +75,9 @@ Setup a *report* to manage all *forms*: Create a Typo3 page and insert a content :: - form = {{form}} + form = {{form:T}} 10 { - sql = SELECT CONCAT('{{pageId}}&form=Form&r=', f.id) as Pagee, f.id, f.name, f.title, f.tableName FROM FormEditor As f ORDER BY f.name + sql = SELECT CONCAT('{{pageId}}&form=Form&r=', f.id) as Pagee, f.id, f.name, f.title, f.tableName FROM Form AS f ORDER BY f.name head = <br><table class="table"> tail = </table> rbeg = <tr class="table-hover"> @@ -72,40 +86,48 @@ Setup a *report* to manage all *forms*: Create a Typo3 page and insert a content fend = </td> } -<ext_dir>/config.ini --------------------- - -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| Keyword | Example | Description | -+========================+==================================+============================================================================+ -| DB_USER | DB_USER=qfqUser | Credentials configured in MySQL | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| DB_PASSWORD | DB_PASSWORD=12345678 | Credentials configured in MySQL | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| DB_SERVER | DB_SERVER=localhost | Hostname of MySQL Server | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| DB_NAME | DB_NAME=qfq_db | Database name | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| DB_NAME_TEST | DB_NAME_TEST=qfq_db_test | Used during development of QFQ | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| SESSION_NAME | SESSION_NAME=qfq | PHP Session name, by default 'qfq' | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| SQL_LOG | SQL_LOG=sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| SQL_LOG_MODE | SQL_LOG_MODE=modify | *all*: every statement will be logged - this is a lot | -| | | *modify*: log only statements who change data | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| SHOW_DEBUG_INFO | SHOW_DEBUG_INFO=auto | Possible values: auto|yes|no. For 'auto': If a BE User is logged in, | -| | | debug information will be shown on the fronend. | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| CSS_LINK_CLASS_INTERNAL| CSS_LINK_CLASS_INTERNAL=internal | CSS class name of links which points to internal tagets | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| CSS_LINK_CLASS_EXTERNAL| CSS_LINK_CLASS_EXTERNAL=external | CSS class name of links which points to internal tagets | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ -| DATE_FORMAT | DATE_FORMAT= yyyy-mm-dd | Possible options: yyyy-mm-dd, dd.mm.yyyy | -+------------------------+----------------------------------+----------------------------------------------------------------------------+ - -Example: *<ext_dir>/config.ini* + +config.qfq.ini +-------------- + ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| Keyword | Example | Description | ++=========================+=========================================+============================================================================+ +| DB_USER | DB_USER=qfqUser | Credentials configured in MySQL | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_PASSWORD | DB_PASSWORD=12345678 | Credentials configured in MySQL | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_SERVER | DB_SERVER=localhost | Hostname of MySQL Server | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_NAME | DB_NAME=qfq_db | Database name | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_NAME_TEST | DB_NAME_TEST=qfq_db_test | Used during development of QFQ | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_INIT | DB_INIT=set names utf8 | Global init for using the database. | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SQL_LOG | SQL_LOG=sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SQL_LOG_MODE | SQL_LOG_MODE=modify | *all*: every statement will be logged - this is a lot | +| | | *modify*: log only statements who change data | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SHOW_DEBUG_INFO | SHOW_DEBUG_INFO=auto | Possible values: auto|yes|no. For 'auto': If a BE User is logged in, | +| | | debug information will be shown on the fronend. | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_LINK_CLASS_INTERNAL | CSS_LINK_CLASS_INTERNAL=internal | CSS class name of links which points to internal tagets | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_LINK_CLASS_EXTERNAL | CSS_LINK_CLASS_EXTERNAL=external | CSS class name of links which points to internal tagets | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_CONTAINER |CSS_CLASS_QFQ_CONTAINER=container | QFQ with own Bootstrap: 'container'. | +| | | QFQ already nested in Bootstrap of mainpage: <empty> | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_FORM_PILL |CSS_CLASS_QFQ_FORM_PILL=qfq-color-grey-1 | Wrap around title bar for pills: CSS Class, typically a background color | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_FORM_BODY |CSS_CLASS_QFQ_FORM_BODY=qfq-color-grey-2 | Wrap around formelements: CSS Class, typically a background color | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DATE_FORMAT | DATE_FORMAT= yyyy-mm-dd | Possible options: yyyy-mm-dd, dd.mm.yyyy | ++-------------------------+-----------------------------------------+----------------------------------------------------------------------------+ + +Example: *typo3conf/config.qfq.ini* :: @@ -114,8 +136,31 @@ Example: *<ext_dir>/config.ini* DB_PASSWORD = 12345678 DB_NAME = qfq_db DB_NAME_TEST = qfq_db_test - SESSION_NAME = qfq + DB_INIT = set names utf8 SQL_LOG = sql.log SHOW_DEBUG_INFO = auto CSS_LINK_CLASS_INTERNAL = internal - CSS_LINK_CLASS_EXT = external \ No newline at end of file + CSS_LINK_CLASS_EXT = external + CSS_CLASS_QFQ_CONTAINER = + CSS_CLASS_QFQ_FORM_PILL = qfq-color-grey-1 + CSS_CLASS_QFQ_FORM_BODY = qfq-color-grey-2 + +Documentation +------------- + +To render the QFQ reST documentation: + +* Take care to have 'unzip' and 'Python setuptools' installed (necessary to run). + +Preparation for Ubuntu 16.04:: + + sudo apt install unzip python-setuptools + +* Install the extension "Sphinx Python Documentation Generator and Viewer" (sphinx). + + * Execute the update script (symbol 'two arrows as a circle' behind the extension name) + * Choose 'Sphinx 1.4.4' - click on 'Import'. + +* In the Exension Manager open the configuration dialog of the extension 'sphinx'. Activate the 'Sphinx 1.4.4' option and save it. +* On top of the browser window click on the 'question mark' to open the menu, choose 'Sphinx'. +* Show doumentation 'QFQ Extension' diff --git a/extension/Documentation/Index.rst b/extension/Documentation/Index.rst index 203bc5231136743d37558a661c8b374d4c3286d8..2d8675aaf8da1a1d2d876e06404b365254c28497 100644 --- a/extension/Documentation/Index.rst +++ b/extension/Documentation/Index.rst @@ -32,7 +32,7 @@ QFQ Extension Quick Form Query, Form, Report, SQL, Query, Generator. :Copyright: - 2016 + 2017 :Author: Carsten Rose, Rafael Ostertag diff --git a/extension/Documentation/Settings.yml b/extension/Documentation/Settings.yml index 6a6bcfe032b9d209ffbe1535f15d38b98eb2e60f..a426140571cfc741dfe5d7645f02ba94a62a3690 100644 --- a/extension/Documentation/Settings.yml +++ b/extension/Documentation/Settings.yml @@ -4,10 +4,10 @@ --- conf.py: - copyright: 2016 + copyright: 2017 project: QFQ Extension - version: 0.3 - release: 0.3.0 + version: 0.10 + release: 0.10.0 latex_documents: - - Index - qfq.tex diff --git a/extension/Documentation/UsersManual/Index.rst b/extension/Documentation/UsersManual/Index.rst index 89c7615ab97bda85d48141ae27e708f79b3526d5..7b9f910c420b15596061c285979cd480fa54588d 100644 --- a/extension/Documentation/UsersManual/Index.rst +++ b/extension/Documentation/UsersManual/Index.rst @@ -1,5 +1,12 @@ .. ================================================== -.. FOR YOUR INFORMATION +.. Header hierachy +.. == +.. -- +.. ^^ +.. '' +.. ;; +.. ,, +.. .. -------------------------------------------------- .. -*- coding: utf-8 -*- with BOM. @@ -19,10 +26,6 @@ Features not implemented yet ---------------------------- * Multi Forms -* FormElement: - - * type=action (especially not *addNupdate*) - * Checkbox: some combinations not tested. QFQ content element ------------------- @@ -36,6 +39,7 @@ The title of the QFQ content element will not be rendered. It's only visible in QFQ Keywords (Bodytext) ^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------+---------------------------------------------------------------------------------+ | Name | Explanation | +===================+=================================================================================+ @@ -44,8 +48,9 @@ QFQ Keywords (Bodytext) | | * by SIP: **form = {{form}}** | | | * by SQL: **form = {{SELECT c.form FROM conference AS c WHERE c.id={{a:C}} }}** | +-------------------+---------------------------------------------------------------------------------+ - | r | recordId. If specified, the form will load the record with the specified id | - | | * Fix. E.g.: **r = 123**, by SQL: **r = {{SELECT ...}}** | + | r | recordId. The form will load the record with the specified id | + | | * Variants: **r = 123**, by SQL: **r = {{SELECT ...}}** | + | | * If not specified, the default is '0' | +-------------------+---------------------------------------------------------------------------------+ | <level>.db | Select a DB. Only necessary if a different than the standard DB should be used. | +-------------------+---------------------------------------------------------------------------------+ @@ -104,16 +109,17 @@ Debug Form ---- -* Forms will be created by using the *Form editor* (HTML form). The Formeditor itself consist of two regular QFQ forms: *form* and *formElement* +* Forms will be created by using the *Form editor* on the Typo3 frontend (HTML form). +* The Formeditor itself consist of two predefined QFQ forms: *form* and *formElement* * Every form consist of a) a *Form* record and b) multiple *FormElement* records. * A form is assigned to a *table*. Such a table is called the *primary table* for this form. -* There are three types of forms: +* There are three types of forms which can roughly categorized into: * *Simple* form: the form acts on one record, stored in one table. - * The form will create necessary SQL commands for insert, update and delete automatically. + * The form will create necessary SQL commands for insert, update and delete (only primary record) automatically. - * *Advanced* form: the form acts on one record, stored in more than one table. + * *Advanced* form: the form acts on multiple records, stored in more than one table. * Fields of the primary table acts like a *simple* form, all other fields have to be specified with *addNupdate* records. @@ -136,15 +142,19 @@ Most fields of a form specification might contain: * A variable (or SQL) statement is surrounded by curly braces: - *{{VarName[:<store / prio>[:<sanitize class>]]}}* + *{{VarName[:<store / prio>[:<sanitize class>[:<escape>]]]}}* * Example: - *{{recordid}}* + *{{r}}* + + *{{index:FS}}* + + *{{name:FS:alnumx:s}}* *{{SELECT name FROM person WHERE id=1234}}* - *{{SELECT name FROM person WHERE id={{recordid}} }}* + *{{SELECT name FROM person WHERE id={{r}} }}* *{{SELECT name FROM person WHERE id={{key1:C:alnumx}} }}* @@ -153,9 +163,9 @@ Most fields of a form specification might contain: * *{{ SELECT "Hello World" }}* acts as *{{SELECT "Hello World"}}* * *{{ varname }}* acts as *{{varname}}* + * There are several stores, from where to retrieve the value. If a value is not found in one store, the next store is searched, until a value is found or there are no more stores available. -* sdkf * If anywhere along the line an empty string is found, this **is** a value: therefore, the search will stop. * If no value is found, the value is an <empty string>. @@ -163,7 +173,20 @@ URL Parameter ------------- * URL (=GET) Parameter can be used in *forms* and *reports* as variables. -* If a value violates a parameter sanitize class, an exception is thrown. +* If a value violates a parameter sanitize class, the value becomes an empty string. + +Escape +------ + +* Variables used in SQL Statements might cause trouble, if they contain single or double ticks. +* Escaping of single or double is defined by the parameter <escape> (fourth parameter): + + * 's' - single ticks will be escaped. + * 'd' - double ticks will be escaped. + +* It's not possible to escape single and double ticks at the same time. +* Which of them to escape (single or double) depends on the surrounding SQL query. +* Escaping is only necessary inside of SQL queries. Sanitize class -------------- @@ -175,14 +198,26 @@ Sanitize class the default class is 'digit'. * A default sanitize class can be overwritten by individual definition: *{{a:C:all}}* - * **alnumx**: [A-Za-z][0-9]@-_.,;: /() - * **digit**: [0-9].-+ - * **email**: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} - * **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 ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| Name | Form | Query | Pattern | ++==================+======+=======+=========================================================================================+ +| **alnumx** | Form | Query | [A-Za-z][0-9]@-_.,;: /() | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **digit** | Form | Query | [0-9].-+ | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **email** | Form | Query | [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **min|max** | Form | | Compares the value against an lower and upper limit (numeric or string). | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **min|max date** | Form | | Compares the value against an lower and upper date or datetime. | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **pattern** | Form | | Compares the value against a regexp. | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **allbut** | Form | Query | All characters allowed, but not [ ] { } % & \ #. The used regexp: '^[^\[\]{}%&\\#]+$', | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ +| **all** | Form | Query | no sanitizing | ++------------------+------+-------+-----------------------------------------------------------------------------------------+ + Store / prio @@ -193,12 +228,12 @@ Only variables that are known in a specified store can be substituted. +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ |Name |Description | Content | +=====+========================================================================================+============================================================================+ - | F | Form: data not saved in database yet. | All native form elements. Recent values from the Browser. | + | F | :ref:`STORE_FORM`: data not saved in database yet. | All native form elements. Recent values from the Browser. | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | S | SIP: Client parameter 's' will indicate the current SIP, which will be loaded from the | sip, r (record_id), form | - | | SESSION repo to the SIP-Store. | | + | S | :ref:`STORE_SIP`: Client parameter 's' will indicate the current SIP, which will be | sip, r (record_id), form | + | | loaded from the SESSION repo to the SIP-Store. | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | R | Record - the record that is going to be edited. For new records: empty. | All columns of the current record from the current table | + | R | :ref:`STORE_RECORD`: Record - the current record loaded in the form | All columns of the current record from the current table | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | P | Parent record. E.g.: on multi forms the current record of the outer query | All columns of the MultiSQL Statement from the table for the current row | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ @@ -206,11 +241,11 @@ Only variables that are known in a specified store can be substituted. +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | M | Column type: The *table.column* specified *type* | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | C | Client: POST variable, if not found: GET variable | Parameter sent from the Client (=Browser). | + | C | :ref:`STORE_CLIENT`: POST variable, if not found: GET variable | Parameter sent from the Client (=Browser). | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | T | Typo3: a) Bodytext (ttcontent record), b) Typo3 internal varibles like fe_user_uid, ...| See Typo3 tt_content record configuration | + | T | :ref:`STORE_TYPO3`: a) Bodytext (ttcontent record), b) Typo3 internal variables | See Typo3 tt_content record configuration | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | V | Vars - Generic variables | | + | V | :ref:`STORE_VARS`: Generic variables | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | 0 | Zero - allways value: 0, might be helpful if a variable is empty or undefined and will | All possible keys | | | be used in an SQL statement. | | @@ -218,7 +253,7 @@ Only variables that are known in a specified store can be substituted. | E | Empty - allways value: 0, might be helpful if a variable is empty or undefined and will| All possible keys | | | be used in an SQL statement | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ - | Y | System: a) Database credentials, b) helper vars for logging/debugging: | | + | Y | :ref:`STORE_SYSTEM`: a) Database, b) helper vars for logging/debugging: | | | | SYSTEM_SQL_RAW ... SYSTEM_FORM_ELEMENT_COLUMN | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ @@ -234,104 +269,190 @@ Only variables that are known in a specified store can be substituted. Predefined variable names ------------------------- -Store: *CLIENT* - C -^^^^^^^^^^^^^^^^^^^ - - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | Name | Explanation | - +===============+==========================================================================================================================================+ - | s | =SIP | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | r | record id. Typically stored in SIP, rarely specified on the URL | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | keySemId | always current Semester Id | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | keySemIdUser | *{{keySemIdUser}}*, may be changed by user | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | HTTP_HOST | current HTTP HOST | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | REMOTE_ADDR | Client IP address | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | '$_SERVER[*]' | All other variables accessable by *$_SERVER[]*. Only the often used have a pre-defined sanitize class. | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | form | Unique name of current form | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | ANREDE | *{{sex}}* == male >> Sehr geehrter Herr, *{{sex}}* == female Sehr geehrte Frau | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - | EANREDE | *{{sex}}* == male >> Dear Mr., *{{sex}}* == female >> Dear Mrs. | - +---------------+------------------------------------------------------------------------------------------------------------------------------------------+ - -Store: *TYPO3* (Bodytext) - T -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - +---------------+-------------------------------------------------------------------+ - | Name | Explanation | - +===============+===================================================================+ - | form | Formname defined in ttcontent record bodytext | - | | | - | | * Fix. E.g. *form = person* | - | | * via SIP. E.g. *form = {{form}}* | - +---------------+-------------------------------------------------------------------+ - | pageId | Record id of current Typo3 page | - +---------------+-------------------------------------------------------------------+ - | pageType | Current selected page type (typically URL parameter 'type') | - +---------------+-------------------------------------------------------------------+ - | pageLanguage | Current selected page language (typically URL parameter 'L') | - +---------------+-------------------------------------------------------------------+ - | ttcontentUid | Record id of current Typo3 content element | - +---------------+-------------------------------------------------------------------+ - | feUser | Logged in Typo3 FE User | - +---------------+-------------------------------------------------------------------+ - | feUserUid | Logged in Typo3 FE User uid | - +---------------+-------------------------------------------------------------------+ - | feUserGroup | FE groups of logged in Typo3 FE User | - +---------------+-------------------------------------------------------------------+ - +.. _STORE_FORM: Store: *FORM* - F ^^^^^^^^^^^^^^^^^ + * Represents the values in the form, typically before saving them. * Used for: * Formelements who will be rerendered, after a parent element has been changed by the user. * Formelement actions, before saving the form. - * Values will be sanatized by the class configured in corresponding the formelement. By default, the sanitize class is `alnumx`. + * Values will be sanitized by the class configured in corresponding the formelement. By default, the sanitize class is `alnumx`. - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | Name | Explanation | - +====================+============================================================================================================================================+ - | FormElement name | Name of native formelement. To get, exactly and only, the specified form element (for 'p_id'): *{{p_id:F}}* | - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Name | Explanation | + +=========================+============================================================================================================================================+ + | FormElement name | Name of native formelement. To get, exactly and only, the specified form element (for 'p_id'): *{{p_id:F}}* | + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +.. _STORE_SIP: + +Store: *SIP* - S +^^^^^^^^^^^^^^^^ + +* Filled automatically by creating links. E.g.: + + * in `Report` by using `_page?` or `_link` (with active 's') + * in `Form` by using subrecords: 'new', 'edit', 'delete' links (system) or by column type `_page?`, `_link`. + + +-------------------------+-----------------------------------------------------------+ + | Name | Explanation | + +=========================+===========================================================+ + | sip | 13 char uniqid | + +-------------------------+-----------------------------------------------------------+ + | r | current record id | + +-------------------------+-----------------------------------------------------------+ + | form | current form name | + +-------------------------+-----------------------------------------------------------+ + | table | current table name | + +-------------------------+-----------------------------------------------------------+ + | urlparam | all non Typo3 paramter in one string | + +-------------------------+-----------------------------------------------------------+ + | <user defined> | additional user defined link parameter | + +-------------------------+-----------------------------------------------------------+ + +.. _STORE_RECORD: Store: *RECORD* - R ^^^^^^^^^^^^^^^^^^^ - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | Name | Explanation | - +====================+============================================================================================================================================+ - | record column name | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form element: *{{p_id:R}}* | - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +* Current record loaded in Form. +* If r=0, alle values are empty. + + +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Name | Explanation | + +========================+============================================================================================================================================+ + | <column name> | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form element: *{{p_id:R}}* | + +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +.. _STORE_CLIENT: + +Store: *CLIENT* - C +^^^^^^^^^^^^^^^^^^^ + + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | Name | Explanation | + +=========================+==========================================================================================================================================+ + | s | =SIP | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | r | record id. Typically stored in SIP, rarely specified on the URL | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | keySemId | always current Semester Id | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | keySemIdUser | *{{keySemIdUser}}*, may be changed by user | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | HTTP_HOST | current HTTP HOST | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | REMOTE_ADDR | Client IP address | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | '$_SERVER[*]' | All other variables accessable by *$_SERVER[]*. Only the often used have a pre-defined sanitize class. | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | form | Unique name of current form | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | ANREDE | *{{sex}}* == male >> Sehr geehrter Herr, *{{sex}}* == female Sehr geehrte Frau | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + | EANREDE | *{{sex}}* == male >> Dear Mr., *{{sex}}* == female >> Dear Mrs. | + +-------------------------+------------------------------------------------------------------------------------------------------------------------------------------+ + +.. _STORE_TYPO3: + +Store: *TYPO3* (Bodytext) - T +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +-------------------------+-------------------------------------------------------------------+----------+ + | Name | Explanation | Note | + +=========================+===================================================================+==========+ + | form | Formname defined in ttcontent record bodytext | see note | + | | | | + | | * Fix. E.g. *form = person* | | + | | * via SIP. E.g. *form = {{form}}* | | + +-------------------------+-------------------------------------------------------------------+----------+ + | pageId | Record id of current Typo3 page | see note | + +-------------------------+-------------------------------------------------------------------+----------+ + | pageType | Current selected page type (typically URL parameter 'type') | see note | + +-------------------------+-------------------------------------------------------------------+----------+ + | pageLanguage | Current selected page language (typically URL parameter 'L') | see note | + +-------------------------+-------------------------------------------------------------------+----------+ + | ttcontentUid | Record id of current Typo3 content element | see note | + +-------------------------+-------------------------------------------------------------------+----------+ + | feUser | Logged in Typo3 FE User | | + +-------------------------+-------------------------------------------------------------------+----------+ + | feUserUid | Logged in Typo3 FE User uid | | + +-------------------------+-------------------------------------------------------------------+----------+ + | feUserGroup | FE groups of logged in Typo3 FE User | | + +-------------------------+-------------------------------------------------------------------+----------+ + +* **note**: not available + * in 'dynamicUpdate' or + * by FormElement class 'action' with type 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete'. + +.. _STORE_VARS: Store: *VARS* - V ^^^^^^^^^^^^^^^^^ - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ - | Name | Explanation | - +====================+============================================================================================================================================+ - | random | random string with length of 32 chars, alphanum | - +--------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | Name | Explanation | + +=========================+============================================================================================================================================+ + | random | random string with length of 32 chars, alphanum | + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ + | slaveId | see FormElement `action` | + +-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ -SQL ---- + +.. _STORE_SYSTEM: + +Store: *SYSTEM* - S +^^^^^^^^^^^^^^^^^^^ + + +-------------------------+------------------------------------------------------------+ + | Name | Explanation | + +=========================+============================================================+ + | DB_USER | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | DB_SERVER | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | DB_NAME | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | DB_INIT | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | SQL_LOG | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | SQL_LOG_MODE | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | SHOW_DEBUG_INFO | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | CSS_LINK_CLASS_INTERNAL | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | CSS_LINK_CLASS_EXTERNAL | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | CSS_CLASS_QFQ_CONTAINER | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | EXT_PATH | computed during runtime | + +-------------------------+------------------------------------------------------------+ + | SITE_PATH | computed during runtime | + +-------------------------+------------------------------------------------------------+ + | DATE_FORMAT | defined in config.ini | + +-------------------------+------------------------------------------------------------+ + | sqlFinal | computed during runtime, used for error reporting | + +-------------------------+------------------------------------------------------------+ + | sqlParamArray | computed during runtime, used for error reporting | + +-------------------------+------------------------------------------------------------+ + | sqlCount | computed during runtime, used for error reporting | + +-------------------------+------------------------------------------------------------+ + +SQL Statement +------------- * The detection of an SQL command is case *insensitive*. * Leading whitespace will be skipped. * The following commands are interpreted as SQL commands: * SELECT - * INSERT - * UPDATE - * DELETE - * SHOW + * INSERT, UPDATE, DELETE, REPLACE, TRUNCATE + * SHOW, DESCRIBE, EXPLAIN, SET * A SQL Statement might contain parameters, including additional SQL statements. Inner SQL queries will be executed first. * All variables will be substituted one by one from inner to outer. @@ -342,17 +463,17 @@ SQL * Example:: {{SELECT id, name FROM Person}} - {{SELECT id, name, IF({{fe_user}}=0,'Yes','No') FROM Vorlesung WHERE sem_id={{keySemId:Y}} }} - {{SELECT id, city FROM Address AS adr WHERE adr.p_id={{SELECT id FROM Account AS acc WHERE acc.name={{fe_user}} }} }} + {{SELECT id, name, IF({{feUser}}=0,'Yes','No') FROM Vorlesung WHERE sem_id={{keySemId:Y}} }} + {{SELECT id, city FROM Address AS adr WHERE adr.p_id={{SELECT id FROM Account AS acc WHERE acc.name={{feUser}} }} }} -* Special case for SELECT input fields. To deliver a result array specify an '!' before the SELECT: +* Special case for SELECT input fields. To deliver a result array specify an '!' before the SELECT: :: - *{{!SELECT ...}}* + {{!SELECT ...}} * This is only possible for the outermost SELECT. -Form: basic setup ------------------ +Form: main +---------- +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ | Name | Type | Description | @@ -371,10 +492,12 @@ Form: basic setup +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |permitEdit | enum('sip', 'logged_in', 'logged_out', 'always', 'never')| Default: sip | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ -|showButton | set('new', 'delete') | Default 'new,delete'. Displays button 'new' and 'delete'. | -+------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |render | enum('plain','table', 'bootstrap') | Default bootstrap | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ +|requiredParameter | string | Name of required SIP parameter, seperated by comma. '#' as comment delimiter | ++------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ +|showButton | set('new', 'delete', 'close', 'save') | Default 'new,delete,close,save'. Shown buttons in the upper right corner of the form. | ++------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |multiMode | enum('none','horizontal','vertical') | Default 'none' | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |multiSql | text | Optional. SQL Query which selects all records to edit. | @@ -390,8 +513,8 @@ Form: basic setup |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 | ++------------------------+----------------------------------------------------------+ Default: label=3, input=6, note=3 | +|bsNoteColumns | string | | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ |parameter | text | Misc additional parameters. See :ref:`form-parameter` | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ @@ -402,35 +525,112 @@ Form: basic setup |created | datetime | set once through QFQ | +------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ -Field: Form.showButton -^^^^^^^^^^^^^^^^^^^^^^ +showButton +^^^^^^^^^^ -Display or hide the button 'new' and / or 'delete'. +Display or hide the button `new`, `delete`, `close`, `save`. * *new*: Creates a new record. If the form needs any special parameter via SIP or Client, hide this 'new' button - the necessary parameter are not provided. -* *delete*: The simple form of deleting a record only deletes the record itself, not any child records. - +* *delete*: This either deletes the current record only, or (if defined via action form element 'before Delete' ) any specified subrecords. +* *close*: Close the current form. If there are changes, a popup opens and ask to save / close / cancel. The last page from the history will be shown. +* *save*: Save the form. +* Default: show all buttons. .. _form-parameter: -Field: Form.parameter -^^^^^^^^^^^^^^^^^^^^^ +parameter +^^^^^^^^^ * The following parameter are optional and can be configured in the *Form.parameter* field. -+------------------------+--------+---------------------------------------------------------------------------------------------------+ -| Name | Type | Description | -+========================+========+===================================================================================================+ -| maxVisiblePill | int | Show pills upto <maxVisiblePill> as button, all further in a dropdown menu. Eg.: maxVisiblePill=3 | -+------------------------+--------+---------------------------------------------------------------------------------------------------+ -| class | string | HTML div with given class, surrounding the whole form. Eg.: class=container-fluid | -+------------------------+--------+---------------------------------------------------------------------------------------------------+ ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| Name | Type | Description | ++========================+========+==========================================================================================================+ +| maxVisiblePill | int | Show pills upto <maxVisiblePill> as button, all further in a dropdown menu. Eg.: maxVisiblePill=3 | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| class | string | HTML div with given class, surrounding the whole form. Eg.: class=container-fluid | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| classPill | string | HTML div with given class, surrounding the `pill` title line. | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| classBody | string | HTML div with given class, surrounding all `form elements`. | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| submitButtonText | string | Show save button, with the <submitButtonText> at the bottom of the form | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| extraDeleteForm | string | Name of a form which specifies how to delete the primary record and optional slave records | ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ * Example: * maxVisiblePill = 5 * class = container-fluid + * classBody = qfq-form-right + +submitButtonText +'''''''''''''''' + +* Optional. +* Default: Empty +* Empty: a 'submit' button with a Bootstrap glyph 'check' symbol is rendered at the *top right corner* of the form. +* Non Empty: a 'submit' button, with <submitButtonText>, is rendered at the bottom of the form (without a 'check' glyph + symbol). Typically 'ShowButton: Save' will be unchecked to hide the regular save glyph symbol. + +class +''''' + +* Optional. +* Default: `container` +* Any CSS class name(s) can be specified. +* Check `typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css` for predefined classes. +* Typical use: adjust the floating rules of the form. + * See: http://getbootstrap.com/css/#overview-container + * Expand the form over the whole area: `container-fluid` + +classPill +''''''''' + +* Optional. +* Default: `qfq-color-grey-1` +* Any CSS class name(s) can be specified. +* Check `typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css` for predefined classes. +* Typical use: adjust the background color of the `pill title` area. +* Predefined background colors: `qfq-color-white`, `qfq-color-grey-1` (dark), `qfq-color-grey-2` (light), + `qfq-color-blue-1` (dark), `qfq-color-blue-2`. (light) +* `classPill` is only visible on forms with container elemants of type 'Pill'. + +classBody +''''''''' + +* Optional. +* Default: `qfq-color-grey-2` +* Any CSS class name(s) can be specified. +* Check `typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css` for predefined classes. +* Typical use: + + 1) adjust the background color of the `form element` area. + 1) make all form labels right align: `qfq-form-right`. + +* Predefined background colors: `qfq-color-white`, `qfq-color-grey-1` (dark), `qfq-color-grey-2` (light), + `qfq-color-blue-1` (dark), `qfq-color-blue-2`. (light) + +submitButtonText +'''''''''''''''' + +If specified and non empty, display a regular submit button at the bottom of the page with the given text. +This gives the form a ordinary HTML-form look'n' feel. With this option, the standard buttons on the top right border +should be hided to not confuse the user. + +extraDeleteForm +''''''''''''''' + +Depending on the database definition, it might be necessary to delete the primary record *and* corresponding slave records. +To not repeat such 'slave record delete definition', an 'extraDeleteForm' could be specified. If the user open's a record +in a form and clicks on the 'delete' button, a defined 'extraDeleteForm'-form will be used to delete primary and slave +records instead of using the current form. +E.g. if there are multiple different forms to work on the same table, all of theses forms might reference to the same +'extraDeleteForm'-form. This simplifies the maintenance. + +The 'extraDeleteForm' parameter might be specified for a 'form' and/or for 'subrecords' FormElements ------------ @@ -449,7 +649,7 @@ FormElements Class: Container ---------------- -* Pills are containers for 'fieldset' *and* 'native' Form-Elements. +* Pills are containers for 'fieldset' *and* / *or* 'native' Form-Elements. * Fieldsets are containers for 'native' Form-Elements Type: fieldset @@ -506,10 +706,10 @@ Class: Native |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') | +|type | enum('checkbox', 'date', 'time', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', 'gridJQW', 'text', 'editor', 'note', | +| | 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', 'beforeLoad', 'beforeSave', | +| | 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', 'afterInsert', 'afterUpdate', 'afterDelete', | +| | 'sendMail') | +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ |checkType | enum('min|max', 'pattern', | | | | 'number', 'email') | | @@ -555,7 +755,7 @@ Class: Native | | | 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'. | +|deleted | string | 'yes'|'no'. | +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ |modified | timestamp |updated autmatically through stored procedure | +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ @@ -563,85 +763,83 @@ Class: Native +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| Attribute | checkbox | dateJQW | datetimeJQW | gridJQW | hidden | input | note | password | radio | select | subrecord | textarea | timeJQW | upload | -+==================+==========+=========+=============+==========+========+=======+======+==========+=======+========+===========+==========+=========+========+ -|id |Internal id | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|formId |Form | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|containerId |Assign the Formelement to user defined fieldSet or pill | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|enabled |Formelement is active or not | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|name |Name of a column of the primary table. Formelements with a corresponding table will be saved automatically. | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|label |Label shown to the user. | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|mode |show, readonly, required, lock, disable. | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|class |native | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|type | checkbox | dateJQW | datetimeJQW | gridJQW | hidden | input | note | password | radio | select | subrecord | textarea | timeJQW | upload | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|checkType | | - | - | | | - | | - | | | | - | - | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|checkPattern | | - | - | | | - | | - | | | | - | - | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|onChange | - | - | - | | | - | | - | - | - | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|ord | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|tabindex | - | - | - | - | - | - | - | - | - | - | - | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|size | - | | | | | - | | - | - | - 2 | | - 1 | - | - ? | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|maxLength | 1 | | | | | - | | - | - 1 | | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|note | - | - | - | | | - | - | - | - | - | - | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|tooltip | - | - | - | | | - | | - | - | - | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|placeholder | | - | - | | | - | | | | | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|clientJs | | - | - | - | | - | | - | - | - | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|value | - | - | - | - | - | - | - | - | - | - | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|sql1 |? | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|sql2 |? | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -|Additional attributes in Field 'parameter'. Typically in key=value format. | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| type | checkbox | dateJQW | datetimeJQW | gridJQW | hidden | input | note | password | radio | select | subrecord | textarea | timeJQW | upload | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| accept |? | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| alt |? | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| autocomplete | | - | - | | | - | | | | | | - | - | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| autofocus | - | - | - | | | - | | - | - | - | | - | - | - | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| checkBoxMode | - | - | | | | | | | | | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| checked | - | | | | | - | | | - | | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| unchecked | - | | | | | - | | | - | | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| label2 | - | | | | | | | | - | | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| itemList | - | | | | | | | | - | - | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| emptyItemAtStart | | | | | | | | | | - | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| emptyItemAtEnd | | | | | | | | | | - | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| emptyHide | | | | | | | | | | - | | | | | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ -| accept | | | | | | | | | | | | | | - 3 | -+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+ ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| Attribute | checkbox | dateJQW | datetimeJQW | gridJQW | extra | text | note | password | radio | select | subrecord | timeJQW | upload | editor | ++==================+==========+=========+=============+==========+========+=======+======+==========+=======+========+===========+=========+========+========+ +|id |Internal id | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|formId |Form | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|containerId |Assign the Formelement to user defined fieldSet or pill | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|enabled |Formelement is active or not | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|name |Name of a column of the primary table. Formelements with a corresponding table will be saved automatically. | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|label |Label shown to the user. | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|mode |show, readonly, required, lock, disable. | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|class |native | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|type | checkbox | dateJQW | datetimeJQW | gridJQW | extra | text | note | password | radio | select | subrecord | timeJQW | upload | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|checkType | | - | - | | | - | | - | | | | - | | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|checkPattern | | - | - | | | - | | - | | | | - | | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|onChange | - | - | - | | | - | | - | - | - | | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|ord | - | - | - | - | - | - | - | - | - | - | - | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|tabindex | - | - | - | - | - | - | - | - | - | - | - | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|size | - | | | | | - | | - | - | - 2 | | - | - ? | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|maxLength | 1 | | | | | - | | - | - 1 | | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|note | - | - | - | | | - | - | - | - | - | - | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|tooltip | - | - | - | | | - | | - | - | - | | - | - | ? | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|placeholder | | - | - | | | - | | | | | | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|clientJs | | - | - | - | | - | | - | - | - | | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|value | - | - | - | - | - | - | - | - | - | - | | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|sql1 |? | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +|Additional attributes in Field 'parameter'. Typically in key=value format. | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| type | checkbox | dateJQW | datetimeJQW | gridJQW | extra | text | note | password | radio | select | subrecord | timeJQW | upload | editor | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| accept |? | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| alt |? | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| autocomplete | | - | - | | | - | | | | | | - | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| autofocus | - | - | - | | | - | | - | - | - | | - | - | - | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| checkBoxMode | - | - | | | | | | | | | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| checked | - | | | | | - | | | - | | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| unchecked | - | | | | | - | | | - | | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| label2 | - | | | | | | | | - | | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| itemList | - | | | | | | | | - | - | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| emptyItemAtStart | | | | | | | | | | - | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| emptyItemAtEnd | | | | | | | | | | - | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| emptyHide | | | | | | | | | | - | | | | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ +| accept | | | | | | | | | | | | | - 3 | | ++------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+---------+--------+--------+ * 1: A line break created every <size> elements. Easy way to make checkboxes or radio vertical instead of horizontal. * 2: Any number >1 makes the 'select' input 'multiple' ready. @@ -695,24 +893,27 @@ Checkboxes can be rendered in mode: Type: date ^^^^^^^^^^ - * Range datetime: '1000-01-01' to '9999-12-31' or '0000-00-00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) - * Optional: - * *dateFormat*: ; yyyy-mm-dd | dd.mm.yyyy +* Range datetime: '1000-01-01' to '9999-12-31' or '0000-00-00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) +* Optional: + * *dateFormat*: ; yyyy-mm-dd | dd.mm.yyyy Type: datetime ^^^^^^^^^^^^^^ - * Range datetime: '1000-01-01 00:00:00' to '9999-12-31 23:59:59' or '0000-00-00 00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) - * Optional: - * *dateFormat*: ; yyyy-mm-dd | dd.mm.yyyy - * *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 '0000-00-00 00:00:00' is displayed. - -Type: hidden -^^^^^^^^^^^^ +* Range datetime: '1000-01-01 00:00:00' to '9999-12-31 23:59:59' or '0000-00-00 00:00:00'. (http://dev.mysql.com/doc/refman/5.5/en/datetime.html) +* Optional: + * *dateFormat*: ; yyyy-mm-dd | dd.mm.yyyy + * *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 '0000-00-00 00:00:00' is displayed. -Type: input +Type: extra ^^^^^^^^^^^ +* Element is not shown in the browser. +* The element can be used to define / precalculate values for a column, which do not already exist as a native FormElement. +* The element is build /computed on form load. + +Type: text +^^^^^^^^^^ * General input for text and number. * size: @@ -720,9 +921,40 @@ Type: input * <number>: width of input element in characters. Lineheight = 1. * <cols>,<rows>: input element = textarea, width=<cols>, height=<rows> +Type: editor +^^^^^^^^^^^^ + +* TinyMCE (https://www.tinymce.com, community edition) is used as the QFQ Rich Text Editor. +* The content will be saved as HTML inside the database. +* All configuration and plugins will be configured via the 'parameter' field. Just prepend the word 'editor-' in front + of each TinyMCE keyword. Check possible options under https://www.tinymce.com/docs/configure/, + https://www.tinymce.com/docs/plugins/, https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols +* Bars: + + * Top: *menubar* - by default hidden. + * Top: *toolbar* - by default visible. + * Bottom: *statusbar* - by default hidden, exception: *min_height* and *max_height* are given via size parameter. + + +* The default setting is:: + + editor-plugins=code link searchreplace table textcolor textpattern visualchars + editor-toolbar=code searchreplace undo redo | styleselect link table | fontselect fontsizeselect | bullist numlist outdent indent | forecolor backcolor bold italic editor-menubar=false + editor-statusbar=false + +* size: + + * <min_height>,<max_height>: in pixels, including top and bottom bars. E.g.: 300,600 + + Type: note ^^^^^^^^^^ +Type: password +^^^^^^^^^^^^^^ + +* Like a `text` element, but every character is shown as a asterisk. + Type: radio ^^^^^^^^^^^ @@ -788,37 +1020,31 @@ Type: select Type: subrecord ^^^^^^^^^^^^^^^ -'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 +The FormElement type 'subrecord' renders a list of records (so called secondary records), typically to show, edit, delete +or add new 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. * *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 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]* + * 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 not be visible to the user, it has to be named **'_id'**. + * Columnname: *[title=]<title>[|[width=]<number>][|nostrip][|icon][|link][|url][|mailto][|_rowClass][|_rowTitle]* - * *<number>*: any 'digit only' will be treated as '''width'''. - * *width=<number>*: max. number of chars displayed per cell in the column. + * All parameter are position independet. + * Separate parameter by '|'. + * *[title=]<text>*: Title of the column. The keyword 'title=' is optional. Columns with a title starting with '_' won't be rendered. + * *[width=]<number>*: Max. width of chars displayed per cell. The keyword 'width=' is optional. Default max width: 20. + This setting also affects the title of 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. + layout. 'nostrip' deactivates the cleaning to make pure html 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:: - - 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' ... - + will omit an html image tag (=nothing rendered in the cell). + * *link*: value will be rendered as described under :ref:`column-link` + * *url*: value will be rendered as a href url. + * *mailto*: value will be rendered as a href mailto. * *_rowClass* * The value is a CSS class name(s) which will be rendered in the *<tr class="<_rowClass>">* of the subrecord table. @@ -832,11 +1058,18 @@ Type: subrecord * Defines the title attribute of a subrecod table row (tooltip). + * Examples:: + + 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', + ELT(status,'info','warning','danger') AS '_rowClass', help AS '_rowTitle' ... + * *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. + * *extraDeleteForm*: Optional. The per row delete Button will reference such form (for deleting) instead of *form* (default). * *detail*: Mapping of values from the primary form to the target form (defined via `form=...`). * Syntax:: @@ -846,23 +1079,14 @@ Type: subrecord * 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'. + field *value* by changing the default Store priority definition. 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 -^^^^^^^^^^^^ - -Type: submit -^^^^^^^^^^^^ - -Typically not used. Useful if user wishes an explicit 'Submit' Button. - Type: time ^^^^^^^^^^ @@ -878,102 +1102,208 @@ Type: upload * 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 file browse button is displayed, if there is no file uploaded already. +The trash button is displayed, if there is file uploaded already. 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. +Either the user is satisfied now or the user can delete the uploaded file (and maybe upload another one). 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. +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:: + * *fileDestination*: Destination where to copy the file. A good practice is to specify a relative `fileDestination` - such an + installation (filesystem and database) are moveable. + + * If `fileDestination` should contain the original filename, the variable *{{_filename}}* can be used. Example :: + + fileDestination={{SELECT 'fileadmin/user/pictures/', p.name, '-{{_filename}}' FROM Person AS p WHERE p.id={{r}} }} + + * If a file already exist under `fileDestination`, an error message is shown and 'save' is aborted. + The user has no possibility to overwrite the already existing file. If the whole workflow is correct, this situation + should no arise. + + * All necessary subdirectories in `fileDestination` are automatically created. - pathFileName={{SELECT 'fileadmin/user/pictures/', p.name, '-{{_filename}}' FROM Person AS p WHERE p.id={{r}} }} + * Using the current record id in the `fileDestination`: Using {{r}} is problematic for new records: that one is still '0' + at the time of saving. Use `{{id:R0}}` instead. +Deleting a record and the referenced file +''''''''''''''''''''''''''''''''''''''''' + +If the user deletes a record which contains reference(s) to files, such files are deleted too. + +Only columns where the columname contains `pathFileName` are checked for file references. Therefore, always choose a +columnanem which contains `pathFileName`. + +If there are other records, which references the same file, such files are not deleted (but the record is deleted). +It's a very basic check: just the current column of the current table is compared. .. _class-action: Class: Action ------------- -Type: before load -^^^^^^^^^^^^^^^^^ +Type: before... | after... +^^^^^^^^^^^^^^^^^^^^^^^^^^ -* Former: formallow -* Function: a) fire SQL, b) allow / deny access -* respects 'processRow' +These type of 'action' formelements will be used to implement data validation or creating/updating additional records. -Type: after load -^^^^^^^^^^^^^^^^ +Types: -* Probably not implemented: no usecase. -* Function: fire SQL -* respects 'processRow' + * beforeLoad -Type: before save -^^^^^^^^^^^^^^^^^ + * good to grant access permission. -* Former: lookup -* Function: a) fire SQL, b) allow / deny access -* respects 'processRow' + * afterLoad + * beforeSave -Type: after save -^^^^^^^^^^^^^^^^ + * good to prohibit creating of duplicate records. -* Maybe successor of *addnupdate* -* Function: fire SQL -* respects 'processRow' + * afterSave -Type: before /after insert -^^^^^^^^^^^^^^^^^^^^^^^^^^ + * good to create & update additional records. -* Function: a) fire SQL, b) (before) allow / deny access -* respects 'processRow' + * beforeInsert + * afterInsert + * beforeUpdate + * afterUpdate + * beforeDelete + * afterDelete -Type: before /after update -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Validate +'''''''' -* Function: a) fire SQL, b) (before) allow / deny access -* respects 'processRow' + Perform checks by fireing a SQL query and expecting a predefined number of selected records. -Type: before / after delete -^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * OK: the `expectRecords` number of records has been selected. Continue processing the next FormElement. + * Fail: the `expectRecords` number of records has not been selected (less or more): Display the error message + `messageFail` and abort the whole (!) current form load or save. -* Function: a) fire SQL, b) (before) allow / deny access -* respects 'processRow' + FormElement.’‘’parameter’‘’: -Type: addnupdate -^^^^^^^^^^^^^^^^ + * `requiredList` - List of `native`-formelement names: only if all of those elements are filled (!=0 and !=''), the *current* + `action`-FormElement will be processed. This will enable or disable the check, based on the user input! If no + `native`-formelement names are given, the specified check will always be performed. + * `sqlValidate` - validation query. E.g.: `sqlValidate={{!SELECT id FROM Person AS p WHERE p.name LIKE {{name:F:all}} AND p.firstname LIKE {{firstname:F:all}} }}` + + * Pay attention to '{{!...' after the equal sign. + + * `expectRecords` - number of expected records. + + * `expectRecords=0` or `expectRecords=0,1` or `expectRecords={{SELECT COUNT(id) FROM Person}}` + * Separate multiple valid record numbers by ','. If at least one of those matches, the check will pass successfully. + + * `messageFail` - Message to show. E.g.: `messageFail=There is already a person called {{firstname:F:all}} {{name:F:all}}` + +sqlInsert / sqlUpdate / sqlDelete +''''''''''''''''''''''''''''''''' + + * Save values of a form to different record(s), optionally on different table(s). + * Typically usefull on 'afterSave' - be carefull when using it earlier, e.g. beforeLoad. + +FormElement.’‘’parameter’‘’: + + * `requiredList` - List of `native`-FormElements: only if all of those elements are filled, the current + `action`-FormElement will be processed. + * `slaveId`: + + * Auto fill: name the action `action`-FormElement equal to an existing column (table from the current form definition). + `slaveId` will be automatically filled with the value of the named column. + + * If there is no such named columnname, set `slaveId=0`. + + * Explicit definition: `slaveId=123` or `slaveId={{SELECT id ...}}` + + * `sqlInsert`: fired if `slaveId=0` or `slaveId=''`. + * `sqlUpdate`: fired if `slaveId>0`: + * `sqlDelete`: always fired (after sqlInsert or sqlUpdate) - the definition, when this query is fired, might change in + the future. + +Note: + + * `{{slaveId:V}}` can be used in any query as the current slaveId. It's *important* to Specify Store V! + + * If the `action`-FormElement name exist as a column in the master record: Update that column *automatically* with the + recent slaveId (after an INSERT the last_insert_id() acts as the new `slaveId`). + + +Example +''''''' + +Situation 1: master.x_id=slave.id (1:1) + + * Name the action element 'x_id': than {{slaveId}} will be automatically set to the value of 'master.x_id' + + * {{slaveId}} == 0 ? 'sqlInsert' will be fired. + * {{slaveId}} != 0 ? 'sqlUpdate' will be fired. + + * In case of fireing 'sqlInsert', the 'slave.id' of the new created record are copied to master.x_id (the database will + be updated automatically). + + * If the automatic update of the master record is not suitable, the action element should have no name or a name + which does not exist as a column of the master record. Define `slaveId={{SELECT id ...}}` + +Situation 2: master.id=slave.x_id (1:n) + + * Name the action element different to any columnname of the master record (or no name). + * Determine the slaveId: `slaveId={{SELECT id FROM slave WHERE slave.xxx={{...}} LIMIT 1}}` + + * {{slaveId}} == 0 ? 'sqlInsert' will be fired. + * {{slaveId}} != 0 ? 'sqlUpdate' will be fired. -* Probably not implemented: no usecase. Probably replaced by after save | after insert. Depends on functionality of 'after ...'. Type: sendmail ^^^^^^^^^^^^^^ -* Send mail(s) on request. -* respects 'processRow' +* Send mail(s) after saving the record. + +* FormElement.'''value''': Body of the email. + +* FormElement.’‘’parameter’‘’: + + * ‘’‘sendMailTo‘’‘ - Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>. If there + is no recipient email address, no mail will be sent. + * ‘’‘sendMailCc‘’‘ - Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>. + * ‘’‘sendMailBcc‘’‘ - Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>. + * ‘’‘sendMailFrom‘’‘ - Sender of the email. Optional: 'realname <john@doe.com>'. **Mandatory**. + * ‘’‘sendMailSubject‘’‘ - Subject of the email. + * ‘’‘sendMailReplyTo‘’‘ - Reply this email address. Optional: 'realname <john@doe.com>'. + * ‘’‘sendMailFlagAutoSubmit‘’‘ - **on|off** - If 'on', the mail contains the header 'Auto-Submitted: auto-send' - suppress OoO replies. + * ‘’‘sendMailGrId‘’‘ - Will be copied to the mailLog record. Helps to setup specific logfile queries. + * ‘’‘sendMailXId‘’‘ - Will be copied to the mailLog record. Helps to setup specific logfile queries. + +* To use values of the submitted form, use the STORE_FORM. E.g. `{{name:F:allbut}}` +* To use the `id` of a new created or already existing one, use the STORE_RECORD. E.g. `{{id:R}}` + + .. _dynamic-update: Dynamic Update -------------- -Depending of the form requirements, it's necessary to update FormElements depending on the recent user input. E.g. a user -activates a checkbox and therefore the content of a select list should change. +The 'Dynamic Update' feature makes a form more interactive. If a user change a FormElement who is tagged with +'dynamicUpdate', *all* elements who are tagged with 'DynamicUpdate', will be recalculated and rerendered. + +The following fields will be recalculated during 'Dynamic Update' + * 'modeSql' - Possible values: 'show', 'required', 'readonly', 'hidden' + * 'value' + * 'parameter.*' - especially 'itemList' -Make a form dynamic: +To make a form dynamic: -* Mark all FormElements with {dynamic update}=enabled, which should send **or** receive a 'do update' signal. -* Define the receiving FormElements in a way, that they will interpret the recent user change. In the example, the FormElement - 'carPriceRange' is the sender and the receiving FormElement might be of type 'radio' or 'selectList'. + * Mark all FormElements with {dynamic update}=enabled, which should send **or** receive a 'do update' signal. + * Define the receiving FormElements in a way, that they will interpret the recent user change. The form variable of the + specific sender FormElement `{{<sender element>:F:<sanitize>}}` should be part of one of the above fields to get an + impact. E.g.: + :: - [receiving formElement].parameter: itemList={{ SELECT IF({{carPriceRange}}='expensive','Ferrari,Tesla,Jaguar','General Motors,Honda,Seat,Fiat') }} + [receiving formElement].parameter: itemList={{ SELECT IF({{carPriceRange:FE:alnumx}}='expensive','Ferrari,Tesla,Jaguar','General Motors,Honda,Seat,Fiat') }} +* Label and Description are not 'Dynamic Update' aware (Feature Request #2081). Report ====== @@ -1088,7 +1418,6 @@ See the example below: :: - 10.sql = SELECT id AS _person_id, CONCAT(first_name, " ", last_name, " ") AS name FROM person 10.rsep = <br /> @@ -1109,15 +1438,16 @@ This would result in .. -Across several lines -^^^^^^^^^^^^^^^^^^^^ +Text across several lines +^^^^^^^^^^^^^^^^^^^^^^^^^ -To make SQL quieres more readable, it's possible to split a line across several lines. Lines with keywords are on their -own - if a line is not a 'keyword' line, it will be appended at the last keyword line. 'Keyword' lines are detected on: +To make SQL queries, or QFQ records in general, more readable, it's possible to split a line across several lines. Lines +with keywords are on their own (`QFQ Keywords (Bodytext)`_) starts a new line - if a line is not a 'keyword' line, it will +be appended at the last keyword line. 'Keyword' lines are detected on: * <level>.<keyword> = * { - * <level> { + * <level>[.<level] { Example:: @@ -1136,7 +1466,45 @@ Example:: Nesting of levels ^^^^^^^^^^^^^^^^^ -Levels can be nested by using curly brackets. Be carefull to write nothing than whitespaces/newline behind open or closing curly braces:: +Levels can be nested. E.g.: :: + + 10 { + sql = SELECT ... + 5 { + sql = SELECT ... + head = ... + } + } + +This is equal to: :: + + 10.sql = SELECT ... + 10.5.sql = SELECT ... + 10.5.head = ... + +By default, curly braces '{}' are used for nesting. Alternatively angle braces '<>', round braces '()' or square +braces '[]' are also possible. To define the braces to use, the **first line** of the bodytext has to be a comment line and the +last character of that line must be one of '{}[]()<>'. The corresponding braces are used for that QFQ record. E.g.: :: + + # Specific code. > + 10 < + sql = SELECT + head = <script> + data = [ + { + 10, 20 + } + ] + </script> + > + + +Per QFQ tt-content record, only one type of nesting braces can be used. + +Be carefull to: + +* write nothing else than whitespaces/newline behind an **open brace** +* the **closing brace** has to be alone on a line. :: 10.sql = SELECT 'hello world' @@ -1164,6 +1532,9 @@ Levels can be nested by using curly brackets. Be carefull to write nothing than } +Access to upper column values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Columns of the upper level result can be accessed via variables, eg. {{10.person_id}} will be replaced by the value in the person_id column. +-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ @@ -1232,8 +1603,8 @@ Special column names * or by a **one character qualifier** followed by the ':' character, placed in front of the actual parameter value. +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -|**Reserved column name**| **Purpose** | -+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Reserved column name | Purpose | ++========================+=============================================================================================================================================================================================+ |_link |Easily create links with different features. | +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |_mailto |Quickly create email links. A click on the link will open the default mailer. The address is encrypted via JS against email bots. | @@ -1252,82 +1623,71 @@ Special column names +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |_check |Display a blue/gray/green/pink/red/yellow checked sign. If none color specified, show nothing | +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -|_F |Wrap/modify content. Undocumented. | -+------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |_<nonReservedName> |Suppress output. Column names with leading underscore are used to select data from the database and make it available in other parts of the report without generating any output. | +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. _column-link: -Column: link -^^^^^^^^^^^^ +Column: _link +^^^^^^^^^^^^^ {{url | display | **i (internal)**, e(external) | **- (same)**,n (new), p (parent), t(top) | **-**, (e(edit), c(copy), n(new), d(delete), i(insert) , f(file)) }} * Most URLs will be rendered via class link. -* Column names like pagee, mailto,... are wrapper to class link. -* The parameters for link contains a prefix to make them position-independent. -* For fewer conflicts: - - A:u|m|p:url|mail|page (A=Anchor) - - G:E|N|T|I|M|C (G=Graphic) - - G:P|b|C:<Color>|<Text> (G=Graphic) - -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|URL|IMG|Meaning |Qualifier |Example |Description | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Anchor |A:... |See above |Superclass for regular URL defnition | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Graphic |G:... |See above |Superclass for graphic definiton | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|x | |URL |u:<url> |u:http://www.example.com |If an image is specified, it will be rendered inside the link, default link class: external | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|x | |Mail |m:<email> |m:info@example.com |Default link class: email | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|x | |Page |p:<pageId> |p:impressum |Prepend '?' or '?id=', no hostname qualifier (automatically set by browser), default link class: internal, default value: {{pageId}} | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Text |t:<text> |t:Firstname Lastname |- | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Render |r:<mode> |r:[0-5] |`render-mode`_ Default: 0 | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Picture |P:<filename> |P:bullet-red.gif |Picture '<img src="bullet-red.gif"alt="....">', default link class: internal. | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Edit |E |E |Show 'edit' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |New |N |N |Show 'new' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Delete |D |D |Show 'delete' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Help |H |H |Show 'help' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Info |I |I |Show 'information' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Show |S |S |Show 'show' icon as image | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Bullet |B:[<color>] |B:green |Show bullet with '<color>'. Colors: blue, gray, green, pink, red, yellow. Default Color: green. | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| |x |Check |C:[<color>] |C:green |Show checked with '<color>'. Colors: blue, gray, green, pink, red, yellow. Default Color: green. | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |URL Params|U:<key1>=<value1>[&<keyN>=<valueN>]|U:a=value1&b=value2&c=...] |Any number of additional Params. Links to forms: U:form=Person&r=1234 | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Tooltip |o:<text> |o:More information here |Tooltip text | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Alttext |a:<text> |a:Name of person |Alttext for images | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Class |c:[n|i|e|<text>] |c:i |CSS class for link. n:no class attribut, i:internal (ext_localconf.php)(default), e:external (ext_localconf.php), <text>: explicit named| -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Target |g:<text> |g:_blank |target=_blank, Default: no target | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Question |q:<text> |q:please confirm |Link will be executed only if user clicks ok, default: 'Please confirm' | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Encryption|e:0|1|... |e:1 |Encryption of the e-mail: 0: no encryption, 1:via Javascript (default) | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Right |R |R |Defines picture position: Default is 'left' (no definition) of the 'text'. 'R' means 'right' of the 'text' | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -| | |Hash |h |h |A hash entry is generated with all Parameters. No other URL parameter than 'S_hash' (=hash) | -+---+---+----------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +* Column names like `_pagee`, `_mailto`, ... are wrapper to class link. +* The parameters for link contains a prefix to make them position-independet. + ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +|URL|IMG|Meaning |Qualifier |Example |Description | ++===+===+==============+===================================+===========================+========================================================================================================================================+ +|x | |URL |u:<url> |u:http://www.example.com |If an image is specified, it will be rendered inside the link, default link class: external | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +|x | |Mail |m:<email> |m:info@example.com |Default link class: email | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +|x | |Page |p:<pageId> |p:impressum |Prepend '?' or '?id=', no hostname qualifier (automatically set by browser), default link class: internal, default value: {{pageId}} | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Text |t:<text> |t:Firstname Lastname |- | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Render |r:<mode> |r:[0-5] |See: `render-mode`_, Default: 0 | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Picture |P:<filename> |P:bullet-red.gif |Picture '<img src="bullet-red.gif"alt="....">', default link class: internal. | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Edit |E |E |Show 'edit' icon as image | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |New |N |N |Show 'new' icon as image | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Delete |D |D |Show 'delete' icon as image (only the icon, no database record 'delete' functionality) | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Help |H |H |Show 'help' icon as image | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Info |I |I |Show 'information' icon as image | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Show |S |S |Show 'show' icon as image | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Bullet |B:[<color>] |B:green |Show bullet with '<color>'. Colors: blue, gray, green, pink, red, yellow. Default Color: green. | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| |x |Check |C:[<color>] |C:green |Show checked with '<color>'. Colors: blue, gray, green, pink, red, yellow. Default Color: green. | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |URL Params |U:<key1>=<value1>[&<keyN>=<valueN>]|U:a=value1&b=value2&c=... |Any number of additional Params. Links to forms: U:form=Person&r=1234 | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Tooltip |o:<text> |o:More information here |Tooltip text | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Alttext |a:<text> |a:Name of person |Alttext for images | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Class |c:[n|i|e|<text>] |c:i |CSS class for link. n:no class attribut, i:internal (ext_localconf.php)(default), e:external (ext_localconf.php), <text>: explicit named| ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Target |g:<text> |g:_blank |target=_blank,_self,_parent,<custom>. Default: no target | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Question |q:<text> |q:please confirm |See: `question`_. Link will be executed only if user clicks ok/cancel, default: 'Please confirm' | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Encryption |e:0|1|... |e:1 |Encryption of the e-mail: 0: no encryption, 1:via Javascript (default) | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Right |R |R |Defines picture position: Default is 'left' (no definition) of the 'text'. 'R' means 'right' of the 'text' | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |SIP |s[:0|1] |s, s:0, s:1 |If 's' or 's:1' a SIP entry is generated with all non Typo 3 Parameters. The URL contains only parameter 's' and Typo 3 parameter | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +| | |Delete record | x[:a|r|c] |x, x:r, x:c |a: ajax (only QFQ internal used), r: report (default), c: close (current page, open last page) | ++---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ .. _render-mode: @@ -1336,7 +1696,7 @@ Render mode +-----------+--------------------+-------------------+----------+-------------------------------------------------------------------+ |Mode |Both: url & text |Only: url |Only: text|Description | -+-----------+--------------------+-------------------+----------+-------------------------------------------------------------------+ ++===========+====================+===================+==========+===================================================================+ |0 (default)|<a href=url>text</a>|<a href=url>url</a>| |text or image will be shown, only if there is a url, page or mailto| +-----------+--------------------+-------------------+----------+-------------------------------------------------------------------+ |1 |<a href=url>text</a>|<a href=url>url</a>|text |Text or image will be shown, independet of there is a url | @@ -1363,15 +1723,15 @@ Link Examples +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "m:info@example.com|P:mail.gif" AS _link |info@example.com as linked image mail.gif, encrypted with javascript, class=external | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "m:info@example.com|P:mail.gif|o:sendmail" AS _link |*info@example.com* as linked image mail.gif, encrypted with javascript, class=external, tooltip: "sendmail" | +|SELECT "m:info@example.com|P:mail.gif|o:Email" AS _link |*info@example.com* as linked image mail.gif, encrypted with javascript, class=external, tooltip: "sendmail" | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "m:info@example.com|t:mailtoinfo@example.com|o:sendmail" AS link|'mail to *info@example.com*' as linked text, encrypted with javascript, class=external | +|SELECT "m:info@example.com|t:mailto:info@example.com|o:Email" AS link |'mail to *info@example.com*' as linked text, encrypted with javascript, class=external | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "u:www.example.com" AS _link |www.example as link, class=external | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "u:http://www.example.com" AS _link |*http://www.example* as link, class=external | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "u:www.example.com|q:Pleaseconfirm" AS _link |www.example as link, class=external, ?JavaScript Window which has to be confirmed with click on 'ok' | +|SELECT "u:www.example.com|q:Please confirm" AS _link |www.example as link, class=external, See: `question`_ | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "u:www.example.com|c:i" AS _link |*http://www.example* as link, class=internal | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ @@ -1379,100 +1739,202 @@ Link Examples +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "p:form_person|c:e" AS _link |<a class="external" href="?form_person">Text</a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "p:form_person&S_person=Text|t:Person" AS _link |<a class="internal" href="?form_person&S_person=Text">Person</a> | +|SELECT "p:form_person¬e=Text|t:Person" AS _link |<a class="internal" href="?form_person¬e=Text">Person</a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "p:form_person|e" AS _link |<a class="internal" href="?form_person"><img alttext="Edit" src="typo3conf/ext/qfq/Resources/Public/icons/edit.gif"></a> | +|SELECT "p:form_person|E" AS _link |<a class="internal" href="?form_person"><img alttext="Edit" src="typo3conf/ext/qfq/Resources/Public/icons/edit.gif"></a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "p:form_person|e|g:_blank" AS _link |<a target="_blank" class="internal" href="?form_person"><img alttext="Edit" src="typo3conf/ext/qfq/Resources/Public/icons/edit.gif"></a>| +|SELECT "p:form_person|E|g:_blank" AS _link |<a target="_blank" class="internal" href="?form_person"><img alttext="Edit" src="typo3conf/ext/qfq/Resources/Public/icons/edit.gif"></a>| +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "p:form_person|C" AS _link |<a class="internal" href="?form_person"><img alttext="Check" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif"></a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ |SELECT "p:form_person|C:green" AS _link |<a class="internal" href="?form_person"><img alttext="Check" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif"></a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "A:p:form_person|G:C" AS _link |<a class="internal" href="?form_person"><img alttext="Check" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif"></a> | +|SELECT "U:form=Person&r=123|x|D" as _link |<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234"><span class="glyphicon glyphicon-trash" ></span>"></a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ -|SELECT "A:u:www.example.com|G:P:home.gif|t:Home" AS _link |<a class="internal" href="?form_person"><img alttext="Check" src="typo3conf/ext/qfq/Resources/Public/icons/home.gif">Home</a> | +|SELECT "U:form=Person&r=123|x|t:Delete" as _link |<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234">Delete</a> | +-----------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ +.. _question: -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. - -[<page id|alias>[¶m=value&...]] | [record id] | [text] | [tooltip] | [msgbox] | [class] | [target] | [render mode] | [create hash] "" as pagee. Fastest way to create links, inside of the current T3 installation. Main purpose is the -automatic hash to be used by the forms +Question +^^^^^^^^ **Syntax** :: - SELECT "[options]" AS page[<link type>] - - with: [options] = [<page>]|[<record id>]|[<text>]|[<tooltip>]|[<msgbox>]|[<class>]|[<target>]|[<render mode>]|[<create hash>] + q[:<alert text>[:<level>[:<positive button text>[:<negative button text>[:<timeout>[:<flag modal>]]]]]] + + +* If a user clicks on a link, an alert is shown. If the user answers the alert by clicking on the 'positive button', the browser opens the specified link. + If the user click on the negative answer (or waits for timout), the alert is closed and the browser does nothing. +* All parameter are optional. +* Parameter are seperated by ':' +* To use ':' inside the text, the colon has to be escaped by '\\'. E.g. 'ok\\: I understand'. + ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Parameter | Description | ++======================+==========================================================================================================================+ +| Text | The text shown by the alert. HTML is allowed to format the text. Any ':' needs to be escaped. Default: 'Please confirm'. | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Level | success, info, warning, danger | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Positive button text | Default: 'Ok' | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Negative button text | Default: 'Cancel' | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Timeout in seconds | 0: no timeout, >0: after the specified time in seconds, the alert will dissapear and behaves like 'negative answer' | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Flag modal | 0: Alert behaves not modal. 1: (default) Alert behaves modal. | ++----------------------+--------------------------------------------------------------------------------------------------------------------------+ + +Examples: + ++------------------------------------------------------------+---------------------------------------------------------------------------+ +| SQL-Query | Result | ++============================================================+===========================================================================+ +| SELECT "p:form_person|q:Edit Person:warn" AS _link | Shows alert with level 'warn' | ++------------------------------------------------------------+---------------------------------------------------------------------------+ +| SELECT "p:form_person|q:Edit Person::I do:No way" AS _link | Instead of 'Ok' and 'Cancel', the button text will be 'I do' and 'No way' | ++------------------------------------------------------------+---------------------------------------------------------------------------+ +| SELECT "p:form_person|q:Edit Person:::10" AS _link | The Alert will be shown 10 seconds | ++------------------------------------------------------------+---------------------------------------------------------------------------+ +| SELECT "p:form_person|q:Edit Person:::10:0" AS _link | The Alert will be shown 10 seconds and is not modal. | ++------------------------------------------------------------+---------------------------------------------------------------------------+ + +Columns: _page[X] +^^^^^^^^^^^^^^^^^ - <link type> = c,d,e,h,i,n,s +The colum name is composed of the string *page* and a trailing character to specify the type of the link. -.. -The following table summarizes all available page columns. For most link types, all parameters are optional. If some parameters are required by a certain link type, this is indicated in the *Mandatory parameters* column of the table -below. +**Syntax** -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|**column name**|**Purpose** |**create hash**|**default value of msgbox parameter**|**Mandatory parameters** | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_page |Internal link without a graphic |no |empty |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pagec |Internal link without graphic, with message box|no |*Please confirm!* |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_paged |Internal link with delete icon (trash) |yes |*Delete record ?* |p:<pageId>,i:<id>,T:<table name>|f:<form name>| -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pagee |Internal link with edit icon (pencil) |yes |empty |p:<pageId>,i:<id> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pageh |Internal link with help icon (question mark) |yes |empty |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pagei |Internal link with information icon (i) |no |empty |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pagen |Internal link with new icon (sheet) |yes |empty |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ -|_pages |Internal link with how icon (magnifier) |yes |empty |p:<pageId> | -+---------------+-----------------------------------------------+---------------+-------------------------------------+----------------------------------------------+ +:: + SELECT "[options]" AS _page[<link type>] -* All paramater are optional. + with: [options] = [p:<page & param>]|[t:<text>]|[o:<tooltip>]|[q:<question parameter>]|[c:<class>]|[g:<target>]|[r:<render mode>] -* Optional set of predefined icons. + <link type> = c,d,e,h,i,n,s -* Optional set of dialog boxes. +.. -* If there is a hash, parameter S_hash ('hash') und N_r ('pseudo record id') will automatically be registered and appended. ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +| column name | Purpose |default value of question parameter | Mandatory parameters | ++===============+===============================================+=====================================+==============================================+ +|_page |Internal link without a grafic |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pagec |Internal link without a grafic, with question |*Please confirm!* |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_paged |Internal link with delete icon (trash) |*Delete record ?* |U:form=<formname>&r=<recordid> *or* | +| | | |U:table=<tablename>&r=<recordid> | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pagee |Internal link with edit icon (pencil) |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pageh |Internal link with help icon (question mark) |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pagei |Internal link with information icon (i) |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pagen |Internal link with new icon (sheet) |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ +|_pages |Internal link with how icon (magnifier) |empty |p:<pageId>[¶m] | ++---------------+-----------------------------------------------+-------------------------------------+----------------------------------------------+ + + +* All parameter are optional. +* Optional set of predefined icons. +* Optional set of dialog boxes. +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -|**Parameter**|**Description** |**Default value** |Example | -+-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ +| Parameter | Description | Default value |Example | ++=============+=================================================================================================+==========================================================+===============================================================+ |<page> |TYPO3 page id or page alias. |The current page: *{{pageId}}* |45 application application&N_param1=1045 | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -|<recordid> |Effective Record ID stored in hash array. |<empty> |7011 | -+-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ |<text> |Text, wrapped by the link. If there is an icon, text will be displayed to the right of it. |empty string | | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ |<tooltip> |Text to appear as a ToolTip |empty string | | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -|<msgbox> |If there is a msgbox text given, a msgbox will be opened. Only if the user clicks on ok, the link|**Expected "=" to follow "see"** | | -| |will be called | | | +|<question> |If there is a question text given, an alert will be opened. Only if the user clicks on 'ok', |**Expected "=" to follow "see"** | | +| |the link will be called | | | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ |<class> |CSS Class for the <a> tag |The default class defined for internal links in | | | | |ext_localconf.php (see ...) | | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ |<target> |Parameter for HTML 'target='. F.e.: Opens a new window |empty |P | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -|<rendermode> |Easy way (not) to show/render a link at all. See `render-mode`_ 0-5 | | | +|<rendermode> |Show/render a link at all or not. See `render-mode`_ 0-5 | | | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ -|<createhash> |h |see below |'h': create a hash, 'H': create no hash. Specify only if | -| | | |default is not suitable | +|<create sip> |s | |'s': create a SIP | +-------------+-------------------------------------------------------------------------------------------------+----------------------------------------------------------+---------------------------------------------------------------+ +Column: _paged +^^^^^^^^^^^^^^ + +These column offers a link, with a confirmation question, to delete one record (mode 'table') or a bunch of records +(mode 'form'). After deleting the record(s), the current page will be reloaded in the browser. + +**Syntax** + +:: + + SELECT "U:table=<tablename>&r=<recordId>|q:<question>|..." AS _paged + SELECT "U:form=<formname>&r=<recordId>|q:<question>|..." AS _paged + +.. + +If the record to delete contains column(s), whose columnname match on `%pathFileName%` and such a +column points to a real existing file, such a file will be deleted too. If the table contains records where the specific +file is multiple times referenced, than the file is not deleted (it would break the still existing references). Multiple +references are not found, if they use different colummnnames or tablenames. + +Mode: table +''''''''''' +* `table=<table name>` +* `r=<record id>` + +Deletes the record with id '<record id>' from table '<table name>'. + +Mode: form +'''''''''' +* `form=<form name>` +* `r=<record id>` + +Deletes the record with id '<record id>' from the table specified in form '<form name>' as primary table. +Additional action *formElement* of type *beforeDelete* or *afterDelete* will be fired too. + +Examples: +''''''''' + +:: + + SELECT 'U:table=Person&r=123|q:Do you want delete John Doe?' AS _paged + SELECT 'U:form=person-main&r=123|q:Do you want delete John Doe?' AS _paged + +Columns: _Page[X] +^^^^^^^^^^^^^^^^^ + +* Similar to `_page[X]` +* Parameter are position dependent and therefore without a qualifier! + +:: + + "[<page id|alias>[¶m=value&...]] | [text] | [tooltip] | [question parameter] | [class] | [target] | [render mode]" as _Pagee. + +.. + +Column: _Paged +^^^^^^^^^^^^^^ + +* Similar to `_paged` +* Parameter are position dependent and therefore without a qualifier! + +:: + + "[table=<table name>&r-<record id>[¶m=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged. + "[form=<form name>&r-<record id>[¶m=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged. + +.. Column: _vertical ^^^^^^^^^^^^^^^^^ @@ -1484,16 +1946,13 @@ angle. :: - - SELECT "<text>|[<angle>]|[<width>]|[<height>]|[<wrap tag>]" AS vertical + SELECT "<text>|[<angle>]|[<width>]|[<height>]|[<wrap tag>]" AS _vertical .. - - +-------------+-------------------------------------------------------------------------------------------------------+-----------------+ |**Parameter**|**Description** |**Default value**| -+-------------+-------------------------------------------------------------------------------------------------------+-----------------+ ++=============+=======================================================================================================+=================+ |<text> |The string that should be rendered vertically. |none | +-------------+-------------------------------------------------------------------------------------------------------+-----------------+ |<angle> |How many degrees should the text be rotated? The angle is measured clockwise from baseline of the text.|*270* | @@ -1548,7 +2007,7 @@ Easily create Email links. +--------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+ |**Parameter** |**Description** |**Default | | | |value** | -+--------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+ ++==============+==============================================================================================================================================================================================================+=============+ |<emailaddress>|The email address where the link should point to. |none | +--------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+ |<linktext> |The text that should be displayed on the website and be linked to the email address. This will typically be the name of the recipient. If this parameter is omitted, the email address will be displayed as |none | @@ -1572,7 +2031,7 @@ Easily create Email links. :: - 10.sql = select "john.doe@example.com|John Doe" AS _mailto + 10.sql = SELECT "john.doe@example.com|John Doe" AS _mailto .. @@ -1581,29 +2040,41 @@ Easily create Email links. 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']. +<TO:email[,email]>|<FROM:email>|<subject>|<body>|[<REPLY-TO:email>]|[<flag autosubmit: on /off>]|[<grid>]|[xId]|<CC:email[,email]>|<BCC:email[,email]> + + +Send text emails. Every mail will be logged in the table `mailLog`. **Syntax** :: - - SELECT "receiver@domain.com[:john doe],receiver2@domain.com[:jane doe]|sender@domain.com[:willi wutzmann]|subject|body" AS _sendmail + SELECT "john@doe.com|jane@doe.com|Reminder tomorrow|Please dont miss the meeting tomorrow" AS _sendmail .. - - +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ |**Parameter** |**Description** |**Required**| ++============================================================+==========================================================================================+============+ +|TO:email[,email] |Comma-separated list of receiver email addresses. Optional: `realname <john@doe.com>` | yes | ++------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ +|FROM:email |Sender of the email. Optional: 'realname <john@doe.com>' | yes | ++------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ +|subject |Subject of the email | yes | ++------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ +|body |Message | yes | ++------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ +|REPLY-TO:email |Email address to reply to (if different from sender) | | ++------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ +|flagAutoSubmit 'on' / 'off' |If 'on', the mail contains the header 'Auto-Submitted: auto-send' - suppress OoO replies | | +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|receiver@domain.com[:johndoe],receiver2@domain.com[:janedoe]|Comma-separated list of Email-receiver(s). An optional name can be added using a colon (:)| | +|grId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|sender@domain.com[:williwutzmann] |Sender of the email. An optional name can be added using a colon (:) | | +|xId |Will be copied to the mailLog record. Helps to setup specific logfile queries | | +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|subject |Subject of the email | | +|CC:email[,email] |Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ -|body |Message | | +|BCC:email[,email] |Comma-separated list of receiver email addresses. Optional: 'realname <john@doe.com>' | | +------------------------------------------------------------+------------------------------------------------------------------------------------------+------------+ @@ -1625,50 +2096,13 @@ This will send an email with subject *Latest News* from company@example.com to j :: - 10.sql = SELECT "customer1@example.com, customer2@example.com|company@example.com|Latest News|The new version is now available." AS _sendmail + 10.sql = SELECT "customer1@example.com,Firstname Lastname <customer2@example.com>, Firstname Lastname <customer3@example.com>|company@example.com|Latest News|The new version is now available.|sales@example.com|on|101|222|ceo@example.com|backup@example.com" AS _sendmail .. - - -This will send an email with subject *Latest news* from company@example.com to customer1@example.com and to customer2@example.com. - -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']. - -**Syntax** - -:: - - - SELECT "receiver@domain.com[:john doe],receiver2@domain.com[:jane doe]|sender@domain.com[:willi wutzmann]|subject|cc1@domain.com[:willi wutzmann]|bcc1@domain.com[:george wutzmann]|replyto@domain.com[:Support-Desk]|format|body" AS _sendmail - -.. - - - -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|**Parameter** |**Description** |**required**| -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|receiver@domain.com[:johndoe],receiver2@domain.com[:janedoe]|Comma-separated list of Email-receiver(s). An optional name can be added using a colon (:) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|sender@domain.com[:williwutzmann] |Sender of the email. An optional name can be added using a colon (:) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|subject |Subject of the email | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|cc1@domain.com[:williwutzmann] |Comma-separated list of CC-receiver(s). An optional name can be added using a colon (:) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|bcc1@domain.com[:georgewutzmann] |Comma-separated list of BCC-receiver(s). An optional name can be added using a colon (:) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|replyto@domain.com[:Support-Desk] |Reply-to address. An optional name can be added using a colon (:) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|format |Flag indicating if this is a plaintext or html message. Possible values are 'plain' and 'html'| | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ -|body |Message (plain text or html) | | -+------------------------------------------------------------+----------------------------------------------------------------------------------------------+------------+ +This will send an email with subject *Latest News* from company@example.com to customer1, customer2 and customer3 by +using a realname for customer2 and customer3 and suppress generating of OoO answer if any receiver is on vacation. +Additional the CEO as well as backup will receive the mail via CC and BCC. Column: _img @@ -1692,7 +2126,7 @@ Renders images. Allows to define an alternative text and a title attribute for t +-------------+-------------------------------------------------------------------------------------------+---------------------------+ |**Parameter**|**Description** |**Default value/behaviour**| -+-------------+-------------------------------------------------------------------------------------------+---------------------------+ ++=============+===========================================================================================+===========================+ |<pathtoimage>|The path to the image file. |none | +-------------+-------------------------------------------------------------------------------------------+---------------------------+ |<alttext> |Alternative text. Will be displayed if image can't be loaded (alt attribute of img tag). |empty string | @@ -1748,7 +2182,7 @@ Runs batch files or executables on the webserver. In case of an error, returncod +-------------+--------------------------------------------------+-----------------+ |**Parameter**|**Description** |**Default value**| -+-------------+--------------------------------------------------+-----------------+ ++=============+==================================================+=================+ |<command> |The command that should be executed on the server.|none | +-------------+--------------------------------------------------+-----------------+ @@ -1764,7 +2198,6 @@ Runs batch files or executables on the webserver. In case of an error, returncod .. - Column: _F ^^^^^^^^^^ @@ -1842,7 +2275,7 @@ Assume you have multiple columns with reserved names in the same query and want :: - 10.sql = SELECT CONCAT("/static/directory/", g.picture) AS _img, CONCAT("/static/preview/", g.thumbnail) AS _img FROM gallery AS g WHHERE ... + 10.sql = SELECT CONCAT("/static/directory/", g.picture) AS _img, CONCAT("/static/preview/", g.thumbnail) AS _img FROM gallery AS g WHERE ... 20.sql = SELECT "{{10.img}}", d.text FROM description AS d ... @@ -1892,7 +2325,7 @@ Solution for *#Challenge_2*: +-------------+--------------------------------------------------------------------+--------+ |**Parameter**|**Description** |Required| -+-------------+--------------------------------------------------------------------+--------+ ++=============+====================================================================+========+ |Q |Any of the *reserved column names* | | +-------------+--------------------------------------------------------------------+--------+ |Z |Process the column but don't display it | | @@ -1925,6 +2358,16 @@ parameter. To achieve this, first build a link on page A which includes the requ The above example builds a link to pageB - refer to the :ref:`column-link`-manual for details. The link tells page B to render the form with name formname and load the record with id id for editing. +QFQ CSS Classes +--------------- + +* `qfq-table-50`, `qfq-table-80` - release the default width of 100% and specify minwidth=50% resp. 80%. + +E.g.:: + + 10.sql = SELECT id, name, firstName, ... + 10.head = <table class='table table-condensed qfq-table-50'> + Examples -------- @@ -2056,11 +2499,11 @@ Formatting Examples Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two different ways: - One can add formatting output directly into the SQL by either putting it in a separate column of the output or by using concat to concatenate data and formatting output in a single column. +One can add formatting output directly into the SQL by either putting it in a separate column of the output or by using concat to concatenate data and formatting output in a single column. - One can use ?level keys to define formatting information that will be put before/after/between all rows/columns of the actual levels result. +One can use ?level keys to define formatting information that will be put before/after/between all rows/columns of the actual levels result. - Two columns +Two columns :: @@ -2072,7 +2515,7 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif - Result: +Result: :: @@ -2086,7 +2529,7 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif - One column 'rend' +One column 'rend' :: @@ -2096,9 +2539,7 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif .. - - - Result: +Result: :: @@ -2110,9 +2551,7 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif .. - - - More HTML +More HTML :: @@ -2125,9 +2564,7 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif .. - - - Result: +Result: :: @@ -2137,11 +2574,8 @@ Formating (i.e. wrapping of data with HTML tags etc.) can be achieved in two dif o Louis Armstrong o Diana Ross -.. - The same as above, but with braces:: - 10 { sql = SELECT p.name FROM exp_person AS p head = <ul> @@ -2150,26 +2584,14 @@ The same as above, but with braces:: rend = </li> } -:: - - Two queries - -:: - +Two queries: :: 10.sql = SELECT p.name FROM exp_person AS p 10.rend = <br /> 20.sql = SELECT a.street FROM exp_address AS a 20.rend = <br /> -.. - - - - Two queries: nested - -:: - +Two queries: nested :: # outer query 10.sql = SELECT p.name FROM exp_person AS p @@ -2179,16 +2601,9 @@ The same as above, but with braces:: 10.10.sql = SELECT a.street FROM exp_address AS a 10.10.rend = <br /> -.. - - - -* For every record of '10', all records of 10.10 will be printed. - -Two queries: nested with variables - -:: +* For every record of '10', all records of 10.10 will be printed. +Two queries: nested with variables :: # outer query 10.sql = SELECT p.id, p.name FROM exp_person AS p @@ -2198,16 +2613,9 @@ Two queries: nested with variables 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pid='{{10.id}}' 10.10.rend = <br /> -.. - - - -* For every record of '10', all assigned records of 10.10 will be printed. - -Two queries: nested with hidden variables in a table - -:: +* For every record of '10', all assigned records of 10.10 will be printed. +Two queries: nested with hidden variables in a table :: 10.sql = SELECT p.id AS _p_id, p.name FROM exp_person AS p 10.rend = <br /> @@ -2216,9 +2624,7 @@ Two queries: nested with hidden variables in a table 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.p_id='{{10.p_id}}' 10.10.rend = <br /> -.. - -Same as above, but written in the nested notation:: +Same as above, but written in the nested notation :: 10 { sql = SELECT p.id AS _p_id, p.name FROM exp_person AS p @@ -2231,11 +2637,11 @@ Same as above, but written in the nested notation:: } } -* Columns starting with a '_' won't be printed but can be accessed as regular columns. +* Columns starting with a '_' won't be printed but can be accessed as regular columns. -Best practice -============= +Best practice: Form +=================== Debug Report ------------ @@ -2279,8 +2685,8 @@ QFQ content record:: } -Secondary records: compute next free 'ord' automatically --------------------------------------------------------- +Form: 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. @@ -2294,8 +2700,17 @@ via SIP parameter to the secondary form. On the secondary form: for 'new' records choose the computed value, for existing records leave the value unchanged. -* Primary form, `subrecord` formelement, field `parameter`: set `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}}`. +* 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 ^^^^^^^^^ @@ -2303,3 +2718,162 @@ 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}}`. + +Form: Person Wizard - firstname, city +------------------------------------- + +Requirement: A form that displays the column 'firstname' from table 'Person' and 'city' from table 'Address'. If the +records not exist, the form should create it. + +Form primary table: Person + +Form salve table: Address + +Relation: `Person.id = Address.personId` + +* Form: wizard + + * Name: wizard + * Title: Person Wizard + * Table: Person + * Render: bootstrap + +* FormElement: firstname + + * Class: **native** + * Type: **text** + * Name: firstname + * Label: Firstname + +* FormElement: email, text, 20 + + * Class: **native** + * Type: **text** + * Name: city + * Label: City + * Value: `{{SELECT city FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` + +* FormElement: insert/update address record + + * Class: **action** + * Type: **afterSave** + * Label: Manage Address + * Parameter: + + * `slaveId={{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` + * `sqlInsert={{INSERT INTO Address (personId, city) VALUES ({{r}}, '{{city:F:allbut:s}}') }}` + * `sqlUpdate={{UPDATE Address SET city='{{city:F:allbut:s}}' WHERE id={{slaveId:V}} }}` + * `sqlDelete={{DELETE FROM Address WHERE id={{slaveId:V}} AND ''='{{city:F:allbut:s}}' LIMIT 1}}` + +Form: Person Wizard - firstname, single note +-------------------------------------------- + +Requirement: A form that displays the column 'firstname' from table 'Person' and 'note' from table 'Note'. +If the records not exist, the form should create it. +Column Person.noteId points to Note.id + +Form primary table: Person + +Form slave table: Address + +Relation: `Person.id = Address.personId` + +* Form: wizard + + * Name: wizard + * Title: Person Wizard + * Table: Person + * Render: bootstrap + +* FormElement: firstname + + * Class: **native** + * Type: **text** + * Name: firstname + * Label: Firstname + +* FormElement: email, text, 20 + + * Class: **native** + * Type: **text** + * Name: note + * Label: Note + * Value: `{{SELECT Note FROM Note AS n, Person AS p WHERE p.id={{r}} AND p.noteId=n.id ORDER BY id }}` + +* FormElement: insert/update address record + + * Class: **action** + * Type: **afterSave** + * Name: noteId + * Label: Manage Note + * Parameter: + + * `sqlInsert={{INSERT INTO Note (note) VALUES ('{{note:F:allbut:s}}') }}` + * `sqlUpdate={{UPDATE Note SET note='{{note:F:allbut:s}}' WHERE id={{slaveId:V}} }}` + + + + +Best Practise: Chart +==================== + +* QFQ delivers a chart JavaScript lib: https://github.com/nnnick/Chart.js.git. Docs: http://www.chartjs.org/docs/ +* The library is not sourced in the HTML page automatically. To do it, either include the lib + `typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js`: + + * in the specific tt_content record (shown below in the example) or + * system wide via Typo3 Template record. + +* By splitting HTML and JavaScript code over several lines, take care not accidently to create a 'nesting'-end token. + Check the line after `10.tail =`. It's '}' alone on one line. This is a valid 'nesting'-end token!. There are two options + to circumvent this: + + * Don't nest the HTML & JavaScript code - bad workaround, this is not human readable. + * Select different nesting token, e.g. '<' / '>' (check the first line on the following example). + +:: + + # < + + 10.sql = SELECT '_' + 10.head = + <div style="height: 1024px; width: 640px;"> + <h3>Distribution of FormElement types over all forms</h3> + <canvas id="barchart" width="1240" height="640"></canvas> + </div> + <script src="typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js"></script> + <script> + $(function () { + var ctx = document.getElementById("barchart"); + var barChart = new Chart(ctx, { + type: 'bar', + data: { + + 10.tail = + } + }); + }); + </script> + + # Labels + 10.10 < + sql = SELECT "'", fe.type, "'" FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type + head = labels: [ + tail = ], + rsep = , + > + + # Data + 10.20 < + sql = SELECT COUNT(fe.id) FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type + head = datasets: [ { data: [ + tail = ], backgroundColor: "steelblue", label: "FormElements" } ] + rsep = , + > + +FAQ +=== + + * Q: A variable {{<var>}} is shown as empty string, but there should be a value. + + * A: The sanatize rule is violeted and therefore the value has been removed. Set {{<var>:<store>:all}} as a test. diff --git a/extension/Documentation/_make/conf.py b/extension/Documentation/_make/conf.py index 5d2cb556c6c9150abb9f703da61667189fac02b5..9470e88de345ebe5bccc4117544743bd0ed57fa7 100644 --- a/extension/Documentation/_make/conf.py +++ b/extension/Documentation/_make/conf.py @@ -50,16 +50,16 @@ master_doc = 'Index' # General information about the project. project = u'QFQ Extension' -copyright = u'2016, Carsten Rose' +copyright = u'2017, Carsten Rose' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.3' +version = '0.8' # The full version, including alpha/beta/rc tags. -release = '0.3.0' +release = '0.10.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/extension/RELEASE.txt b/extension/RELEASE.txt new file mode 100644 index 0000000000000000000000000000000000000000..81f22c77334025a8be708b3e90d564d09b37f81f --- /dev/null +++ b/extension/RELEASE.txt @@ -0,0 +1,54 @@ +Version 0.10 +============ + +Features +-------- + + * Implemented Parameter 'extraDeleteForm' for 'forms' and 'subrecords'. Update doc. + +Bug fixes +--------- + + * Suppress rendering of form title during a 'delete' call. No one will see it and required parameters are not supplied. + * In case of broken SQL queries, print them in ajax error message. + * Remove parameter 'table' from Delete SIP URLs. ToolTip updated. + +Version 0.9 +=========== + +Features +-------- + * FormEditor: + * design update - new default background color: grey. + * per form configureable background colors. + * Optional right align of all form element labels. + * Added config.qfq.ini values CSS_CLASS_QFQ_FORM_PILL, CSS_CLASS_QFQ_FORM_BODY, CSS_CLASS_QFQ_CONTAINER. + +Bug fixes +--------- + + * BuildFormBootstrap.php: added new class name 'qfq-label' to form labels - needed to assign 'qfq-form-right' class. Changed wrapping of formelements from 'col-md-8' (wrong) to 'col-md-12'. + * QuickFormQuery.php: Set default for new F_CLASS_PILL & F_CLASS_BODY. + * formEditor.sql: New default background color for formElements is blue. + * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-*, qfq-form-right. + * Index.rst: Add note to hierachy chars. Fixed uncomplete doc to a) bs*Columns, showButton. Add classPill, classBody. Rewrote form.paramter.class. + * QuickFormQuery.php: Button save/ close/ delete/ new - align to right border of form. + * UsersManual/index.rst: renamed chapter for formelements. Cleanup formelement types. Wrote chapter 'Detailed concept'. + * QuickFormQuery.php, FormAction.php: '#2931 / afterSave Hauptrecord xId nicht direkt verfügbar' - load master record again, after 'action'-elements has been processed. + * UsersManual/index.rst: Startet FAQ section. + * config.qfq.example.ini: Added comment where to save config.qfq.ini. + * UsersManual/index.rst: Rewrite of 'action'-FormElement definition. + * #2739: beforeDelete / afterDelete. + * PROTOCOL.md: update 'delete' description. + * delete.php: fixed unwanted loose of MSG_CONTENT. + * Report.php: Fixed double '&&' in building UrlParam. + * FormAction.php: In case of 'AFTER_DELETE', do not try to load primary record - that one is already deleted. + * Sip.php: Do not skip SIP_TARGET_URL as parameter for the SIP. + * #3001 Report: delete implementieren. + * Index.rst, Constants.php: reverted parameter '_table' in delete links back to 'table' - Reason: 'form' needs to be 'form' (instead of '_form') due to many used places already. + * Sip.php: move SIP_TARGET_URL back to stored inside SIP - it's necessary for 'delete'-links. + * Report.php, Constants.php: Remove code to handle unecessary 'p:' tag for delete links. + * Link.php: Check paged / Paged that the parameter r, table and form are given in the right combination. + * Link.php, Report.php: New '_link' token 'x'. '_paged' and '_Paged' are rendered via Link() class, Link() class now supports delete links. + * QuickFormQuery.php: for modeForm='Form Delete' the 'required param' are not respected - this makes sense, cause these parameters typically filled in newly created records. + * Fixed: #3076 Delete Button bei Subrecords erzeugt sporadisch Javascript Exceptions (Webkit: Chrome / Vivaldi) - kein loeschen moeglich. \ No newline at end of file diff --git a/extension/Resources/Private/Templates/Qfq/Show.html b/extension/Resources/Private/Templates/Qfq/Show.html index 339556a88e82d03c0223d2ca7c205b29e9ca8fff..ab280df5fddbebf263ccbdeb3c0e42d22b6da4e3 100644 --- a/extension/Resources/Private/Templates/Qfq/Show.html +++ b/extension/Resources/Private/Templates/Qfq/Show.html @@ -1,3 +1 @@ -<p> - <f:format.raw value="{qfqOutput}"/> -</p> \ No newline at end of file +<f:format.raw value="{qfqOutput}"/> diff --git a/extension/config.example.ini b/extension/config.qfq.example.ini similarity index 55% rename from extension/config.example.ini rename to extension/config.qfq.example.ini index da8a93b86bb8db95959cdf215da95d83e9d861ef..9a6752b008bbac1ebe4de99a4473b4f7e7a5c710 100644 --- a/extension/config.example.ini +++ b/extension/config.qfq.example.ini @@ -1,4 +1,7 @@ -; comment +; QFQ configuration +; +; Save this file as: <Documentroot>/typo3conf/config.qfq.ini + DB_USER = <DBUSER> DB_SERVER = <DBSERVER> DB_PASSWORD = <DBPW> @@ -6,13 +9,13 @@ DB_NAME = <DB> DB_NAME_TEST = <TESTDB> -SESSION_NAME = qfq +DB_INIT = set names utf8 SQL_LOG = sql.log ; all, modify SQL_LOG_MODE = modify -; auto|yes|no. 'auto': if BE User is loggend in 'true', else 'false' +; auto|yes|no. 'auto': if BE User is logged in the value will be 'true', else 'false' SHOW_DEBUG_INFO = auto CSS_LINK_CLASS_INTERNAL = internal @@ -21,5 +24,9 @@ CSS_LINK_CLASS_EXTERNAL = external ; QFQ with own Bootstrap: 'container'. QFQ already nested in Bootstrap of mainpage: <empty> CSS_CLASS_QFQ_CONTAINER = +; Default background color, specified via CSS class +CSS_CLASS_QFQ_FORM_PILL = qfq-color-grey-1 +CSS_CLASS_QFQ_FORM_BODY = qfq-color-grey-2 + ; yyyy-mm-dd, dd.mm.yyyy DATE_FORMAT = yyyy-mm-dd diff --git a/extension/ext_emconf.php b/extension/ext_emconf.php index 9a139688219679f670298873425b4ac36b1f6998..fa3ccf410aa1b686e6258ddd465ae1fdbf694a09 100644 --- a/extension/ext_emconf.php +++ b/extension/ext_emconf.php @@ -10,5 +10,5 @@ $EM_CONF[$_EXTKEY] = array( 'dependencies' => 'fluid,extbase', 'clearcacheonload' => true, 'state' => 'alpha', - 'version' => '0.3' + 'version' => '0.10' ); \ No newline at end of file diff --git a/extension/qfq/api/delete.php b/extension/qfq/api/delete.php index 707fce1f45f324d97257a14ea0867901f1dbcdd2..b58e5e05c15e2c906b1c57e6e0ef9764a9be5242 100644 --- a/extension/qfq/api/delete.php +++ b/extension/qfq/api/delete.php @@ -11,12 +11,28 @@ namespace qfq; use qfq; require_once(__DIR__ . '/../qfq/QuickFormQuery.php'); -//require_once(__DIR__ . '/../qfq/store/Store.php'); +require_once(__DIR__ . '/../qfq/store/Store.php'); require_once(__DIR__ . '/../qfq/Constants.php'); +require_once(__DIR__ . '/../qfq/exceptions/CodeException.php'); /** - * Return JSON encoded answer + * delete: success + * SIP_MODE_ANSWER: MODE_HTML + * Send header 'location' to targetUrl + * + * SUP_MODE_ANSWER: MODE_JSON + * Send AJAX answer with Status 'success' + * + * delete: failed + * SIP_MODE_ANSWER: MODE_HTML + * No forward, print error message. + * + * SUP_MODE_ANSWER: MODE_JSON + * Send AJAX answer with Status 'error' and 'error message' as JSON encoded + * + * + * JSON Format: * * status: success|error * message: <message> @@ -50,18 +66,55 @@ $answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO; $answer[API_MESSAGE] = ''; $answer[API_STATUS] = API_ANSWER_STATUS_ERROR; +$result[MSG_HEADER] = ''; +$result[MSG_CONTENT] = ''; + +$modeAnswer = false; +$flagSuccess = false; + try { $qfq = new \qfq\QuickFormQuery(['bodytext' => '']); - $qfq->delete(); + $flagSuccess = $qfq->delete(); + + $targetUrl = Store::getVar(SIP_TARGET_URL, STORE_SIP); + $modeAnswer = Store::getVar(SIP_MODE_ANSWER, STORE_SIP); + + switch ($modeAnswer) { + case MODE_JSON: + + $answer[API_MESSAGE] = 'delete: success'; + $answer[API_REDIRECT] = API_ANSWER_REDIRECT_CLIENT; + $answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS; + break; - $answer[API_MESSAGE] = 'delete: success'; - $answer[API_REDIRECT] = API_ANSWER_REDIRECT_CLIENT; - $answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS; + case MODE_HTML: + if ($targetUrl === false || $targetUrl === '') { + throw new CodeException('Missing target URL', ERROR_MISSING_VALUE); + } + + $result[MSG_HEADER] = "Location: $targetUrl"; + break; + + default: + throw new CodeException('Unknown mode: ' . $modeAnswer, ERROR_UNKNOWN_MODE); + break; + } } catch (qfq\UserFormException $e) { $answer[API_MESSAGE] = $e->formatMessage(); + + $val = Store::getVar(SYSTEM_FORM_ELEMENT, STORE_SYSTEM); + if ($val !== false) { + $answer[API_FIELD_NAME] = $val; + } + + $val = Store::getVar(SYSTEM_FORM_ELEMENT_MESSAGE, STORE_SYSTEM); + if ($val !== false) { + $answer[API_FIELD_MESSAGE] = $val; + } + } catch (qfq\CodeException $e) { $answer[API_MESSAGE] = $e->formatMessage(); } catch (qfq\DbException $e) { @@ -70,6 +123,28 @@ try { $answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage(); } -header("Content-Type: application/json"); -echo json_encode($answer); +// In case $modeAnswer is still missing: try to get it again - maybe the SIP store has been initialized before the exception has been thrown. +if ($modeAnswer === false) { + $modeAnswer = Store::getVar(SIP_MODE_ANSWER, STORE_SIP); +} + +if ($modeAnswer === MODE_JSON) { + // JSON + $result[MSG_HEADER] = "Content-Type: application/json"; + $result[MSG_CONTENT] = json_encode($answer); +} else { + // HTML + if (!$flagSuccess) { + $result[MSG_CONTENT] = "<p>" . $answer[API_MESSAGE] . "</p>"; + if (isset($answer[API_FIELD_NAME])) { + $result[MSG_CONTENT] .= "<p>" . $answer[API_FIELD_NAME] . " : " . $answer[API_FIELD_MESSAGE] . "</p>"; + } + } +} + +// Send header, if given. +if ($result[MSG_HEADER] !== '') { + header($result[MSG_HEADER]); +} +echo $result[MSG_CONTENT]; \ No newline at end of file diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 9db254b9d8a8a6b26eb3035e8eac1383c0d907a6..218cf93ea4f10d1445c61aa2d7f426c89fc77769 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -21,6 +21,7 @@ require_once(__DIR__ . '/../qfq/Database.php'); require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php'); require_once(__DIR__ . '/../qfq/helper/Support.php'); require_once(__DIR__ . '/../qfq/helper/OnArray.php'); +require_once(__DIR__ . '/../qfq/report/Link.php'); /** @@ -31,8 +32,6 @@ abstract class AbstractBuildForm { protected $formSpec = array(); // copy of the loaded form protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form - protected $store = null; - protected $evaluate = null; protected $buildElementFunctionName = array(); protected $pattern = array(); protected $wrap = array(); @@ -42,11 +41,18 @@ abstract class AbstractBuildForm { // protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]"> + /** + * @var Store + */ + protected $store = null; + /** + * @var Evaluate + */ + protected $evaluate = null; /** * @var string */ private $formId = null; - /** * @var Sip */ @@ -81,8 +87,9 @@ abstract class AbstractBuildForm { 'datetimeJQW' => 'DateJQW', 'email' => 'Input', 'gridJQW' => 'GridJQW', - 'hidden' => 'Hidden', + FE_TYPE_EXTRA => 'Extra', 'text' => 'Input', + 'editor' => 'Editor', 'time' => 'DateTime', 'note' => 'Note', 'password' => 'Input', @@ -102,8 +109,9 @@ abstract class AbstractBuildForm { 'datetimeJQW' => 'Native', 'email' => 'Native', 'gridJQW' => 'Native', - 'hidden' => 'Native', + FE_TYPE_EXTRA => 'Native', 'text' => 'Native', + 'editor' => 'Native', 'time' => 'Native', 'note' => 'Native', 'password' => 'Native', @@ -115,9 +123,9 @@ abstract class AbstractBuildForm { 'pill' => 'Pill' ]; - $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon glyphicon-pencil'></span>"; - $this->symbol[SYMBOL_NEW] = "<span class='glyphicon glyphicon-plus'></span>"; - $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon glyphicon-trash'></span>"; + $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>"; + $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>"; + $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>"; $this->inputCheckPattern = Sanitize::inputCheckPatternArray(); } @@ -127,7 +135,7 @@ abstract class AbstractBuildForm { /** * Builds complete form. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'. * - * @param $mode + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string|array $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes values/states * @throws CodeException * @throws DbException @@ -151,8 +159,6 @@ abstract class AbstractBuildForm { // <form> if ($mode === FORM_LOAD) { $htmlHead = $this->head(); - $htmlTail = $this->tail(); - $htmlSubrecords = $this->doSubrecords(); } $filter = $this->getProcessFilter(); @@ -167,12 +173,17 @@ abstract class AbstractBuildForm { $json[] = $jsonTmp; } } else { - $htmlElements = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse); + $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); + $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode); } - $htmlSip = $this->buildHiddenSip($json); + // <form> + if ($mode === FORM_LOAD) { + $htmlTail = $this->tail(); + $htmlSubrecords = $this->doSubrecords(); + } - // </form> + $htmlSip = $this->buildHiddenSip($json); return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlSip . $htmlTail . $htmlSubrecords : $json; } @@ -191,7 +202,7 @@ abstract class AbstractBuildForm { $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'"); $formEditUrl = $this->createFormEditUrl(); - $html .= "<p><a href='$formEditUrl'>Edit</a> <small>[$sipParamString]</small></p>"; + $html .= "<p><a " . Support::doAttribute('href', $formEditUrl) . ">Edit</a> <small>[$sipParamString]</small></p>"; $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec['title'], true); @@ -270,6 +281,8 @@ abstract class AbstractBuildForm { } /** + * Return a uniq form id + * * @return string */ public function getFormId() { @@ -307,10 +320,6 @@ abstract class AbstractBuildForm { } - abstract public function tail(); - - abstract public function doSubrecords(); - abstract public function getProcessFilter(); /** @@ -319,23 +328,30 @@ abstract class AbstractBuildForm { * @param $recordId * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD * @param int $feIdContainer + * @param array $json + * @param string $modeCollectFe + * @param bool $htmlElementNameIdZero + * @param string $storeUse + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string * @throws CodeException * @throws DbException * @throws \qfq\UserFormException */ - public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, &$json, - $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUse = STORE_USE_DEFAULT) { + public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json, + $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUse = STORE_USE_DEFAULT, $mode = FORM_LOAD) { $html = ''; + $flagOutput = false; // get current data record if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) { - $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId)); + $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1, array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" . $this->formSpec[F_TABLE_NAME] . "'."); $this->store->setVarArray($row, STORE_RECORD); } // Iterate over all FormElements foreach ($this->feSpecNative as $fe) { + 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') @@ -343,40 +359,43 @@ abstract class AbstractBuildForm { continue; // skip this FE } + $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); + $debugStack = array(); // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); // evaluate current FormElement - $evaluate = new Evaluate($this->store, $this->db); - $formElement = $evaluate->parseArray($fe, $debugStack); + $formElement = $this->evaluate->parseArray($fe, $debugStack); // Some Defaults $formElement = Support::setFeDefaults($formElement); - Support::setIfNotSet($formElement, F_BS_LABEL_COLUMNS); - Support::setIfNotSet($formElement, F_BS_INPUT_COLUMNS); - Support::setIfNotSet($formElement, F_BS_NOTE_COLUMNS); - $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); + if ($flagOutput === true) { + Support::setIfNotSet($formElement, F_BS_LABEL_COLUMNS); + Support::setIfNotSet($formElement, F_BS_INPUT_COLUMNS); + Support::setIfNotSet($formElement, F_BS_NOTE_COLUMNS); + $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']; + $value = ($formElement[FE_VALUE] === '') ? $this->store->getVar($formElement['name'], $storeUse, + $formElement['checkType']) : $formElement[FE_VALUE]; // Typically: $htmlElementNameIdZero = true // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'. - $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId); + $htmlFormElementId = HelperFormElement::buildFormElementName($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId); // Construct Marshaller Name: buildElement $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]]; $jsonElement = array(); // Render pure element - $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement); + $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement, $mode); // $fake0 = $fe['dynamicUpdate']; // $fake1 = $formElement['dynamicUpdate']; @@ -389,21 +408,26 @@ abstract class AbstractBuildForm { } else { // for non container elements: just add the current json status if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) { - $json[] = $jsonElement; + if (isset($jsonElement[0]) && is_array($jsonElement[0])) { + // Checkboxes are delivered as array of arrays: unnest them and append them to the existing json array. + $json = array_merge($json, $jsonElement); + } else { + $json[] = $jsonElement; + } } } - // debugStack as Tooltip - if ($this->showDebugInfo && count($debugStack) > 0) { -// $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack))); - $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack)); - } + if ($flagOutput) { + // debugStack as Tooltip + if ($this->showDebugInfo && count($debugStack) > 0) { + $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack)); + } - // Construct Marshaller Name: buildRow - $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]]; + // Construct Marshaller Name: buildRow + $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]]; - $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId); -// break; + $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId); + } } // Log / Debug: Last FormElement has been processed. @@ -414,16 +438,23 @@ abstract class AbstractBuildForm { abstract public function fillWrapLabelInputNote($label, $input, $note); + abstract public function tail(); + + abstract public function doSubrecords(); + /** * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element. * - * @param $json + * @param array $json * @return string <input type='hidden' name='s' value='<sip>'> * @throws CodeException * @throws \qfq\UserFormException */ - public function buildHiddenSip(&$json) { + public function buildHiddenSip(array &$json) { + $sipArray = $this->store->getStore(STORE_SIP); + + // do not include system vars unset($sipArray[SIP_SIP]); unset($sipArray[SIP_URLPARAM]); @@ -438,18 +469,19 @@ abstract class AbstractBuildForm { } /** + * Create an array with standard elements and add 'form-element', 'value'. + * * @param $htmlFormElementId * @param string|array $value * @param string $feMode disabled|readonly|'' * @return array */ private function getJsonElementUpdate($htmlFormElementId, $value, $feMode) { + $json = $this->getJsonFeMode($feMode); $json['form-element'] = $htmlFormElementId; $json['value'] = $value; -// $json['disabled'] = ($feMode === 'disabled'); -// $json['readonly'] = ($feMode === 'readonly'); return $json; } @@ -469,6 +501,8 @@ abstract class AbstractBuildForm { } /** + * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'. + * * @param $feMode * @param $hidden * @param $disabled @@ -564,16 +598,18 @@ abstract class AbstractBuildForm { * Builds HTML 'input' element. * Format: <input name="$htmlFormElementId" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"] * [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"] - * [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value"> + * [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value"> * * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string - * @throws UserFormException + * @throws \qfq\UserFormException */ - public function buildInput(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildInput(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $textarea = ''; $attribute = Support::doAttribute('name', $htmlFormElementId); @@ -594,16 +630,14 @@ abstract class AbstractBuildForm { $this->adjustMaxLength($formElement); - // <input> - if ($formElement['maxLength'] > 0) { + if ($formElement['maxLength'] > 0 && $value !== '') { // crop string only if it's not empty (substr returns false on empty strings) - if ($value !== '') - $value = substr($value, 0, $formElement['maxLength']); - - // 'maxLength' needs an upper 'L': naming convention for DB tables! - $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']); - $attribute .= Support::doAttribute('value', htmlentities($value), false); + $value = substr($value, 0, $formElement['maxLength']); } + + // 'maxLength' needs an upper 'L': naming convention for DB tables! + $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']); + $attribute .= Support::doAttribute('value', htmlentities($value), false); } $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']); @@ -620,6 +654,8 @@ abstract class AbstractBuildForm { } /** + * Calculates the maxlength of an input field, based on formElement type, formElement user definition and table.field definition. + * * @param array $formElement */ private function adjustMaxLength(array &$formElement) { @@ -649,7 +685,7 @@ abstract class AbstractBuildForm { // date/datetime if ($maxLength !== false) { - if (is_numeric($formElement['maxLength'])) { + if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) { if ($formElement['maxLength'] > $maxLength) { $formElement['maxLength'] = $maxLength; } @@ -678,6 +714,9 @@ abstract class AbstractBuildForm { case 'time': // hh:mm:ss return 8; default: + if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') { + return $this->maxLengthSetEnum($typeSpec); + } break; } @@ -690,6 +729,29 @@ abstract class AbstractBuildForm { return false; } + /** + * Get the strlen of the longest element in enum('val1','val2',...,'valn') or set('val1','val2',...,'valn') + * + * @param string $typeSpec + * @return int + */ + private function maxLengthSetEnum($typeSpec) { + $startPos = (substr($typeSpec, 0, 4) === 'set(') ? 4 : 5; + $max = 0; + + $valueList = substr($typeSpec, $startPos, strlen($typeSpec) - $startPos - 1); + $valueArr = explode(',', $valueList); + foreach ($valueArr as $value) { + $value = trim($value, "'"); + $len = strlen($value); + if ($len > $max) { + $max = $len; + } + } + + return $max; + } + /** * Builds a HTML attribute list, based on $attributeList. * @@ -794,9 +856,13 @@ abstract class AbstractBuildForm { } /** + * Build HelpBlock + * * @return string */ private function getHelpBlock() { + //TODO: #3066 Class 'hidden' einbauen +// return '<div class="help-block with-errors hidden"></div>'; return '<div class="help-block with-errors"></div>'; } @@ -807,15 +873,18 @@ abstract class AbstractBuildForm { * * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked"> * <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"] - * [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] > + * [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] > * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE* * @return string - * @throws UserFormException + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildCheckbox(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $itemKey = array(); $itemValue = array(); @@ -829,7 +898,7 @@ abstract class AbstractBuildForm { } if ($formElement['checkBoxMode'] === 'multi') { - $htmlFormElementId .= '[]'; +// $htmlFormElementId .= '[]'; } else { // Fill meaningfull defaults to parameter: checked|unchecked (CHECKBOX_VALUE_CHECKED|CHECKBOX_VALUE_UNCHECKED) $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement); @@ -840,17 +909,15 @@ abstract class AbstractBuildForm { switch ($formElement['checkBoxMode']) { case 'single': - $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value); + $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value, $json); break; case 'multi'; - $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue); + $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue, $json); break; default: throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN); } - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); -// return Support::wrapTag('<div class="checkbox">', $html, true); return $html; } @@ -946,7 +1013,7 @@ abstract class AbstractBuildForm { $fieldTypeDefinition = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES); if ($fieldTypeDefinition === false) { - throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN); + throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec[F_TABLE_NAME] . "'", ERROR_DB_UNKNOWN_COLUMN); } $length = strlen($fieldTypeDefinition); @@ -966,7 +1033,8 @@ abstract class AbstractBuildForm { // enum('a','b','c', ...) >> [ 'a', 'b', 'c', ... ] // set('a','b','c', ...) >> [ 'a', 'b', 'c', ... ] - $items = OnArray::trimArray(explode(',', substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1)), "'"); + $values = substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1); + $items = OnArray::trimArray(explode(',', $values), "'"); $fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1); return $items; @@ -1016,10 +1084,12 @@ abstract class AbstractBuildForm { * @param $htmlFormElementId * @param $attribute * @param $value + * @param array $json * @return string */ - public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) { + public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value, array &$json) { $html = ''; + $valueJson = false; $attribute .= Support::doAttribute('name', $htmlFormElementId); $attribute .= Support::doAttribute('value', $formElement['checked'], false); @@ -1028,6 +1098,7 @@ abstract class AbstractBuildForm { if ($formElement['checked'] === $value) { $attribute .= Support::doAttribute('checked', 'checked'); + $valueJson = true; } $attribute .= $this->getAttributeList($formElement, ['autofocus']); @@ -1042,6 +1113,8 @@ abstract class AbstractBuildForm { $html = Support::wrapTag("<label>", $html, true); $html = Support::wrapTag("<div class='checkbox'>", $html, true); + $json = $this->getJsonElementUpdate($htmlFormElementId, $valueJson, $formElement[FE_MODE]); + return $html; } @@ -1057,17 +1130,19 @@ abstract class AbstractBuildForm { * @param $value * @param array $itemKey * @param array $itemValue + * @param array $json * @return string */ - public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) { + public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue, array &$json) { + $json = array(); // Defines which of the checkboxes will be checked. $values = explode(',', $value); - $attributeBase .= Support::doAttribute('name', $htmlFormElementId); +// $attributeBase .= Support::doAttribute('name', $htmlFormElementId); $attributeBase .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); - $html = $this->buildNativeHidden($htmlFormElementId, ''); + $html = $this->buildNativeHidden(HelperFormElement::prependFormElementIdCheckBoxMulti($htmlFormElementId, 'h'), ''); $orientation = ($formElement['maxLength'] > 1) ? ALIGN_HORIZONTAL : ALIGN_VERTICAL; $checkboxClass = ($orientation === ALIGN_HORIZONTAL) ? 'checkbox-inline' : 'checkbox'; @@ -1075,8 +1150,10 @@ abstract class AbstractBuildForm { $flagFirst = true; for ($ii = 0, $jj = 1; $ii < count($itemKey); $ii++, $jj++) { - + $jsonValue = false; $attribute = $attributeBase; + $htmlFormElementIdUniq = HelperFormElement::prependFormElementIdCheckBoxMulti($htmlFormElementId, $ii); + $attribute .= Support::doAttribute('name', $htmlFormElementIdUniq); // Do this only the first round. if ($flagFirst) { @@ -1090,6 +1167,7 @@ abstract class AbstractBuildForm { // Check if the given key is found in field. if (false !== array_search($itemKey[$ii], $values)) { $attribute .= Support::doAttribute('checked', 'checked'); + $jsonValue = true; } // ' ' - This is necessary to correctly align an empty input. @@ -1097,16 +1175,14 @@ abstract class AbstractBuildForm { $htmlElement = '<input ' . $attribute . '>' . $value; -// $htmlElement = Support::wrapTag("<label class='$orientation'>", $htmlElement, true); -// $htmlElement = Support::wrapTag("<label>", $htmlElement, true); // With ALIGN_HORIZONTAL: the label causes some trouble: skip it if (($orientation === ALIGN_VERTICAL)) { $htmlElement = Support::wrapTag('<label>', $htmlElement); } - $htmlElement = Support::wrapTag("<div class='$checkboxClass'>", $htmlElement, true); + // control orientation if ($formElement['maxLength'] > 1) { if ($jj == $formElement['maxLength']) { @@ -1118,6 +1194,8 @@ abstract class AbstractBuildForm { } $html .= $htmlElement . $br; + $json[] = $this->getJsonElementUpdate($htmlFormElementIdUniq, $jsonValue, $formElement[FE_MODE]); + } // if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] !== 'vertical') @@ -1127,7 +1205,7 @@ abstract class AbstractBuildForm { } /** - * Submit hidden values by SIP. + * Submit extra (hidden) values by SIP. * * Sometimes, it's usefull to precalculate values during formload and to submit them as hidden fields. * To avoid any manipulation on those fields, the values will be transferred by SIP. @@ -1135,11 +1213,20 @@ abstract class AbstractBuildForm { * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildHidden(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildExtra(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { - $this->store->setVar($htmlFormElementId, $value, STORE_SIP, false); + if ($mode === FORM_LOAD) { + // Split 'grId:0' in 'grId' and '0' + $name = explode(':', $htmlFormElementId, 2); + + $this->store->setVar($name[0], $value, STORE_SIP, false); + } } /** @@ -1149,15 +1236,18 @@ abstract class AbstractBuildForm { * * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked"> * <input name="$htmlFormElementId" type="radio" [autofocus="autofocus"] - * [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] > + * [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] > * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string - * @throws UserFormException + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildRadio(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildRadio(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $itemKey = array(); $itemValue = array(); @@ -1177,6 +1267,7 @@ abstract class AbstractBuildForm { $br = ''; $html = $this->buildNativeHidden($htmlFormElementId, $value); + for ($ii = 0; $ii < count($itemValue); $ii++) { $jj++; $attribute = $attributeBase; // @@ -1194,9 +1285,9 @@ abstract class AbstractBuildForm { } // ' ' - This is necessary to correctly align an empty input. - $value = ($itemValue[$ii] === '') ? ' ' : $itemValue[$ii]; + $tmpValue = ($itemValue[$ii] === '') ? ' ' : $itemValue[$ii]; - $htmlElement = '<input ' . $attribute . '>' . $value; + $htmlElement = '<input ' . $attribute . '>' . $tmpValue; // With ALIGN_HORIZONTAL: the label causes some trouble: skip it if (($orientation === ALIGN_VERTICAL)) { @@ -1229,9 +1320,13 @@ abstract class AbstractBuildForm { * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return mixed + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildSelect(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildSelect(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $itemKey = array(); $itemValue = array(); @@ -1278,16 +1373,19 @@ abstract class AbstractBuildForm { } /** - * Constuct a HTML table of the subrecord data. + * Construct a HTML table of the subrecord data. * Column syntax definition: https://wikiit.math.uzh.ch/it/projekt/qfq/qfq-jqwidgets/Documentation#Type:_subrecord * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string - * @throws UserFormException + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildSubrecord(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildSubrecord(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $rcText = false; $nameColumnId = 'id'; $targetTableName = ''; @@ -1305,6 +1403,9 @@ abstract class AbstractBuildForm { if (isset($formElement[SUBRECORD_PARAMETER_FORM])) { + Support::setIfNotSet($formElement, F_EXTRA_DELETE_FORM, ''); + $formElement[F_FINAL_DELETE_FORM] = $formElement[F_EXTRA_DELETE_FORM] != '' ? $formElement[F_EXTRA_DELETE_FORM] : $formElement[SUBRECORD_PARAMETER_FORM]; + $linkNew = Support::wrapTag('<th>', $this->createFormLink($formElement, 0, $primaryRecord, $this->symbol[SYMBOL_NEW], 'New')); // Decode settings in subrecordOption @@ -1325,9 +1426,11 @@ abstract class AbstractBuildForm { $columns .= '<th>' . implode('</th><th>', $control['title']) . '</th>'; } - if ($flagDelete) + if ($flagDelete) { $columns .= '<th></th>'; + } + // Table head $html = Support::wrapTag('<tr>', $columns); foreach ($formElement['sql1'] as $row) { @@ -1341,13 +1444,25 @@ abstract class AbstractBuildForm { // All columns foreach ($row as $columnName => $value) { - if (isset($control['title'][$columnName])) + if (isset($control['title'][$columnName])) { $rowHtml .= Support::wrapTag('<td>', $this->renderCell($control, $columnName, $value)); + } } if ($flagDelete) { - $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>')); + $toolTip = 'Delete'; + + if ($this->showDebugInfo) { + $toolTip .= PHP_EOL . "form = '" . $formElement[F_FINAL_DELETE_FORM] . "'" . PHP_EOL . "r = '" . $row[$nameColumnId] . "'"; + } + +// $buttonDelete = $this->buildButtonCode('delete-button', $toolTip, GLYPH_ICON_DELETE, $disabled); + + $s = $this->createDeleteUrl($formElement[F_FINAL_DELETE_FORM], '', $row[$nameColumnId], RETURN_SIP); +// $rowHtml .= Support::wrapTag('<td>', Support::wrapTag("<button type='button' class='record-delete btn btn-default' data-sip='$s'>", '<span class="glyphicon ' . GLYPH_ICON_DELETE . '"></span>')); + $rowHtml .= Support::wrapTag('<td>', Support::wrapTag("<button type='button' class='record-delete btn btn-default' data-sip='$s' " . Support::doAttribute('title', $toolTip) . ">", '<span class="glyphicon ' . GLYPH_ICON_DELETE . '"></span>')); + + } Support::setIfNotSet($row, FE_SUBRECORD_ROW_CLASS); @@ -1369,6 +1484,7 @@ abstract class AbstractBuildForm { * 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. + * - determine &$nameColumnId * * @param $formElement * @param $primaryRecord @@ -1380,7 +1496,7 @@ abstract class AbstractBuildForm { private function prepareSubrecod(array $formElement, array $primaryRecord, &$rcText, &$nameColumnId) { if (!isset($primaryRecord['id'])) { - $rcText = 'Please save record first.'; + $rcText = 'Please save and close record and reopen it.'; return false; } @@ -1394,9 +1510,16 @@ abstract class AbstractBuildForm { return true; } - if (!isset($formElement['sql1'][0][$nameColumnId])) + // Check if $nameColumnId column exist. + if (!isset($formElement['sql1'][0][$nameColumnId])) { + // no: try fallback. $nameColumnId = '_id'; + if (!isset($formElement['sql1'][0][$nameColumnId])) { + throw new UserFormException('Missing column \'id\' or \'_id\' in subrecord query', ERROR_SUBRECORD_MISSING_COLUMN_ID); + } + } + if (!isset($formElement['sql1'][0][$nameColumnId])) { throw new UserFormException('Missing column \'id\' (or "_id") in \'sql1\' Query', ERROR_DB_MISSING_COLUMN_ID); } @@ -1426,6 +1549,8 @@ abstract class AbstractBuildForm { */ private function createFormLink(array $formElement, $targetRecordId, array $record, $symbol, $toolTip) { + //TODO: Umstellen auf Benutzung der Link Klasse. + $queryStringArray = [ SIP_FORM => $formElement[SUBRECORD_PARAMETER_FORM], SIP_RECORD_ID => $targetRecordId @@ -1462,7 +1587,7 @@ abstract class AbstractBuildForm { $sip = $this->store->getSipInstance(); $url = $sip->queryStringToSip($queryString); - return Support::wrapTag('<a class="btn btn-default" href="' . $url . '" title="' . $toolTip . '">', $symbol); + return Support::wrapTag('<a class="btn btn-default" ' . Support::doAttribute('href', $url) . ' title="' . $toolTip . '">', $symbol); } /** @@ -1474,9 +1599,9 @@ abstract class AbstractBuildForm { * @throws DbException */ private function getFormTable($formName) { - $row = $this->db->sql("SELECT tableName FROM Form AS f WHERE f.name = ?", ROW_EXPECT_0_1, [$formName]); - if (isset($row['tableName'])) { - return $row['tableName']; + $row = $this->db->sql("SELECT " . F_TABLE_NAME . " FROM Form AS f WHERE f.name = ?", ROW_EXPECT_0_1, [$formName]); + if (isset($row[F_TABLE_NAME])) { + return $row[F_TABLE_NAME]; } return ''; @@ -1498,7 +1623,7 @@ abstract class AbstractBuildForm { * mailto Only key. Value will rendered (later) as a 'href mailto' * * - * @param $titleRaw + * @param array $titleRaw * @return array * @throws UserFormException */ @@ -1525,6 +1650,7 @@ abstract class AbstractBuildForm { case 'width': case 'nostrip': case 'title': + case 'link': break; case 'icon': case 'url': @@ -1541,6 +1667,12 @@ abstract class AbstractBuildForm { if (!isset($control['title'][$columnName])) $control['title'][$columnName] = ''; // Fallback: Might be wrong, but better than nothing. + // Don't render Columns starting with '_...'. + if (substr($control['title'][$columnName], 0, 1) === '_') { + unset($control['title'][$columnName]); // Do not render column later. + continue; + } + // Limit title length $control['title'][$columnName] = substr($control['title'][$columnName], 0, $control['width'][$columnName]); @@ -1564,10 +1696,11 @@ abstract class AbstractBuildForm { * * @param array $control * @param $columnName - * @param $value + * @param $columnValue * @return string */ - private function renderCell(array $control, $columnName, $value) { + private function renderCell(array $control, $columnName, $columnValue) { + $link = null; switch ($columnName) { case FE_SUBRECORD_ROW_CLASS: @@ -1577,25 +1710,32 @@ abstract class AbstractBuildForm { break; } - $arr = explode('|', $value); + $arr = explode('|', $columnValue); if (count($arr) == 1) $arr[1] = $arr[0]; - $cell = isset($control['nostrip'][$columnName]) ? $value : strip_tags($value); + $cell = isset($control['nostrip'][$columnName]) ? $columnValue : strip_tags($columnValue); if ($control['width'][$columnName] !== false) $cell = substr($cell, 0, $control['width'][$columnName]); if (isset($control['icon'][$columnName])) { - $cell = ($cell === '') ? '' : "<image src='fileadmin/icons/$cell'>"; + $cell = ($cell === '') ? '' : "<image src='" . PATH_ICONS . "/$cell'>"; } if (isset($control['mailto'][$columnName])) { - $cell = "<a href='mailto:$arr[0]'>$arr[1]</a>"; + $cell = "<a " . Support::doAttribute('href', "mailto:$arr[0]") . ">$arr[1]</a>"; } if (isset($control['url'][$columnName])) { - $cell = "<a href='$arr[0]'>$arr[1]</a>"; + $cell = "<a " . Support::doAttribute('href', $arr[0]) . ">$arr[1]</a>"; + } + + if (isset($control['link'][$columnName])) { + if (!isset($link)) { + $link = new Link($this->sip); + } + $cell = $link->renderLink($columnValue); } return $cell; @@ -1604,34 +1744,54 @@ abstract class AbstractBuildForm { /** * Create a link (incl. SIP) to delete the current record. * + * @param string $formName if there is a form, specify that + * @param string $tableName if there is no form , specify the table from where to delete the record. + * @param int $recordId record to delete + * @param string $mode + * * mode=RETURN_URL: return complete URL + * * mode=RETURN_SIP: returns only the sip + * * mode=RETURN_ARRAY: returns array with url ('_url') and all decoded and created parameters. * @return string String: "API_DIR/delete.php?sip=...." + * @throws CodeException */ - public function createDeleteUrl($table, $recordId, $mode = RETURN_URL) { + public function createDeleteUrl($formName, $tableName, $recordId, $mode = RETURN_URL) { $queryStringArray = [ - SIP_TABLE => $table, - SIP_RECORD_ID => $recordId + SIP_RECORD_ID => $recordId, + SIP_MODE_ANSWER => MODE_JSON ]; + if ($formName !== '') { + $queryStringArray[SIP_FORM] = $formName; + } + + if ($tableName !== '') { + $queryStringArray[SIP_TABLE] = $tableName; + } + $queryString = Support::arrayToQueryString($queryStringArray); $sip = $this->store->getSipInstance(); - return $sip->queryStringToSip($queryString, $mode, API_DIR . '/delete.php'); + return $sip->queryStringToSip($queryString, $mode, API_DIR . '/' . API_DELETE_PHP); } /** - * Builds an Upload (File) Button. + * Build an Upload (File) Button. * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string - * @throws UserFormException + * @throws CodeException + * @throws \qfq\UserFormException */ - public function buildFile(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildFile(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $attribute = ''; + # Build param array for uniq SIP $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); @@ -1639,6 +1799,7 @@ abstract class AbstractBuildForm { $arr[CLIENT_FORM] = $this->formSpec['name']; $arr[CLIENT_RECORD_ID] = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); $arr[CLIENT_PAGE_ID] = 'fake'; + $arr[EXISTING_PATH_FILE_NAME] = $value; $sipUpload = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP); $hiddenSipUpload = $this->buildNativeHidden($htmlFormElementId, $sipUpload); @@ -1651,7 +1812,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); $attribute .= Support::doAttribute('data-sip', $sipUpload); - if ($value === '') { + if ($value === '' || $value === false) { $textDeleteClass = 'hidden'; $uploadClass = ''; } else { @@ -1664,7 +1825,7 @@ abstract class AbstractBuildForm { $attribute .= Support::doAttribute('class', $uploadClass, true); $htmlInputFile = '<input ' . $attribute . '>' . $this->getHelpBlock(); - $deleteButton = Support::wrapTag("<button class='delete-file' data-sip='$sipUpload' name='delete-$htmlFormElementId'>", $this->symbol[SYMBOL_DELETE]); + $deleteButton = Support::wrapTag("<button type='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); @@ -1676,74 +1837,22 @@ abstract class AbstractBuildForm { return $htmlTextDelete . $htmlInputFile . $hiddenSipUpload; } - /** - * Builds HTML 'input' element. - * Format: <input name="$htmlFormElementId" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"] - * [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"] - * [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value"> - * - * - * @param array $formElement - * @param $htmlFormElementId - * @param $value - * @return string - * @throws UserFormException - */ - public function builddateJQW(array $formElement, $htmlFormElementId, $value, &$json) { - $textarea = ''; - - $attribute = Support::doAttribute('name', $htmlFormElementId); - - $htmlTag = '<input'; - - $this->adjustMaxLength($formElement); - - // Get date format - if (!isset($formElement['dateFormat'])) { - $formElement['dateFormat'] = $this->store->getVar(SYSTEM_DATE_FORMAT, STORE_SYSTEM); - } - // Format date - $value = Support::convertDateTime($value, $formElement[FE_DATE_FORMAT], $formElement[FE_SHOW_ZERO], 0, $formElement[FE_SHOW_SECONDS]); - - // <input> - if ($formElement['maxLength'] > 0) { - // crop string only if it's not empty (substr returns false on empty strings) - if ($value !== '') - $value = substr($value, 0, $formElement['maxLength']); - - // 'maxLength' needs an upper 'L': naming convention for DB tables! - $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']); - $attribute .= Support::doAttribute('value', htmlentities($value), false); - } - - $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']); - $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); - $attribute .= Support::doAttribute('title', $formElement['tooltip']); - $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']); - - $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]); - - $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); - - return "$htmlTag $attribute>$textarea"; - - } - /** * Builds HTML 'input' element. * Format: <input name="$htmlFormElementId" <type="date" [autocomplete="autocomplete"] [autofocus="autofocus"] * [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"] - * [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value"> + * [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value"> * * * @param array $formElement * @param $htmlFormElementId * @param $value - * @param $json + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string * @throws UserFormException */ - public function buildDateTime(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildDateTime(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $attribute = Support::doAttribute('name', $htmlFormElementId); $attribute .= Support::doAttribute('class', 'form-control'); @@ -1755,7 +1864,7 @@ abstract class AbstractBuildForm { $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[FE_TYPE], $formElement['dateFormat']); + $formElement['checkPattern'] = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT]); switch ($formElement['checkType']) { @@ -1797,10 +1906,10 @@ abstract class AbstractBuildForm { $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm'; switch ($formElement[FE_TYPE]) { case 'date': - $placeholder = $formElement['dateFormat']; + $placeholder = $formElement[FE_DATE_FORMAT]; break; case 'datetime': - $placeholder = $formElement['dateFormat'] . ' ' . $timePattern; + $placeholder = $formElement[FE_DATE_FORMAT] . ' ' . $timePattern; break; case 'time': $placeholder = $timePattern; @@ -1829,15 +1938,234 @@ abstract class AbstractBuildForm { } + /** + * Builds HTML 'input' element. + * Format: <input name="$htmlFormElementId" <type="date" [autocomplete="autocomplete"] [autofocus="autofocus"] + * [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"] + * [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value"> + * + * + * @param array $formElement + * @param $htmlFormElementId + * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE + * @return string + * @throws UserFormException + */ + public function buildDateJQW(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { + $arrMinMax = null; + +// if ($formElement['placeholder'] == '') { +// $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm'; +// switch ($formElement[FE_TYPE]) { +// case 'date': +// $placeholder = $formElement[FE_DATE_FORMAT]; +// break; +// case 'datetime': +// $placeholder = $formElement[FE_DATE_FORMAT] . ' ' . $timePattern; +// break; +// case 'time': +// $placeholder = $timePattern; +// break; +// default: +// throw new UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE); +// } +// $formElement['placeholder'] = $placeholder; +// } + +// switch ($formElement['checkType']) { +//// case SANITIZE_ALLOW_PATTERN: +//// $formElement['checkPattern'] = $tmpPattern; +//// break; +// case SANITIZE_ALLOW_MIN_MAX_DATE: +// $arrMinMax = explode('|', $formElement['checkPattern'], 2); +// if (count($arrMinMax) != 2) { +// throw new UserFormException('Missing min|max definition', ERROR_MISSING_MIN_MAX); +// } +// break; +//// case SANITIZE_ALLOW_ALL: +//// case SANITIZE_ALLOW_ALNUMX: +//// case SANITIZE_ALLOW_ALLBUT: +//// $formElement['checkType'] = SANITIZE_ALLOW_PATTERN; +//// break; +// default: +// throw new UserFormException("Checktype not applicable for date/time: '" . $formElement['checkType'] . "'", ERROR_NOT_APPLICABLE); +// } + + $this->adjustMaxLength($formElement); + $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]); + +// $formElement[FE_DATE_FORMAT] + + $attribute = Support::doAttribute('id', $htmlFormElementId); + $attribute .= Support::doAttribute('class', 'jqw-datetimepicker'); + $attribute .= Support::doAttribute('data-control-name', "$htmlFormElementId"); + $attribute .= Support::doAttribute('data-format-string', "dd.MM.yyyy HH:mm"); + $attribute .= Support::doAttribute('data-show-time-button', "true"); +// $attribute .= Support::doAttribute('data-placeholder', $formElement['placeholder']); + $attribute .= Support::doAttribute('data-value', htmlentities($value), false); +// $attribute .= Support::doAttribute('data-autofocus', $formElement['autofocus']); + $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); + $attribute .= Support::doAttribute('data-title', $formElement['tooltip']); + +// if (is_array($arrMinMax)) { +// $attribute .= Support::doAttribute('data-min', $arrMinMax[0]); +// $attribute .= Support::doAttribute('data-max', $arrMinMax[1]); +// } + + $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]); + +// $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); + + $element = Support::wrapTag("<div $attribute>", '', false); + + return $element . $this->getHelpBlock(); + } + + /** + * Build a HTML 'textarea' element which becomes a TinyMCE Editor. + * List of possible plugins: https://www.tinymce.com/docs/plugins/ + * + * @param array $formElement + * @param $htmlFormElementId + * @param $value + * @param array $json + * @param string $mode + * @return string + * @throws \qfq\UserFormException + */ + public function buildEditor(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { + + //TODO plugin autoresize nutzen um Editorgroesse anzugeben + + $this->adjustMaxLength($formElement); + + $attribute = Support::doAttribute('name', $htmlFormElementId); + $attribute .= Support::doAttribute('id', $htmlFormElementId); + + $attribute .= Support::doAttribute('class', 'qfq-tinymce'); + $attribute .= Support::doAttribute('data-control-name', "$htmlFormElementId"); + $attribute .= Support::doAttribute('data-placeholder', $formElement['placeholder']); +// $attribute .= Support::doAttribute('data-autofocus', $formElement['autofocus']); + $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : ''); + $attribute .= Support::doAttribute('data-title', $formElement['tooltip']); + + $formElement = $this->setEditorConfig($formElement, $htmlFormElementId); + // $formElement['editor-plugins']='autoresize code' + // $formElement['editor-contextmenu']='link image | cell row column' + $json = $this->getPrefixedElementsAsJSON(FE_EDITOR_PREFIX, $formElement); + $attribute .= Support::doAttribute('data-config', $json, true, ESCAPE_WITH_HTML_QUOTE); + + + $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]); + + $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]); + + $element = Support::wrapTag("<textarea $attribute>", htmlentities($value), false); + + return $element . $this->getHelpBlock(); + } + + /** + * Parse $formElement[FE_EDITOR_*] settings and build editor settings. + * + * @param array $formElement + * @param $htmlFormElementId + * @return array + */ + private function setEditorConfig(array $formElement, $htmlFormElementId) { + $flagMaxHeight = false; + + // plugins + if (!isset($formElement[FE_EDITOR_PREFIX . 'plugins'])) { + $formElement[FE_EDITOR_PREFIX . 'plugins'] = 'code link searchreplace table textcolor textpattern visualchars'; + } + + // toolbar: https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols + if (!isset($formElement[FE_EDITOR_PREFIX . 'toolbar'])) { + $formElement[FE_EDITOR_PREFIX . 'toolbar'] = 'code searchreplace undo redo | ' . + 'styleselect link table | fontselect fontsizeselect | ' . + 'bullist numlist outdent indent | forecolor backcolor bold italic'; + } + + // menubar + if (!isset($formElement[FE_EDITOR_PREFIX . 'menubar'])) { + $formElement[FE_EDITOR_PREFIX . 'menubar'] = 'false'; + } + + // autofocus + if (isset($formElement['autofocus']) && $formElement['autofocus'] == 'yes') { + $formElement[FE_EDITOR_PREFIX . 'auto_focus'] = $htmlFormElementId; + } + + // Check for min_height, max_height + $minMax = explode(',', $formElement['size'], 2); + if (isset($minMax[0]) && ctype_digit($minMax[0]) && !isset($formElement[FE_EDITOR_PREFIX . 'min_height'])) { + $formElement[FE_EDITOR_PREFIX . 'min_height'] = $minMax[0]; + } + if (isset($minMax[1]) && ctype_digit($minMax[1]) && !isset($formElement[FE_EDITOR_PREFIX . 'max_height'])) { + $formElement[FE_EDITOR_PREFIX . 'max_height'] = $minMax[1]; + $flagMaxHeight = true; + } + + // statusbar: disable if not user defined and if no max_height is given. + if (!$flagMaxHeight && !isset($formElement[FE_EDITOR_PREFIX . 'statusbar'])) { + $formElement[FE_EDITOR_PREFIX . 'statusbar'] = 'false'; + } + + return $formElement; + } + + /** + * Searches for '$prefix*' elements in $formElement. Collect all found elements, strip $prefix (=$keyName) and + * returns keys/values JSON encoded. + * Only 'alpha' chars are allowed as keyName. + * Empty $settings are ok. + * + * @param string $prefix + * @param array $formElement + * @return string + * @throws \qfq\UserFormException + */ + private function getPrefixedElementsAsJSON($prefix, array $formElement) { + $settings = array(); + + // E.g.: $key = editor-plugins + foreach ($formElement as $key => $value) { + if (substr($key, 0, strlen($prefix)) == $prefix) { + + $keyName = substr($key, strlen($prefix)); + + if ($keyName == '') { + throw new UserFormException("Empty '" . $prefix . "*' keyname: '" . $keyName . "'", ERROR_INVALID_EDITOR_PROPERTY_NAME); + } + + // $value might be boolean false, which should be used! Do not compare with ''. + if (isset($value)) { + // real boolean are important for TinyMCE config 'statusbar', 'menubar', ... + if ($value === 'false') $value = false; + if ($value === 'true') $value = true; + $settings[$keyName] = $value; + } + } + } + + return json_encode($settings); + } + /** * Build Grid JQW element. * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param $fake + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @throws UserFormException */ - public function buildGridJQW(array $formElement, $htmlFormElementId, $value) { + public function buildGridJQW(array $formElement, $htmlFormElementId, $value, $fake, $mode = FORM_LOAD) { // TODO: implement throw new UserFormException("Not implemented yet: buildGridJQW()", ERROR_NOT_IMPLEMENTED); } @@ -1848,21 +2176,24 @@ abstract class AbstractBuildForm { * @param array $formElement * @param $htmlFormElementId * @param $value + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE + * @param array $json * @return mixed */ - public function buildNote(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildNote(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { return $value; } /** - * Build Pill + * Build Pill: * * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json * @return mixed */ - public function buildPill(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildPill(array $formElement, $htmlFormElementId, $value, array &$json) { return $value; } @@ -1872,9 +2203,13 @@ abstract class AbstractBuildForm { * @param array $formElement * @param $htmlFormElementId * @param $value + * @param array $json + * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return mixed + * @throws CodeException + * @throws DbException */ - public function buildFieldset(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildFieldset(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) { $attribute = ''; // save parent processed FE's @@ -1913,22 +2248,24 @@ abstract class AbstractBuildForm { } /** + * Create a delete link. + * * @param $table * @param $recordId * @param $symbol * @param $toolTip * @return string */ - private function createDeleteLink($table, $recordId, $symbol, $toolTip) { - - if ($this->showDebugInfo) { - $toolTip .= PHP_EOL . "table = '$table'" . PHP_EOL . "id = '$recordId'"; - } - - $url = $this->createDeleteUrl($table, $recordId); - - return Support::wrapTag('<a href="' . $url . '" title="' . $toolTip . '">', $symbol); - - } +// private function createDeleteLink($table, $recordId, $symbol, $toolTip) { +// +// if ($this->showDebugInfo) { +// $toolTip .= PHP_EOL . "table = '$table'" . PHP_EOL . "id = '$recordId'"; +// } +// +// $url = $this->createDeleteUrl('', $table, $recordId); +// +// return Support::wrapTag('<a ' . Support::doAttribute('href', $url) . ' title="' . $toolTip . '">', $symbol); +// +// } } \ No newline at end of file diff --git a/extension/qfq/qfq/BodytextParser.php b/extension/qfq/qfq/BodytextParser.php index 706e69d2f9c889ed658085ff84c2600c07abe8a1..f4db8c699c099fa6798a16dcc48f30633c914799 100644 --- a/extension/qfq/qfq/BodytextParser.php +++ b/extension/qfq/qfq/BodytextParser.php @@ -12,21 +12,27 @@ const NESTING_TOKEN_OPEN = '#&nesting-open-&#'; const NESTING_TOKEN_CLOSE = '#&nesting-close&#'; const NESTING_TOKEN_LENGTH = 17; -class BodytextParser { +class BodytextParser { /** * @param $bodytext * @return mixed */ public function process($bodytext) { - $bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext); + + $nestingOpen = ''; + $nestingClose = ''; + + $bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext, $nestingOpen, $nestingClose); // 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->trimAndRemoveCommentAndEmptyLine($bodytext); + $bodytext = $this->joinLine($bodytext, $nestingOpen, $nestingClose); + + $bodytext = $this->encryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose); + $bodytext = $this->unNest($bodytext, $nestingOpen, $nestingClose); + + $bodytext = $this->trimAndRemoveCommentAndEmptyLine($bodytext, $nestingOpen, $nestingClose); $bodytext = Support::decryptDoubleCurlyBraces($bodytext); if (strpos($bodytext, NESTING_TOKEN_OPEN) !== false) { @@ -42,55 +48,125 @@ class BodytextParser { * @return string */ - private function trimAndRemoveCommentAndEmptyLine($bodytext) { + private function trimAndRemoveCommentAndEmptyLine($bodytext, &$nestingOpen, &$nestingClose) { $data = array(); $src = explode(PHP_EOL, $bodytext); + if ($src === false) { + return ''; + } + + $firstLine = trim($src[0]); + foreach ($src as $row) { $row = trim($row); + if ($row === '' || $row[0] === '#') { continue; } $data[] = $row; } + $this->setNestingToken($firstLine, $nestingOpen, $nestingClose); + return implode(PHP_EOL, $data); } /** - * Encrypt '{\n' and '}\n' by more complex token. + * Set the 'nesting token for this tt-conten record. Valid tokens are {}, <>, [], (). + * If the first line of bodytext is a comment line and the last char of that line is a valid token: set that one. + * If not: set {} as nesting token. * - * @param $bodytext - * @return mixed + * Example: + * # Some nice text - no token found, take {} + * # ] - [] + * # Powefull QFQ: < - <> + * + * @param $firstLine + * @param $nestingOpen + * @param $nestingClose */ - private function encryptNestingDelimeter($bodytext) { - // Take care that a trailing '}' will be recognised: add '\n' - if (substr($bodytext, -1) === '}') { - $bodytext .= "\n"; + private function setNestingToken($firstLine, &$nestingOpen, &$nestingClose) { + + if ($nestingOpen !== '') { + return; // tokens already set or not bodytext: do not change. } - $bodytext = str_replace("{\n", NESTING_TOKEN_OPEN, $bodytext); - $bodytext = str_replace("}\n", NESTING_TOKEN_CLOSE, $bodytext); - return $bodytext; + // Nothing defined: set default {}. + if ($firstLine === false || $firstLine === '' || $firstLine[0] !== '#') { + $nestingOpen = '{'; + $nestingClose = '}'; + return; + } + + $pos = 0; + $tokenList = '{}<>[]()'; + + // Definition: first line of bodytext, has to be a comment line. If the last char is one of the valid token: set that one. + // Nothing found: set {}. + + if ($firstLine[0] === '#') { + $token = substr($firstLine, -1); + $pos = strpos($tokenList, $token); + if ($pos === false) { + $pos = 0; + } else { + if ($pos % 2 === 1) { + $pos -= 1; + } + } + } + + $nestingOpen = substr($tokenList, $pos, 1); + $nestingClose = substr($tokenList, $pos + 1, 1); } /** - * Join lines, which do not begin with '<level>.<keyword>[ ]=' + * Join lines. Preservers Nesting. + * + * Iterates over all lines. + * Is a line a 'new line'? + * no: concat it to the last one. + * yes: flush the buffer, start a new 'new line' + * + * New Line Trigger: + * a: { + * b: } + * c: 20 + * d: 20.30 + * + * e: 5 { + * f: 5.10 { + * + * g: head = + * h: 10.20.head = + * + * c,d,e,f: ^\d+(\.\d+)*(\s*{)?$ + * g,h: ^(\d+\.)*(sql|head)\s*= * * @param array $bodytextArray * @return string */ - private function joinLine($bodytext) { + private function joinLine($bodytext, $nestingOpen, $nestingClose) { $data = array(); $bodytextArray = explode(PHP_EOL, $bodytext); + $nestingOpenRegexp = $nestingOpen; + if ($nestingOpen === '(' || $nestingOpen === '[') { + $nestingOpenRegexp = '\\' . $nestingOpen; + } + $full = ''; foreach ($bodytextArray as $row) { - // Valid 'new line' starts indicators: form, <level>, <level.sublevel>, <level>.<keyword>, {, <level> {, } - if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(head|althead|tail|sql|rbeg|rend|renr|rsep|fbeg|fend|fsep|' . - TYPO3_FORM . '|' . TYPO3_DEBUG_SHOW_BODY_TEXT . '|' . TYPO3_RECORD_ID . ') *=/', $row)) - || (1 === preg_match('/^\s*(\d*(\.)?)*\s*({|})\s*/', $row)) - || (1 === preg_match('/^\s*(\d+(\.)?)+/', $row)) + + +// if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . TOKEN_VALID_LIST . ') *=/', $row)) +// || (1 === preg_match('/^\s*(\d*(\.)?)*\s*(' . $nestingOpen . '|' . $nestingClose . ')/', $row)) +// || (1 === preg_match('/^\s*(\d+(\.)?)+/', $row)) + + if (($row == $nestingOpen || $row == $nestingClose) + || (1 === preg_match('/^\d+(\.\d+)*(\s*' . $nestingOpenRegexp . ')?$/', $row)) + || (1 === preg_match('/^(\d+\.)*(' . TOKEN_VALID_LIST . ')\s*=/', $row)) ) { // if there is already something: save this @@ -114,14 +190,50 @@ class BodytextParser { return implode(PHP_EOL, $data); } -//PREG_SPLIT_DELIM_CAPTURE + /** + * Encrypt $nestingOpen and $nestingClose by a more complex token. This makes it easy to search later for '}' or '{' + * + * Valid open (complete line): {, 10 {, 10.20 { + * Valid close (complete line): } + * + * @param $bodytext + * @return mixed + */ + private function encryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) { + + if ($nestingOpen === '(' || $nestingOpen === '[') { + $nestingOpen = '\\' . $nestingOpen; + $nestingClose = '\\' . $nestingClose; + } + + $bodytext = preg_replace('/^((\d+)(\.\d+)*\s*)?(' . $nestingOpen . ')$/m', '$1' . NESTING_TOKEN_OPEN, $bodytext); + $bodytext = preg_replace('/^' . $nestingClose . '$/m', '$1' . NESTING_TOKEN_CLOSE, $bodytext); + + return $bodytext; + } /** + * Unnest all level. + * + * Input: + * 10 { + * sql = SELECT + * 20.sql = INSERT .. + * 30 { + * sql = DELETE + * } + * } + * + * Output: + * 10.sql = SELECT + * 10.20.sql = INSERT + * 10.20.30.sql = DELETE + * * @param $bodytext * @return mixed|string * @throws UserFormException */ - private function unNest($bodytext) { + private function unNest($bodytext, $nestingOpen, $nestingClose) { // Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested' // $bodytext = str_replace('\{', '#&[_#', $bodytext); @@ -135,7 +247,7 @@ class BodytextParser { $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), NESTING_TOKEN_OPEN); if ($posMatchOpen === false) { - $result = $this->decryptNestingDelimeter($result); + $result = $this->decryptNestingDelimeter($result, $nestingOpen, $nestingClose); throw new \qfq\UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER); } @@ -164,7 +276,9 @@ class BodytextParser { // Split nested content in single rows $lines = explode(PHP_EOL, $match); foreach ($lines as $line) { - $pre .= $level . '.' . $line . PHP_EOL; + if ($line !== '') { + $pre .= $level . '.' . $line . PHP_EOL; + } } $result = $pre . $post; @@ -184,10 +298,11 @@ class BodytextParser { * @param $bodytext * @return mixed */ - private function decryptNestingDelimeter($bodytext) { + private function decryptNestingDelimeter($bodytext, $nestingOpen, $nestingClose) { + + $bodytext = str_replace(NESTING_TOKEN_OPEN, "$nestingOpen\n", $bodytext); + $bodytext = str_replace(NESTING_TOKEN_CLOSE, "$nestingClose\n", $bodytext); - $bodytext = str_replace(NESTING_TOKEN_OPEN, "{\n", $bodytext); - $bodytext = str_replace(NESTING_TOKEN_CLOSE, "}\n", $bodytext); return $bodytext; } diff --git a/extension/qfq/qfq/BuildFormBootstrap.php b/extension/qfq/qfq/BuildFormBootstrap.php index e60a293388e51bd74a5e2082eec0f4670e968cd5..5e3e999afc1fb25ae0775ee9eb87a9b3f5d63a8d 100644 --- a/extension/qfq/qfq/BuildFormBootstrap.php +++ b/extension/qfq/qfq/BuildFormBootstrap.php @@ -59,8 +59,8 @@ class BuildFormBootstrap extends AbstractBuildForm { $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] = ""; + $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END] = ""; // $this->feDivClass['radio'] = 'radio'; // $this->feDivClass['checkbox'] = 'checkbox'; @@ -72,7 +72,7 @@ class BuildFormBootstrap extends AbstractBuildForm { * @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_START] = "<div class='col-md-$label qfq-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>"; @@ -113,12 +113,15 @@ class BuildFormBootstrap extends AbstractBuildForm { $html .= $this->getFormTag(); - $html .= '<div class="tab-content">'; + //TODO: Problem fuer Forms ohne Pils + $html .= '<div class="tab-content ' . $this->formSpec[F_CLASS_BODY] . '">'; +// $html .= '<div class="col-md-12 ' . $this->formSpec[F_CLASS_BODY] . '">'; return $html; } /** + * Build Buttons panel on top right corner of form. * Simulate Submit Button: http://www.javascript-coder.com/javascript-form/javascript-form-submit.phtml * * @return string @@ -137,29 +140,25 @@ class BuildFormBootstrap extends AbstractBuildForm { $toolTip = "Edit form" . PHP_EOL . PHP_EOL . OnArray::toString($this->store->getStore(STORE_SIP), ' = ', PHP_EOL, "'"); $url = $this->createFormEditUrl(); - $buttonEditForm = $this->buildButtonAnchor('form-edit-button', $url, $toolTip, 'glyphicon-wrench'); + $buttonEditForm = $this->buildButtonAnchor('form-edit-button', $url, $toolTip, GLYPH_ICON_TOOL); } // Button: Save - if (Support::findInSet(FORM_BUTTON_SAVE, $this->formSpec['showButton'])) { + if (Support::findInSet(FORM_BUTTON_SAVE, $this->formSpec['showButton']) && $this->formSpec[F_SUBMIT_BUTTON_TEXT] === '') { $toolTip = 'Save'; if ($this->showDebugInfo) { - $toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'"; + $toolTip .= PHP_EOL . "table = '" . $this->formSpec[F_TABLE_NAME] . "'" . PHP_EOL . "r = '" . $recordId . "'"; } - $buttonSave = $this->buildButtonCode('save-button', $toolTip, 'glyphicon-ok'); + $buttonSave = $this->buildButtonCode('save-button', $toolTip, GLYPH_ICON_CHECK); } // Button: Close if (Support::findInSet(FORM_BUTTON_CLOSE, $this->formSpec['showButton'])) { $toolTip = 'Close'; - if ($this->showDebugInfo) { - $toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'"; - } - - $buttonClose = $this->buildButtonCode('close-button', 'Close', 'glyphicon-remove'); + $buttonClose = $this->buildButtonCode('close-button', 'Close', GLYPH_ICON_CLOSE); } // Button: Delete @@ -167,11 +166,11 @@ class BuildFormBootstrap extends AbstractBuildForm { $toolTip = 'Delete'; if ($this->showDebugInfo && $recordId > 0) { - $toolTip .= PHP_EOL . "table = '" . $this->formSpec['tableName'] . "'" . PHP_EOL . "r = '" . $recordId . "'"; + $toolTip .= PHP_EOL . "form = '" . $this->formSpec[F_FINAL_DELETE_FORM] . "'" . PHP_EOL . "r = '" . $recordId . "'"; } $disabled = ($recordId > 0) ? '' : 'disabled'; - $buttonDelete = $this->buildButtonCode('delete-button', $toolTip, 'glyphicon-trash', $disabled); + $buttonDelete = $this->buildButtonCode('delete-button', $toolTip, GLYPH_ICON_DELETE, $disabled); } // Button: New @@ -179,13 +178,16 @@ class BuildFormBootstrap extends AbstractBuildForm { $toolTip = 'New'; $url = $this->deriveNewRecordUrlFromExistingSip($toolTip); - $buttonNew = $this->buildButtonAnchor('form-new-button', $url, $toolTip, 'glyphicon-plus'); + $buttonNew = $this->buildButtonAnchor('form-new-button', $url, $toolTip, GLYPH_ICON_NEW); } - $html = Support::wrapTag('<div class="btn-group" role="group">', $buttonEditForm); - $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonSave . $buttonClose); - $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonDelete); - $html .= Support::wrapTag('<div class="btn-group" role="group">', $buttonNew); + // Arrangement: Edit Form / Save / Close / Delete / New + // Specified in reverse order cause 'pull-right' inverts the order. http://getbootstrap.com/css/#helper-classes-floats + $html = ''; + $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonNew); + $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonDelete); + $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonSave . $buttonClose); + $html .= Support::wrapTag('<div class="btn-group pull-right" role="group">', $buttonEditForm); $html = Support::wrapTag('<div class="btn-toolbar" role="toolbar">', $html); @@ -201,10 +203,12 @@ class BuildFormBootstrap extends AbstractBuildForm { * @return string */ private function buildButtonAnchor($id, $url, $title, $icon, $disabled = '') { - return "<a href='$url' id='$id' class='btn btn-default navbar-btn $disabled' " . Support::doAttribute('title', $title) . "><span class='glyphicon $icon'></span></a>"; + return "<a " . Support::doAttribute('href', $url) . " id='$id' class='btn btn-default navbar-btn $disabled' " . Support::doAttribute('title', $title) . "><span class='glyphicon $icon'></span></a>"; } /** + * Creates a button with the given attributes. If there is no $icon given, render the button without glyph. + * * @param $id * @param $title * @param $icon @@ -212,7 +216,16 @@ class BuildFormBootstrap extends AbstractBuildForm { * @return string */ private function buildButtonCode($id, $title, $icon, $disabled = '') { - return "<button id='$id' type='button' class='btn btn-default navbar-btn $disabled' " . Support::doAttribute('title', $title) . "><span class='glyphicon $icon'></span></button>"; + + $element = "<span class='glyphicon $icon'></span>"; + $classAdd = "navbar-btn"; + + if ($icon === '') { + $element = $title; + $classAdd = ''; + } + + return "<button id='$id' type='button' class='btn btn-default $classAdd $disabled' " . Support::doAttribute('title', $title) . ">$element</button>"; } /** @@ -243,7 +256,7 @@ class BuildFormBootstrap extends AbstractBuildForm { } // Anker for pill navigation - $a = '<a href="#' . $this->createAnker($formElement['id']) . '" data-toggle="tab">' . $formElement['label'] . '</a>'; + $a = '<a ' . Support::doAttribute('href', '#' . $this->createAnker($formElement['id'])) . ' data-toggle="tab">' . $formElement['label'] . '</a>'; if ($ii <= $maxVisiblePill) { $pillButton .= '<li role="presentation" ' . $active . '>' . $a . '</li>'; @@ -255,12 +268,12 @@ class BuildFormBootstrap extends AbstractBuildForm { // Pill Dropdown necessary? if ($ii > $maxVisiblePill) { - $htmlDropdown = Support::wrapTag('<ul class="dropdown-menu">', $pillDropdown, true); + $htmlDropdown = Support::wrapTag('<ul class="dropdown-menu qfq-form-pill ' . $this->formSpec[F_CLASS_PILL] . '">', $pillDropdown, true); $htmlDropdown = '<a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button">more <span class="caret"></span></a>' . $htmlDropdown; $htmlDropdown = Support::wrapTag('<li role="presentation" class="dropdown">', $htmlDropdown, false); } - $htmlDropdown = Support::wrapTag('<ul id="' . $this->getTabId() . '" class="nav nav-pills" role="tablist">', $pillButton . $htmlDropdown); + $htmlDropdown = Support::wrapTag('<ul id="' . $this->getTabId() . '" class="nav nav-pills qfq-form-pill ' . $this->formSpec[F_CLASS_PILL] . '" role="tablist">', $pillButton . $htmlDropdown); $htmlDropdown = Support::wrapTag('<div class="col-md-12">', $htmlDropdown); return $htmlDropdown; @@ -308,6 +321,19 @@ class BuildFormBootstrap extends AbstractBuildForm { $formId = $this->getFormId(); + // Button Save at bottom of form - only if there is a button text given. + if ($this->formSpec[F_SUBMIT_BUTTON_TEXT] !== '') { + $buttonText = $this->formSpec[F_SUBMIT_BUTTON_TEXT]; + + $htmlElement = $this->buildButtonCode('save-button', $buttonText, ''); + + $html .= $this->wrapItem(WRAP_SETUP_LABEL, ''); + $html .= $this->wrapItem(WRAP_SETUP_INPUT, $htmlElement); + $html .= $this->wrapItem(WRAP_SETUP_NOTE, ''); + + $html = $this->wrapItem(WRAP_SETUP_ELEMENT, $html); + } + $html .= '</div> <!--class="tab-content" -->'; // <div class="tab-content"> // $html .= '<input type="submit" value="Submit">'; @@ -315,12 +341,15 @@ class BuildFormBootstrap extends AbstractBuildForm { $tabId = $this->getTabId(); if (0 < ($recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP))) { - $deleteUrl = $this->createDeleteUrl($this->formSpec['tableName'], $recordId); + $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], '', $recordId); } $actionUpload = FILE_ACTION . '=' . FILE_ACTION_UPLOAD; $actionDelete = FILE_ACTION . '=' . FILE_ACTION_DELETE; + $apiDir = API_DIR; + $apiDeletePhp = API_DIR . '/' . API_DELETE_PHP; + $html .= '</form>'; // <form class="form-horizontal" ... $html .= <<<EOF <script type="text/javascript"> @@ -331,14 +360,14 @@ class BuildFormBootstrap extends AbstractBuildForm { var qfqPage = new QfqNS.QfqPage({ tabsId: '$tabId', formId: '$formId', - submitTo: 'typo3conf/ext/qfq/qfq/api/save.php', + submitTo: '$apiDir/save.php', deleteUrl: '$deleteUrl', - refreshUrl: "typo3conf/ext/qfq/qfq/api/load.php", - fileUploadTo: 'typo3conf/ext/qfq/qfq/api/file.php?$actionUpload', - fileDeleteUrl: 'typo3conf/ext/qfq/qfq/api/file.php?$actionDelete' + refreshUrl: '$apiDir/load.php', + fileUploadTo: '$apiDir/file.php?$actionUpload', + fileDeleteUrl: '$apiDir/file.php?$actionDelete' }); - var qfqRecordList = new QfqNS.QfqRecordList('typo3conf/ext/qfq/qfq/api/delete.php'); + var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp'); }) </script> EOF; @@ -353,7 +382,7 @@ EOF; * @param $value * @return mixed */ - public function buildPill(array $formElement, $htmlFormElementId, $value, &$json) { + public function buildPill(array $formElement, $htmlFormElementId, $value, array &$json) { $html = ''; // save parent processed FE's $tmpStore = $this->feSpecNative; @@ -397,7 +426,7 @@ EOF; public function buildRowPill(array $formElement, $elementHtml) { $html = ''; - $html .= $this->wrapItem(WRAP_SETUP_INPUT, $elementHtml); + $html .= Support::wrapTag('<div class="col-md-12 qfq-form-body ' . $this->formSpec[F_CLASS_BODY] . '">', $elementHtml); $active = $this->isFirstPill ? ' active' : ''; @@ -409,10 +438,15 @@ EOF; } /** + * Builds a fieldset + * * @param $formElement * @param $elementHtml */ public function buildRowFieldset(array $formElement, $elementHtml) { + $html = $elementHtml; + + return $html; } /** diff --git a/extension/qfq/qfq/BuildFormPlain.php b/extension/qfq/qfq/BuildFormPlain.php index 5558eea537978c06e6e6b30b8590d1adccfae82d..9652a32f0871493d6d4bfd39458e60c9df7c09f5 100644 --- a/extension/qfq/qfq/BuildFormPlain.php +++ b/extension/qfq/qfq/BuildFormPlain.php @@ -44,6 +44,7 @@ class BuildFormPlain extends AbstractBuildForm { public function fillWrapLabelInputNote($label, $input, $note) { } + /** * @return string */ diff --git a/extension/qfq/qfq/BuildFormTable.php b/extension/qfq/qfq/BuildFormTable.php index 6964eb9a8863ab26f2b6225956cee80c11bc7b9d..4149457d713053c10b13d4355fd892b5c630282f 100644 --- a/extension/qfq/qfq/BuildFormTable.php +++ b/extension/qfq/qfq/BuildFormTable.php @@ -75,10 +75,10 @@ class BuildFormTable extends AbstractBuildForm { // Logged in BE User will see a FormEdit Link $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'"); $formEditUrl = $this->createFormEditUrl(); - $html .= "<p><a href='$formEditUrl'>Edit</a><small>[$sipParamString]</small></p>"; + $html .= "<p><a " . Support::doAttribute('href', $formEditUrl) . ">Edit</a><small>[$sipParamString]</small></p>"; - $deleteUrl = $this->createDeleteUrl($this->formSpec['tableName'], $this->store->getVar(SIP_RECORD_ID, STORE_SIP)); - $html .= "<p><a href='$deleteUrl'>Delete</a>"; + $deleteUrl = $this->createDeleteUrl($this->formSpec[F_FINAL_DELETE_FORM], '', $this->store->getVar(SIP_RECORD_ID, STORE_SIP)); + $html .= "<p><a " . Support::doAttribute('href', $deleteUrl) . ">Delete</a>"; $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec['title'], true); $html .= $this->getFormTag(); diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 2800e5de0275103723dfd0a6229f85c2e5894a86..d7f6e2e264626da937b1d0bb8c747cfd6c51b266 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -7,7 +7,7 @@ */ const EXT_KEY = 'qfq'; -const CONFIG_INI = "config.ini"; // QFQ configuration file: db access +const CONFIG_INI = "config.qfq.ini"; // QFQ configuration file: db access const GFX_INFO = 'typo3conf/ext/qfq/Resources/Public/icons/note.gif'; const API_DIR = 'typo3conf/ext/qfq/qfq/api'; @@ -15,10 +15,15 @@ const API_DIR = 'typo3conf/ext/qfq/qfq/api'; const QFQ_LOG = 'qfq.log'; const SESSION_LIFETIME_SECONDS = 86400; +const SESSION_NAME = 'qfq'; +const SESSION_FE_USER_UID = 'feUserUid'; +const SESSION_FE_USER = 'feUser'; +const SESSION_FE_USER_GROUP = 'feUserGroup'; const FORM_LOAD = 'form_load'; const FORM_SAVE = 'form_save'; const FORM_UPDATE = 'form_update'; +const FORM_DELETE = 'form_delete'; const FORM_PERMISSION_SIP = 'sip'; const FORM_PERMISSION_LOGGED_IN = 'logged_id'; const FORM_PERMISSION_LOGGED_OUT = 'logged_out'; @@ -37,15 +42,13 @@ 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'; const RETURN_SIP = 'return_sip'; const RETURN_ARRAY = 'return_array'; const SQL_FORM_ELEMENT_SPECIFIC_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.feIdContainer = ? AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; const SQL_FORM_ELEMENT_ALL_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; -const SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER = "SELECT fe.id, fe.name, fe.label, fe.type, fe.checkType, fe.checkPattern, fe.mode, fe.parameter FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; +const SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER = "SELECT fe.id, fe.name, fe.label, fe.type, fe.checkType, fe.checkPattern, fe.mode, fe.modeSql, fe.parameter, fe.dynamicUpdate FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; // SANITIZE Classifier const SANITIZE_ALLOW_ALNUMX = "alnumx"; @@ -91,8 +94,9 @@ const ERROR_UNKNOW_SANITIZE_CLASS = 1001; const ERROR_CODE_SHOULD_NOT_HAPPEN = 1003; const ERROR_SIP_MALFORMED = 1005; const ERROR_SIP_INVALID = 1006; -//const ERROR_MISSING_FORM_NAME = 1007; +const ERROR_MISSING_RECORD_ID = 1007; const ERROR_IN_SQL_STATEMENT = 1008; +const ERROR_MISSING_REQUIRED_PARAMETER = 1009; const ERROR_MISSING_SESSIONNAME = 1010; const ERROR_BROKEN_PARAMETER = 1011; const ERROR_FE_USER_UID_CHANGED = 1012; @@ -139,33 +143,53 @@ const ERROR_NOT_APPLICABLE = 1057; const ERROR_FORMELEMENT_TYPE = 1058; const ERROR_MISSING_OPEN_DELIMITER = 1059; const ERROR_MISSING_CLOSE_DELIMITER = 1060; +const ERROR_EXPECTED_ARRAY = 1061; +const ERROR_REPORT_FAILED_ACTION = 1062; +const ERROR_MISSING_MESSAGE_FAIL = 1063; +const ERROR_MISSING_TABLE_NAME = 1064; +const ERROR_MISSING_TABLE = 1065; +const ERROR_RECORD_NOT_FOUND = 1066; +const ERROR_INVALID_EDITOR_PROPERTY_NAME = 1067; +const ERROR_UNKNOWN_ESCAPE_MODE = 1068; +const ERROR_MISSING_CONFIG_INI_VALUE = 1069; +const ERROR_SENDMAIL = 1070; +const ERROR_SENDMAIL_MISSING_VALUE = 1071; +const ERROR_OVERWRITE_RECORD_ID = 1072; + +// Subrecord +const ERROR_SUBRECORD_MISSING_COLUMN_ID = 1100; // Store -const ERROR_STORE_VALUE_ALREADY_CODPIED = 1100; -const ERROR_STORE_KEY_EXIST = 1101; +const ERROR_STORE_VALUE_ALREADY_CODPIED = 1200; +const ERROR_STORE_KEY_EXIST = 1201; // I/O Error -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; +const ERROR_IO_READ_FILE = 1300; +const ERROR_IO_WRITE = 1303; +const ERROR_IO_OPEN = 1304; +const ERROR_IO_UNLINK = 1305; +const ERROR_IO_FILE_EXIST = 1306; +const ERROR_IO_RENAME = 1307; +const ERROR_IO_INVALID_LINK = 1308; +const ERROR_IO_DIR_EXIST_AS_FILE = 1309; +const ERROR_IO_CHDIR = 1310; //Report -const ERROR_UNKNOWN_LINK_QUALIFIER = 1300; -const ERROR_UNDEFINED_RENDER_CONTROL_COMBINATION = 1301; -const ERROR_MISSING_VALUE = 1302; -const ERROR_MULTIPLE_DEFINITION = 1303; -const ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION = 1304; +const ERROR_UNKNOWN_LINK_QUALIFIER = 1400; +const ERROR_UNDEFINED_RENDER_CONTROL_COMBINATION = 1401; +const ERROR_MISSING_REQUIRED_DELETE_QUALIFIER = 1402; +const ERROR_MISSING_VALUE = 1403; +const ERROR_INVALID_VALUE = 1404; +const ERROR_MULTIPLE_DEFINITION = 1405; +const ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION = 1406; +const ERROR_UNKNOWN_TOKEN = 1407; +const ERROR_TOO_FEW_PARAMETER_FOR_SENDMAIL = 1408; +const ERROR_TOO_MANY_PARAMETER = 1409; // Upload -const ERROR_UPLOAD = 1400; -const ERROR_UNKNOWN_ACTION = 1402; -const ERROR_NO_TARGET_PATH_FILE_NAME = 1403; +const ERROR_UPLOAD = 1500; +const ERROR_UNKNOWN_ACTION = 1502; +const ERROR_NO_TARGET_PATH_FILE_NAME = 1503; // KeyValueParser const ERROR_KVP_VALUE_HAS_NO_KEY = 1900; @@ -186,6 +210,10 @@ const ERROR_DB_UNKNOWN_COLUMN = 2011; const ERROR_DB_UNKNOWN_COMMAND = 2012; const ERROR_DB_MISSING_COLUMN_ID = 2013; const ERROR_DB_COLUMN_NOT_FOUND_IN_TABLE = 2014; + +// onArray +const ERROR_SUBSTITUTE_FOUND = 2100; + // // Store Names: Identifier // @@ -230,9 +258,9 @@ const CLIENT_SCRIPT_URI = 'SCRIPT_URI'; const CLIENT_HTTP_HOST = 'HTTP_HOST'; const CLIENT_HTTP_USER_AGENT = 'HTTP_USER_AGENT'; const CLIENT_SERVER_NAME = 'SERVER_NAME'; -const CLIENT_SERVER_ADDRESS = 'SERVER_ADDRESS'; +const CLIENT_SERVER_ADDRESS = 'SERVER_ADDR'; const CLIENT_SERVER_PORT = 'SERVER_PORT'; -const CLIENT_REMOTE_ADDRESS = 'REMOTE_ADDRESS'; +const CLIENT_REMOTE_ADDRESS = 'REMOTE_ADDR'; const CLIENT_REQUEST_SCHEME = 'REQUEST_SCHEME'; const CLIENT_SCRIPT_FILENAME = 'SCRIPT_FILENAME'; const CLIENT_QUERY_STRING = 'QUERY_STRING'; @@ -258,7 +286,7 @@ const SYSTEM_DB_SERVER = 'DB_SERVER'; const SYSTEM_DB_PASSWORD = 'DB_PASSWORD'; const SYSTEM_DB_NAME = 'DB_NAME'; const SYSTEM_DB_NAME_TEST = 'DB_NAME_TEST'; -const SYSTEM_SESSION_NAME = 'SESSION_NAME'; +const SYSTEM_DB_INIT = 'DB_INIT'; const SYSTEM_SQL_LOG = 'SQL_LOG'; // Logging to file const SYSTEM_SQL_LOG_MODE = 'SQL_LOG_MODE'; // Mode, which statements to log. const SYSTEM_DATE_FORMAT = 'DATE_FORMAT'; @@ -267,6 +295,8 @@ const SYSTEM_SHOW_DEBUG_INFO = 'SHOW_DEBUG_INFO'; const SYSTEM_CSS_LINK_CLASS_INTERNAL = 'CSS_LINK_CLASS_INTERNAL'; const SYSTEM_CSS_LINK_CLASS_EXTERNAL = 'CSS_LINK_CLASS_EXTERNAL'; const SYSTEM_CSS_CLASS_QFQ_CONTAINER = 'CSS_CLASS_QFQ_CONTAINER'; +const SYSTEM_CSS_CLASS_QFQ_FORM_PILL = 'CSS_CLASS_QFQ_FORM_PILL'; +const SYSTEM_CSS_CLASS_QFQ_FORM_BODY = 'CSS_CLASS_QFQ_FORM_BODY'; // computed automatically during runtime const SYSTEM_PATH_EXT = 'EXT_PATH'; @@ -291,18 +321,54 @@ const SYSTEM_REPORT_FULL_LEVEL = 'reportFullLevel'; // Keyname of SQL-column pro //const SYSTEM_FORM_ELEMENT_DEF = 'formElementDefinition'; // Type: SANITIZE_ALL / AssocArray. Formelement which are processed at the moment. Useful for error reporting. //const SYSTEM_FORM_ELEMENT_FIELD = 'formElementField'; // Type: SANITIZE_ALNUMX / String. Fieldname of processed Formelement. Useful for error reporting. +const MODE_HTML = 'html'; +const MODE_JSON = 'json'; + +const MSG_HEADER = 'header'; +const MSG_CONTENT = 'content'; +const MSG_ERROR_CODE = 'errorCode'; + +const SIP_TOKEN_LENGTH = 13; // length of string returned by `uniqid()` const SIP_SIP = CLIENT_SIP; // s const SIP_RECORD_ID = CLIENT_RECORD_ID; // r +const SIP_TARGET_URL= '_targetUrl'; // URL where to jump after delete() +const SIP_MODE_ANSWER = '_modeAnswer'; // Mode how delete() will answer to client: MODE_HTML, MODE_JSON const SIP_FORM = CLIENT_FORM; const SIP_TABLE = 'table'; // delete a record from 'table' const SIP_URLPARAM = 'urlparam'; +const SIP_MAKE_URLPARAM_UNIQ = '_makeUrlParamUniq'; // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter // FURTHER: all extracted params from 'urlparam const VAR_RANDOM = 'random'; +//const RECORD_ID_NEW = -1; + +// TOKEN evaluate +const TOKEN_ESCAPE_SINGLE_TICK = 's'; +const TOKEN_ESCAPE_DOUBLE_TICK = 'd'; +const TOKEN_FOUND_IN_STORE_QUERY = 'query'; const RANDOM_LENGTH = 32; +// Report, BodyText +const TOKEN_SQL = 'sql'; +const TOKEN_HEAD = 'head'; +const TOKEN_ALT_HEAD = 'althead'; +const TOKEN_TAIL = 'tail'; +const TOKEN_RBEG = 'rbeg'; +const TOKEN_REND = 'rend'; +const TOKEN_RENR = 'renr'; +const TOKEN_RSEP = 'rsep'; +const TOKEN_FBEG = 'fbeg'; +const TOKEN_FEND = 'fend'; +const TOKEN_FSEP = 'fsep'; +const TOKEN_RBGD = 'rbgd'; +const TOKEN_DEBUG = 'debug'; +const TOKEN_FORM = CLIENT_FORM; +const TOKEN_RECORD_ID = CLIENT_RECORD_ID; +const TOKEN_DEBUG_BODYTEXT = TYPO3_DEBUG_SHOW_BODY_TEXT; + +const TOKEN_VALID_LIST = 'sql|head|althead|tail|rbeg|rend|renr|rsep|fbeg|fend|fsep|rbgd|debug|form|r|debugShowBodyText'; // FORM - copy from table 'form' of processed form //const DEF_FORM_NAME = CLIENT_FORM; @@ -313,8 +379,11 @@ const RANDOM_LENGTH = 32; // SQL logging Modes const SQL_LOG_MODE_ALL = 'all'; const SQL_LOG_MODE_MODIFY = 'modify'; +const SQL_LOG_MODE_ERROR = 'error'; // write log entry, independent of global setting (e.g. broken Query) // api/save.php, api/delete.php, api/load.php +const API_DELETE_PHP = 'delete.php'; + const API_STATUS = 'status'; const API_MESSAGE = 'message'; const API_REDIRECT = 'redirect'; @@ -366,11 +435,25 @@ const SUBRECORD_PARAMETER_DETAIL = 'detail'; const GLYPH_ICON_EDIT = 'glyphicon-pencil'; const GLYPH_ICON_NEW = 'glyphicon-plus'; const GLYPH_ICON_DELETE = 'glyphicon-trash'; -const GLYPH_ICON_HELP = 'glyphicon glyphicon-question-sign'; -const GLYPH_ICON_INFO = 'glyphicon glyphicon-info-sign'; -const GLYPH_ICON_SHOW = 'glyphicon glyphicon-search'; +const GLYPH_ICON_HELP = 'glyphicon-question-sign'; +const GLYPH_ICON_INFO = 'glyphicon-info-sign'; +const GLYPH_ICON_SHOW = 'glyphicon-search'; const GLYPH_ICON_TOOL = 'glyphicon-wrench'; -const GLYPH_ICON_CHECK = 'glyphicon glyphicon-ok'; +const GLYPH_ICON_CHECK = 'glyphicon-ok'; +const GLYPH_ICON_CLOSE = 'glyphicon-remove'; + +// FORM +const F_NAME = 'name'; +const F_TITLE = 'title'; +const F_TABLE_NAME = 'tableName'; +const F_REQUIRED_PARAMETER = 'requiredParameter'; +const F_EXTRA_DELETE_FORM = 'extraDeleteForm'; +const F_FINAL_DELETE_FORM = 'finalDeleteForm'; + +const F_SUBMIT_BUTTON_TEXT = 'submitButtonText'; + +const F_CLASS_PILL = 'classPill'; +const F_CLASS_BODY = 'classBody'; // FORM_ELEMENT_STATI const FE_MODE_SHOW = 'show'; @@ -382,18 +465,60 @@ const FE_SUBRECORD_ROW_CLASS = '_rowClass'; const FE_SUBRECORD_ROW_TITLE = '_rowTitle'; // FormElement columns: real +const FE_NAME = 'name'; const FE_TYPE = 'type'; const FE_MODE = 'mode'; +const FE_MODE_SQL = 'modeSql'; +// TODO: Konstante FE_DYNAMIC_UPDATE ueberall einsetzen +const FE_DYNAMIC_UPDATE = 'dynamicUpdate'; +const FE_VALUE = 'value'; // 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. +const FE_FILE_DESTINATION = 'fileDestination'; // Target pathFilename for an uploaded file. +const FE_SQL_VALIDATE = 'sqlValidate'; // Action: Query to validate form load +const FE_EXPECT_RECORDS = 'expectRecords'; // Action: expected number of rows of FE_SQL_VALIDATE +const FE_MESSAGE_FAIL = 'messageFail'; // Action: Message to display, if FE_SQL_VALIDATE fails. +const FE_REQUIRED_LIST = 'requiredList'; // Optional list of FormElements which have to be non empty to make this 'action'-FormElement active. +const FE_SLAVE_ID = 'slaveId'; // Action; Value or Query to compute id of slave record. +const FE_SQL_UPDATE = 'sqlUpdate'; // Action: Update Statement for slave record +const FE_SQL_INSERT = 'sqlInsert'; // Action: Insert Statement to create slave record. +const FE_SQL_DELETE = 'sqlDelete'; // Action: Delete Statement to delete unused slave record. +const FE_EDITOR_PREFIX = 'editor-'; // TinyMCE configuration settings. +const FE_SENDMAIL_TO = 'sendMailTo'; // Receiver email adresses. Separate multiple by comma. +const FE_SENDMAIL_CC = 'sendMailCc'; // CC Receiver email adresses. Separate multiple by comma. +const FE_SENDMAIL_BCC = 'sendMailBcc'; // BCC Receiver email adresses. Separate multiple by comma. +const FE_SENDMAIL_FROM = 'sendMailFrom'; // Sender email address. +const FE_SENDMAIL_SUBJECT = 'sendMailSubject'; // Email subject +const FE_SENDMAIL_REPLY_TO = 'sendMailReplyTo'; // Reply to email address +const FE_SENDMAIL_FLAG_AUTO_SUBMIT = 'sendMailFlagAutoSubmit'; // on|off - if 'on', suppresses OoO answers from receivers. +const FE_SENDMAIL_GR_ID = 'sendMailGrId'; // gr_id: used to classify mail log entries ind table mailLog +const FE_SENDMAIL_X_ID = 'sendMailXId'; // x_id: used to classify mail log entries ind table mailLog + + +// FormElement Types +const FE_TYPE_EXTRA = 'extra'; +const FE_TYPE_SENDMAIL = 'sendMail'; +const FE_TYPE_BEFORE_LOAD = 'beforeLoad'; +const FE_TYPE_BEFORE_SAVE = 'beforeSave'; +const FE_TYPE_BEFORE_INSERT = 'beforeInsert'; +const FE_TYPE_BEFORE_UPDATE = 'beforeUpdate'; +const FE_TYPE_BEFORE_DELETE = 'beforeDelete'; +const FE_TYPE_AFTER_LOAD = 'afterLoad'; +const FE_TYPE_AFTER_SAVE = 'afterSave'; +const FE_TYPE_AFTER_INSERT = 'afterInsert'; +const FE_TYPE_AFTER_UPDATE = 'afterUpdate'; +const FE_TYPE_AFTER_DELETE = 'afterDelete'; + +const ACTION_KEYWORD_SLAVE_ID = 'slaveId'; // SUPPORT const PARAM_T3_ALL = 't3 all'; const PARAM_T3_NO_ID = "t3 no id"; +const ESCAPE_WITH_BACKSLASH = 'backslash'; +const ESCAPE_WITH_HTML_QUOTE = 'htmlquote'; // AbstractBuildForm const FLAG_ALL = 'flagAll'; @@ -402,6 +527,7 @@ const FLAG_DYNAMIC_UPDATE = 'flagDynamicUpdate'; const QUERY_TYPE_SELECT = 'type: select,show,describe,explain'; const QUERY_TYPE_INSERT = 'type: insert'; const QUERY_TYPE_UPDATE = 'type: update,replace,delete'; +const QUERY_TYPE_CONTROL = 'type: set'; //Regexp //const REGEXP_DATE_INT = '^\d{4}-\d{2}-\d{2}$'; @@ -430,3 +556,49 @@ const DB_NUM_ROWS = 'numRows'; const DB_AFFECTED_ROWS = 'affectedRows'; const DB_INSERT_ID = 'insertId'; +const COLUMN_CREATED = 'created'; +const COLUMN_MODIFIED = 'modified'; + +const INDEX_PHP = 'index.php'; + +// QuickFormQuery.php +const T3DATA_BODYTEXT = 'bodytext'; +const T3DATA_UID = 'uid'; + +// Special Column to check for uploads +const COLUMN_PATH_FILE_NAME = 'pathFileName'; + +// Used to in SIP Store to handle 'delete' after upload +const EXISTING_PATH_FILE_NAME = '_existingPathFileName'; + +//SENDMAIL +const SENDMAIL_IDX_RECEIVER = 0; +const SENDMAIL_IDX_SENDER = 1; +const SENDMAIL_IDX_SUBJECT = 2; +const SENDMAIL_IDX_BODY = 3; +const SENDMAIL_IDX_REPLY_TO = 4; +const SENDMAIL_IDX_FLAG_AUTO_SUBMIT = 5; +const SENDMAIL_IDX_GR_ID = 6; +const SENDMAIL_IDX_X_ID = 7; +const SENDMAIL_IDX_RECEIVER_CC = 8; +const SENDMAIL_IDX_RECEIVER_BCC = 9; +const SENDMAIL_IDX_SRC = 10; + +//Report: Column Token +const COLUMN_PPAGE = "Page"; +const COLUMN_PPAGEC = "Pagec"; +const COLUMN_PPAGED = "Paged"; +const COLUMN_PPAGEE = "Pagee"; +const COLUMN_PPAGEH = "Pageh"; +const COLUMN_PPAGEI = "Pagei"; +const COLUMN_PPAGEN = "Pagen"; +const COLUMN_PPAGES = "Pages"; + +const COLUMN_PAGE = "page"; +const COLUMN_PAGEC = "pagec"; +const COLUMN_PAGED = "paged"; +const COLUMN_PAGEE = "pagee"; +const COLUMN_PAGEH = "pageh"; +const COLUMN_PAGEI = "pagei"; +const COLUMN_PAGEN = "pagen"; +const COLUMN_PAGES = "pages"; \ No newline at end of file diff --git a/extension/qfq/qfq/Database.php b/extension/qfq/qfq/Database.php index 9d2fb57f37daacaeebe029c20b1d69d6cc52e1f4..25c88e3a192234632736d20e949d7a2ead3d403d 100644 --- a/extension/qfq/qfq/Database.php +++ b/extension/qfq/qfq/Database.php @@ -69,6 +69,12 @@ class Database { $this->mysqli = $this->dbConnect(); } $this->sqlLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM); + + // DB Init + $dbInit = $this->store->getVar(SYSTEM_DB_INIT, STORE_SYSTEM); + if ($dbInit !== false && $dbInit != '') { + $this->sql($dbInit); + } } @@ -96,115 +102,29 @@ class Database { } /** - * Return the number of rows returned by the last call to execute(). - * - * If execute() has never been called, returns FALSE. - * - * @return mixed Number of rows returned by last call to execute(). If Database::execute() - * has never been called prior a call to this method, false is returned. - */ - public function getRowCount() { - if ($this->mysqli_result == null) { - return false; - } - - return $this->mysqli_result->num_rows; - } - - /** - * Get the values for a given ENUM or SET column - * - * @param string $table name of the table - * @param string $columnName name of the column - * - * @throws UserFormException if the table or column does not exist, or is not of type ENUM or SET - * @return array - */ - public function getEnumSetValueList($table, $columnName) { - - $columnDefinition = $this->getFieldDefinitionFromTable($table, $columnName); - $setEnumDefinition = $columnDefinition["Type"]; - - // $setEnumDefinition holds now a string like - // String: enum('','red','blue','green') - $len = mb_strlen($setEnumDefinition); - - # "enum('" = 6, "set('" = 5 - $tokenLength = strpos($setEnumDefinition, "'") + 1; - - // count("enum('") == 6, count("')") == 2 - $enumSetString = mb_substr($setEnumDefinition, $tokenLength, $len - (2 + $tokenLength)); - - // String: ','red','blue','green - - if (($setEnumValueList = explode("','", $enumSetString)) === false) { - return array(); - } - - return $setEnumValueList; - } - - /** - * Get database column definition. - * - * If the column is not found in the table, an exception is thrown. - * - * @param string $table name of the table - * - * @param string $columnName name of the column - * @return array the definition of the column as retrieved by Database::getTableDefinition(). - * - * @throws \qfq\DbException - */ - private function getFieldDefinitionFromTable($table, $columnName) { - $tableDefinition = $this->getTableDefinition($table); - foreach ($tableDefinition AS $row) { - if ($row["Field"] == $columnName) { - return $row; - } - } - throw new DbException("Column name '$columnName' not found in table '$table'.", ERROR_DB_COLUMN_NOT_FOUND_IN_TABLE); - } - - /** - * Get all column definitions for a table. Return Assoc Array: - * - * Field Type Null Key Default Extra - * -------------------------------------------------------------------------- - * id bigint(20) NO PRI NULL auto_increment - * name varchar(128) YES NULL - * firstname varchar(128) YES NULL - * gender enum('','male','female') NO male - * groups set('','a','b','c') NO a - * - * @param string $table table to retrieve column definition from - * - * @return array column definition of table as returned by SHOW FIELDS FROM as associative array. - */ - public function getTableDefinition($table) { - return $this->sql("SHOW FIELDS FROM `$table`"); - } - - /** - * Fires query $sql and fetches result als assoc array (all modes but ROW_KEYS) or as num array (mode: ROW_EKYS). Throws exception. + * Fires query $sql and fetches result as assoc array (all modes but ROW_KEYS) or as num array (mode: ROW_KEYS). Throws exception. * * $mode * ROW_REGULAR: Return 2-dimensional assoc array. Every query row is one array row. * ROW_IMPLODE_ALL: Return string. All cells of all rows imploded to one string. - * ROW_EXPECT_0: Return empty string if there is now record row, Else an exception. + * ROW_EXPECT_0: Return empty string if there is no record row, Else an exception. * ROW_EXPECT_1: Return 1-dimensional assoc array if there are exact one row. Else an exception. * ROW_EXPECT_0_1: Return empty string if there is no row. Return 1- dimensional assoc array if there is one row. Else an exception. * ROW_EXPECT_GE_1: Like 'ROW_REGULAR'. Throws an exception if there is an empty resultset. - * ROW_KEYS: Return 2-dimensional num(!) array. Every query row is one array row. In $keys are the column names. + * ROW_KEYS: Return 2-dimensional num(!) array. Every query row is one array row. $keys are the column names. * * @param $sql * @param string $mode * @param array $parameterArray * @param string $specificMessage - * @return mixed|null - * SELECT | SHOW | DESCRIBE | EXPLAIN: If no record found, empty string ( ROW_EXPECT_0_1, ROW_EXPECT_1) or empty array (all other modes) - * INSERT: last_insert_id - * UPDATE | DELETE | REPLACE: affected rows + * @param array $keys + * @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + * @return array|int + * SELECT | SHOW | DESCRIBE | EXPLAIN: + * If no record found: a) ROW_EXPECT_0_1, ROW_EXPECT_1: empty string, b) All other modes: empty array + * If record(s) found: a) ROW_EXPECT_0_1, ROW_EXPECT_1: one dimensional array, b) All other modes: 2 dimensional array + * INSERT: last_insert_id + * UPDATE | DELETE | REPLACE: affected rows * @throws \qfq\CodeException * @throws \qfq\DbException */ @@ -214,12 +134,14 @@ class Database { $this->closeMysqliStmt(); // CR often forgets to specify the $mode and use prepared statement with parameter instead. - if (is_array($mode)) + if (is_array($mode)) { throw new CodeException("Probably a parameter forgotten: \$mode ?"); + } // for error reporting in exception - if ($specificMessage) + if ($specificMessage) { $specificMessage .= " "; + } $count = $this->prepareExecute($sql, $parameterArray, $queryType, $stat); @@ -266,6 +188,8 @@ class Database { default: throw new DbException($specificMessage . "Unknown mode: $mode", ERROR_UNKNOWN_MODE); } + } elseif ($queryType === QUERY_TYPE_INSERT) { + $result = $stat[DB_INSERT_ID]; } else { $result = $count; } @@ -303,33 +227,38 @@ class Database { * @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 + * @param array $stat DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS * @return int|mixed * @throws \qfq\CodeException * @throws \qfq\DbException * @throws \qfq\UserFormException */ private function prepareExecute($sql, array $parameterArray = array(), &$queryType, array &$stat) { + + $sqlLogMode = $this->isSqlModify($sql) ? SQL_LOG_MODE_MODIFY : SQL_LOG_MODE_ALL;; $result = 0; $stat = array(); + $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); $this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM); // Logfile - $this->dbLog($sql, $parameterArray); + $this->dbLog($sqlLogMode, $sql, $parameterArray); if (false === ($this->mysqli_stmt = $this->mysqli->prepare($sql))) { + $this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray); throw new DbException('[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error, ERROR_DB_PREPARE); } - if (count($parameterArray) > 0) { if (false === $this->prepareBindParam($parameterArray)) { + $this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray); throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_BIND); } } if (false === $this->mysqli_stmt->execute()) { + $this->dbLog(SQL_LOG_MODE_ERROR, $sql, $parameterArray); throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_EXECUTE); } @@ -344,27 +273,36 @@ class Database { if (false === ($result = $this->mysqli_stmt->get_result())) { throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_EXECUTE); } - $queryType = QUERY_TYPE_SELECT; + $queryType = QUERY_TYPE_SELECT; $this->mysqli_result = $result; - $stat[DB_NUM_ROWS] = $this->mysqli_result->num_rows; - $count = $stat[DB_NUM_ROWS]; - $msg = 'Read rows: ' . $stat[DB_NUM_ROWS]; + $stat[DB_NUM_ROWS] = $this->mysqli_result->num_rows; + $count = $stat[DB_NUM_ROWS]; + $msg = 'Read rows: ' . $stat[DB_NUM_ROWS]; break; + case 'REPLACE': case 'INSERT': $queryType = QUERY_TYPE_INSERT; $stat[DB_INSERT_ID] = $this->mysqli->insert_id; $stat[DB_AFFECTED_ROWS] = $this->mysqli->affected_rows; $count = $stat[DB_AFFECTED_ROWS]; - $msg = 'ID: ' . $count; + $msg = 'ID: ' . $this->mysqli->insert_id; break; case 'UPDATE': - case 'REPLACE': case 'DELETE': - $queryType = QUERY_TYPE_UPDATE; - $stat[DB_AFFECTED_ROWS] = $this->mysqli->affected_rows; - $count = $stat[DB_AFFECTED_ROWS]; + case 'TRUNCATE': + $queryType = QUERY_TYPE_UPDATE; + $stat[DB_AFFECTED_ROWS] = $this->mysqli->affected_rows; + $count = $stat[DB_AFFECTED_ROWS]; $msg = 'Affected rows: ' . $count; break; + + case 'SET': + $queryType = QUERY_TYPE_CONTROL; + $stat[DB_AFFECTED_ROWS] = 0; + $count = $stat[DB_AFFECTED_ROWS]; + $msg = ''; + break; + default: throw new DbException('Unknown comand: "' . $command . '"', ERROR_DB_UNKNOWN_COMMAND); break; @@ -373,11 +311,30 @@ class Database { $this->store->setVar(SYSTEM_SQL_COUNT, $count, STORE_SYSTEM); // Logfile - $this->dbLog($msg); + $this->dbLog($sqlLogMode, $msg); return $count; } + /** + * Check if the given SQL Statement might modify data. + * + * @param $sql + * @return bool true is the statement might modify data, else: false + */ + private function isSqlModify($sql) { + $command = explode(' ', $sql, 2); + switch (strtoupper($command[0])) { + case 'INSERT': + case 'UPDATE': + case 'DELETE': + case 'REPLACE': + case 'TRUNCATE': + return true; + } + return false; + } + /** * Decide if the SQL statement has to be logged. If yes, create a timestamp and do the log. * @@ -386,67 +343,79 @@ class Database { * @return string * @throws \qfq\UserFormException */ - private function dbLog($sql, $parameterArray = array()) { + private function dbLog($mode = SQL_LOG_MODE_ALL, $sql = '', $parameterArray = array()) { + + $status = ''; + + $sqlLogMode = $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM); - $mode = $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM); switch ($mode) { case SQL_LOG_MODE_ALL: + if ($sqlLogMode != SQL_LOG_MODE_ALL) { + return; + } break; case SQL_LOG_MODE_MODIFY: - if ($this->isSqlModify($sql)) { - break; - } - // nothing to log. - return; + break; + + case SQL_LOG_MODE_ERROR: + break; + default: throw new UserFormException("Unknown SQL_LOG_MODE: $mode", ERROR_UNKNOWN_SQL_LOG_MODE); } - $msg = '[' . date('Y.m.d H:i:s O') . ']['; + // Client IP Address + $remoteAddress = $this->store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT); - if (count($parameterArray) === 0) { - $msg .= $sql; - } else { + $msg = '[' . date('Y.m.d H:i:s O') . '][' . $remoteAddress . ']'; - $sqlArray = explode('?', $sql); - $ii = 0; - foreach ($parameterArray as $value) { - if (isset($sqlArray[$ii])) { - if (is_array($value)) { - $value = OnArray::toString($value); - } - - $msg .= $sqlArray[$ii++] . "'" . $value . "'"; - } else { - $msg = '?'; - } - } - if (isset($sqlArray[$ii])) - $msg .= $sqlArray[$ii]; +// // FE User +// $feUser = $this->sqlLog = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3); +// $pageId = $this->sqlLog = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); +// $ttcontentId = $this->sqlLog = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3); + + if (count($parameterArray) > 0) { + $sql = $this->preparedStatementInsertParameter($sql, $parameterArray); } - $msg .= ']'; + if ($sql !== '') { + if ($mode == SQL_LOG_MODE_ERROR) { + $status = 'FAILED: '; + } + $msg .= '[' . $status . $sql . ']'; + } Logger::logMessage($msg, $this->sqlLog); } /** - * Check if the given SQL Statement might modify data. - * * @param $sql - * @return bool true is the statement might modify data, else: false + * @param $parameterArray + * @return string */ - private function isSqlModify($sql) { - $command = explode(' ', $sql, 2); - switch (strtoupper($command[0])) { - case 'INSERT': - case 'UPDATE': - case 'DELETE': - case 'REPLACE': - return true; + private function preparedStatementInsertParameter($sql, $parameterArray) { + $msg = ''; + + $sqlArray = explode('?', $sql); + $ii = 0; + foreach ($parameterArray as $value) { + if (isset($sqlArray[$ii])) { + if (is_array($value)) { + $value = OnArray::toString($value); + } + + $msg .= $sqlArray[$ii++] . "'" . $value . "'"; + } else { + $msg = '?'; + } } - return false; + if (isset($sqlArray[$ii])) { + $msg .= $sqlArray[$ii]; + } + + return $msg; } /** @@ -510,6 +479,96 @@ class Database { } } + /** + * Return the number of rows returned by the last call to execute(). + * + * If execute() has never been called, returns FALSE. + * + * @return mixed Number of rows returned by last call to execute(). If Database::execute() + * has never been called prior a call to this method, false is returned. + */ + public function getRowCount() { + if ($this->mysqli_result == null) { + return false; + } + + return $this->mysqli_result->num_rows; + } + + /** + * Get the values for a given ENUM or SET column + * + * @param string $table name of the table + * @param string $columnName name of the column + * + * @throws UserFormException if the table or column does not exist, or is not of type ENUM or SET + * @return array + */ + public function getEnumSetValueList($table, $columnName) { + + $columnDefinition = $this->getFieldDefinitionFromTable($table, $columnName); + $setEnumDefinition = $columnDefinition["Type"]; + + // $setEnumDefinition holds now a string like + // String: enum('','red','blue','green') + $len = mb_strlen($setEnumDefinition); + + # "enum('" = 6, "set('" = 5 + $tokenLength = strpos($setEnumDefinition, "'") + 1; + + // count("enum('") == 6, count("')") == 2 + $enumSetString = mb_substr($setEnumDefinition, $tokenLength, $len - (2 + $tokenLength)); + + // String: ','red','blue','green + + if (($setEnumValueList = explode("','", $enumSetString)) === false) { + return array(); + } + + return $setEnumValueList; + } + + /** + * Get database column definition. + * + * If the column is not found in the table, an exception is thrown. + * + * @param string $table name of the table + * + * @param string $columnName name of the column + * @return array the definition of the column as retrieved by Database::getTableDefinition(). + * + * @throws \qfq\DbException + */ + private function getFieldDefinitionFromTable($table, $columnName) { + $tableDefinition = $this->getTableDefinition($table); + foreach ($tableDefinition AS $row) { + if ($row["Field"] == $columnName) { + return $row; + } + } + throw new DbException("Column name '$columnName' not found in table '$table'.", ERROR_DB_COLUMN_NOT_FOUND_IN_TABLE); + } + + /** + * Get all column definitions for a table. Return Assoc Array: + * + * Field Type Null Key Default Extra + * -------------------------------------------------------------------------- + * id bigint(20) NO PRI NULL auto_increment + * name varchar(128) YES NULL + * firstname varchar(128) YES NULL + * gender enum('','male','female') NO male + * groups set('','a','b','c') NO a + * + * @param string $table table to retrieve column definition from + * + * @return array column definition of table as returned by SHOW FIELDS FROM as associative array. + */ + public function getTableDefinition($table) { + return $this->sql("SHOW FIELDS FROM `$table`"); + } + /** * Wrapper for sql(), to simplyfy access. * @@ -535,4 +594,27 @@ class Database { return $this->mysqli->insert_id; } + /** + * Searches for the table '$name'. + * + * @param $name + * @return bool true if found, else false + */ + public function existTable($name) { + $found = false; + + $tables = $this->sql("SHOW tables"); + + foreach ($tables as $t) { + foreach ($t as $key => $value) { + if ($value === $name) { + $found = true; + break 2; + } + } + } + + return $found; + } + } \ No newline at end of file diff --git a/extension/qfq/qfq/Delete.php b/extension/qfq/qfq/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..edbfded443eb0b6baf258585c47e37980fce3b9e --- /dev/null +++ b/extension/qfq/qfq/Delete.php @@ -0,0 +1,113 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 6/1/16 + * Time: 8:52 AM + */ + +namespace qfq; + +require_once(__DIR__ . '/Constants.php'); +require_once(__DIR__ . '/Database.php'); +require_once(__DIR__ . '/store/Store.php'); + + +class Delete { + /** + * @var Database + */ + private $db = null; + + /** + * @var Store + */ + private $store = null; + + /** + * + */ + public function __construct($phpUnit = false) { + $this->db = new Database(); + $this->store = Store::getInstance('', $phpUnit); + } + + /** + * Deletes the record id=$recordId from table $form[F_TABLE_NAME]. + * If the table has a column named COLUMN_PATH_FILE_NAME and the value of that specific record column points + * to a file: delete such a file if their are no other records in the same table which also have a reference to that file. + * + * @param string $tableName + * @param integer $recordId + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + public function process($tableName, $recordId) { + $msg = array(); + + if ($tableName === false || $tableName === '') { + throw new CodeException('Missing table name', ERROR_MISSING_TABLE_NAME); + } + + if ($recordId === 0 || $recordId === '') { + throw new CodeException('Invalid record id', ERROR_MISSING_RECORD_ID); + } + + // 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); + } + + // Read record first. + $row = $this->db->sql("SELECT * FROM $tableName WHERE id=?", ROW_EXPECT_0_1, [$recordId]); + if (count($row) > 0) { + + $this->deleteReferencedFiles($row, $tableName); + + $this->db->sql("DELETE FROM $tableName WHERE id =? LIMIT 1", ROW_REGULAR, [$recordId]); + } else { + throw new UserFormException("Record $recordId not found in table '$tableName'.", ERROR_RECORD_NOT_FOUND); + } + + chdir($cwd); + } + + /** + * Iterates over array $row and searches for column names with substring COLUMN_PATH_FILE_NAME. + * For any found, check if it references a writeable file. + * If yes: check if there are other records (same table, same column) which references the same file. + * If no: delete the file + * If yes: do nothing, continue with the next column. + * If no: do nothing, continue with the next column. + * + * @param array $row + * @param $tableName + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + private function deleteReferencedFiles(array $row, $tableName) { + + foreach ($row AS $key => $file) { + if (false === strpos($key, COLUMN_PATH_FILE_NAME)) { + continue; + } + + // check if there is a file referenced in the record which have to be deleted too. + if ($file !== '' && is_writable($file)) { + + // check if there are other records referencing the same file: do not delete the file now. + // This check won't find duplicates, if they are spread over different columns or tables. + $samePathFileName = $this->db->sql("SELECT COUNT(id) AS cnt FROM $tableName WHERE $key LIKE ?", ROW_EXPECT_1, [$file]); + if ($samePathFileName['cnt'] === 1) { + if (!unlink($file)) { + throw new UserFormException("Error deleting file: $file", ERROR_IO_UNLINK); + } + } + } + } + } +} \ No newline at end of file diff --git a/extension/qfq/qfq/Evaluate.php b/extension/qfq/qfq/Evaluate.php index 3d815149acb93376d65be14b2c9e9c3e41ee278f..0f9296223450c5facf9c5500a8ab7190f5746aa6 100644 --- a/extension/qfq/qfq/Evaluate.php +++ b/extension/qfq/qfq/Evaluate.php @@ -30,6 +30,12 @@ class Evaluate { // private $debugStack = array(); + /** + * @param \qfq\Store $store + * @param Database $db + * @param string $startDelimiter + * @param string $endDelimiter + */ public function __construct(Store $store, Database $db, $startDelimiter = '{{', $endDelimiter = '}}') { $this->store = $store; $this->db = $db; @@ -102,9 +108,11 @@ class Evaluate { $debugLocal[] = $debugIndent . "REPLACE: $match"; if ($foundInStore === '') { - // Encode the non replaceable part as preparation not to process again and to recode at the end. + + // Encode the non replaceable part as preparation not to process again. Recode them at the end. $evaluated = Support::encryptDoubleCurlyBraces($this->startDelimiter . $match . $this->endDelimiter); $debugLocal[] = $debugIndent . "BY: <nothing found - not replaced>"; + } else { $flagTokenReplaced = true; @@ -139,15 +147,16 @@ class Evaluate { /** * Tries to substitute $token. - * Token might be + * Token might be: * a) a SQL statement to fire - * b) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:ALNUMX' + * b) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:alnumx', 'form:F:all:s' + * * The token have to be _without_ Delimiter '{{' / '}}' - * If neither a) or b) match, return the token itself, surrounded by single ticks, to emphase that substition failed. + * If neither a) or b) match, return the token itself. * * @param $token * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found, return ''. - * @return array|mixed|null|string + * @return array|null|string * @throws CodeException * @throws DbException */ @@ -167,21 +176,31 @@ class Evaluate { // SQL Statement? if (in_array(strtoupper($arr[0] . ' '), $this->sqlKeywords)) { - $foundInStore = 'query'; + $foundInStore = TOKEN_FOUND_IN_STORE_QUERY; return $this->db->sql($token, $sqlMode); } - // explode for: <key>:<store priority>:<sanitize class> - $arr = explode(':', $token, 3); - if (!isset($arr[1])) - $arr[1] = null; - if (!isset($arr[2])) - $arr[2] = null; - + // explode for: <key>:<store priority>:<sanitize class>:<escape> + $arr = explode(':', $token, 4); + $arr = array_merge($arr, [null, null, null, null]); // fake isset() // search for value in stores $value = $this->store->getVar($arr[0], $arr[1], $arr[2], $foundInStore); + // escape ticks + if (is_string($value)) { + switch ($arr[3]) { + case TOKEN_ESCAPE_SINGLE_TICK: + $value = str_replace("'", "\\'", $value); + break; + case TOKEN_ESCAPE_DOUBLE_TICK: + $value = str_replace('"', '\\"', $value); + break; + default: + break; + } + } + // OLD: nothing replaced: put ticks around, to sanitize strings for SQL statements. Nothing to substitute is not a wished situation. // return ($value === false) ? "'" . $token . "'" : $value; @@ -189,7 +208,10 @@ class Evaluate { return $value; } - public function getDebug() { - return '<pre>' . implode("\n", $this->debugStack) . '</pre>'; - } + /** + * @return string + */ +// public function getDebug() { +// return '<pre>' . implode("\n", $this->debugStack) . '</pre>'; +// } } \ No newline at end of file diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index b25508d5f6928937d143e11cc4bd13ea3acef175..c21158561fe4e0cc2c7504aa3671a25e8b4b61e0 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -23,24 +23,26 @@ use qfq; //use qfq\Store; -require_once(__DIR__ . '/../qfq/store/Store.php'); -require_once(__DIR__ . '/../qfq/store/FillStoreForm.php'); -require_once(__DIR__ . '/../qfq/store/Session.php'); -require_once(__DIR__ . '/../qfq/Constants.php'); -require_once(__DIR__ . '/../qfq/Save.php'); -require_once(__DIR__ . '/../qfq/helper/KeyValueStringParser.php'); -require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php'); -require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php'); -require_once(__DIR__ . '/../qfq/exceptions/CodeException.php'); -require_once(__DIR__ . '/../qfq/exceptions/DbException.php'); -require_once(__DIR__ . '/../qfq/exceptions/ErrorHandler.php'); -require_once(__DIR__ . '/../qfq/Database.php'); -require_once(__DIR__ . '/../qfq/Evaluate.php'); -require_once(__DIR__ . '/../qfq/BuildFormPlain.php'); -require_once(__DIR__ . '/../qfq/BuildFormTable.php'); -require_once(__DIR__ . '/../qfq/BuildFormBootstrap.php'); -require_once(__DIR__ . '/../qfq/report/Report.php'); -require_once(__DIR__ . '/../qfq/BodytextParser.php'); +require_once(__DIR__ . '/store/Store.php'); +require_once(__DIR__ . '/store/FillStoreForm.php'); +require_once(__DIR__ . '/store/Session.php'); +require_once(__DIR__ . '/Constants.php'); +require_once(__DIR__ . '/Save.php'); +require_once(__DIR__ . '/helper/KeyValueStringParser.php'); +require_once(__DIR__ . '/helper/HelperFormElement.php'); +require_once(__DIR__ . '/exceptions/UserFormException.php'); +require_once(__DIR__ . '/exceptions/CodeException.php'); +require_once(__DIR__ . '/exceptions/DbException.php'); +require_once(__DIR__ . '/exceptions/ErrorHandler.php'); +require_once(__DIR__ . '/Database.php'); +require_once(__DIR__ . '/Evaluate.php'); +require_once(__DIR__ . '/BuildFormPlain.php'); +require_once(__DIR__ . '/BuildFormTable.php'); +require_once(__DIR__ . '/BuildFormBootstrap.php'); +require_once(__DIR__ . '/report/Report.php'); +require_once(__DIR__ . '/BodytextParser.php'); +require_once(__DIR__ . '/Delete.php'); +require_once(__DIR__ . '/form/FormAction.php'); /* * Form will be called @@ -119,6 +121,11 @@ class QuickFormQuery { mb_internal_encoding("UTF-8"); +// session_name(SESSION_NAME); +// session_start(); + + $this->session = Session::getInstance($phpUnit); + // session.cache_expire // session.cookie_lifetime // session.gc_maxlifetime @@ -134,27 +141,29 @@ class QuickFormQuery { set_error_handler("\\qfq\\ErrorHandler::exception_error_handler"); - if (!isset($t3data['bodytext'])) - $t3data['bodytext'] = ''; - if (!isset($t3data['uid'])) - $t3data['uid'] = 0; + if (!isset($t3data[T3DATA_BODYTEXT])) { + $t3data[T3DATA_BODYTEXT] = ''; + } + + if (!isset($t3data[T3DATA_UID])) { + $t3data[T3DATA_UID] = 0; + } $btp = new BodytextParser(); - $t3data['bodytext'] = $btp->process($t3data['bodytext']); + $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]); $this->t3data = $t3data; - $bodytext = $this->t3data['bodytext']; + $bodytext = $this->t3data[T3DATA_BODYTEXT]; - $this->session = Session::getInstance($phpUnit); $this->store = Store::getInstance($bodytext, $phpUnit); - $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data['uid'], STORE_TYPO3); + $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3); $this->db = new Database(); $this->eval = new Evaluate($this->store, $this->db); } /** - * Returns the defined forwardMode and set, if necessary, $forwardPage + * Returns the defined forwardMode and set $forwardPage (call be reference) * * @param $forwardPage * @return mixed @@ -165,7 +174,7 @@ class QuickFormQuery { } /** - * Main entrypoint for display content: form or report + * Main entrypoint for display content: a) form and/or b) report * * @return string */ @@ -183,82 +192,143 @@ class QuickFormQuery { if ($class) $html = Support::wrapTag("<div class='$class'>", $html); +// $feUidLoggedIn = isset($GLOBALS["TSFE"]->fe_user->user["uid"]) ? $GLOBALS["TSFE"]->fe_user->user["uid"] : false; +// $feUidSession = $_SESSION[SESSION_NAME][SESSION_FE_USER_UID]; +// $html .= "<p>feUidLoggedIn: $feUidLoggedIn / feUidSession: $feUidSession</p>"; + return $html; } /** * Process form. - * $mode=FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements - * $mode=FORM_UPDATE: States and values of all form elements will be returned as JSON. - * $mode=FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON. + * $mode= + * FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements + * FORM_UPDATE: States and values of all form elements will be returned as JSON. + * FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON. + * FORM_DELETE: * - * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE + * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE * @return array|string * @throws CodeException * @throws UserFormException */ - private function doForm($mode) { + private function doForm($formMode) { $data = ''; $foundInStore = ''; // Fill STORE_FORM - if ($mode === FORM_UPDATE || $mode === FORM_SAVE) { + if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) { $fillStoreForm = new FillStoreForm(); $fillStoreForm->process(); } - $formName = $this->loadFormSpecification($mode, $foundInStore); - if ($formName === false) + $formName = $this->loadFormSpecification($formMode, $foundInStore); + if ($formName === false && $formMode !== FORM_DELETE) { + // No form found: do nothing return ''; + } - $sipFound = $this->validateForm($foundInStore); - if (!$sipFound) { + if ($formName !== false) { + // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename). + $sipFound = $this->validateForm($foundInStore, $formMode); + + } else { + // FORM_DELETE without a form definition: Fake the form with only a tableName. + $table = $this->store->getVar(SIP_TABLE, STORE_SIP); + if ($table === false) { + throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE); + } + $sipFound = true; + $this->formSpec[F_NAME] = ''; + $this->formSpec[F_TABLE_NAME] = $table; + } + + $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT); + // For 'new' record always create a new TAB-uniq (for this current form, nowhere else used) SIP. + // With such a TAB-uniq SIP, multiple TABs and following repeated NEWs are easily implemented. + if (!$sipFound || ($formMode == FORM_LOAD && $recordId == 0)) { $this->store->createSipAfterFormLoad($formName); } - $this->store->fillStoreTableDefaultColumnType($this->formSpec['tableName']); - switch ($this->formSpec['render']) { - case 'plain': - $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative); - break; - case 'table': - $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative); - break; - case 'bootstrap': - $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative); - break; - default: - throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); + if ($formMode === FORM_DELETE) { + + $build = new Delete(); + + } else { + $this->store->fillStoreTableDefaultColumnType($this->formSpec[F_TABLE_NAME]); + + switch ($this->formSpec['render']) { + case 'plain': + $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative); + break; + case 'table': + $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative); + break; + case 'bootstrap': + $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative); + break; + default: + throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); + } } - switch ($mode) { + $formAction = new FormAction($this->formSpec, $this->db, $this->phpUnit); + switch ($formMode) { case FORM_LOAD: case FORM_UPDATE: + $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); - $data = $build->process($mode); + $data = $build->process($formMode); + + $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD); + break; + + case FORM_DELETE: + $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE); + + $build->process($this->formSpec[F_TABLE_NAME], $recordId); + + $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_DELETE); break; case FORM_SAVE: + $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); + + // Action: Before + $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_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)); + $this->fillStoreRecord($this->formSpec[F_TABLE_NAME], $recordId); $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative); $rc = $save->process(); - // Reload fresh saved record and fill STORE_RECORD with it - $this->fillStoreRecord($this->formSpec['tableName'], $rc); + // Reload fresh saved record and fill STORE_RECORD with it. + $this->fillStoreRecord($this->formSpec[F_TABLE_NAME], $rc); + + $save->processAllUploads($rc); + + // Action: After + $modified = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE); + if ($modified) { + // Reload fresh saved record and fill STORE_RECORD with it. + $this->fillStoreRecord($this->formSpec[F_TABLE_NAME], $rc); + } $htmlElementNameIdZero = false; // Retrieve current STORE_SIP. $sipArray = $this->store->getStore(STORE_SIP); if ($sipArray[SIP_RECORD_ID] == 0) { - // After insert: a new SIP for the new record id is required - $this->newRecordCreateSip($sipArray, $rc); + // After insert: a new SIP for the new record id is required. + $this->newRecordUpdateSip($rc); $htmlElementNameIdZero = true; } + // Action: Sendmail + $formAction->elements($rc, $this->feSpecAction, FE_TYPE_SENDMAIL); + // Retrieve FE Values as JSON - $data = $build->process($mode, $htmlElementNameIdZero); + $data = $build->process($formMode, $htmlElementNameIdZero); break; default: @@ -290,6 +360,10 @@ class QuickFormQuery { return false; } + if (!$this->db->existTable('Form')) { + throw new UserFormException("Table 'Form' not found", ERROR_MISSING_TABLE); + } + // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM); @@ -301,24 +375,43 @@ class QuickFormQuery { } // Load form - $form = $this->db->sql("SELECT * FROM Form AS f WHERE f.name LIKE ? AND f.deleted='no'", ROW_EXPECT_1, + $form = $this->db->sql("SELECT * FROM Form AS f WHERE f." . F_NAME . " LIKE ? AND f.deleted='no'", ROW_EXPECT_1, [$formName], 'Form not found or multiple forms with the same name.'); + $form = $this->modeAdjustFormConfig($mode, $form); + $this->formSpec = $this->eval->parseArray($form); HelperFormElement::explodeParameter($this->formSpec); # Set defaults: 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, ''); + Support::setIfNotSet($this->formSpec, F_BS_INPUT_COLUMNS, 6, ''); + Support::setIfNotSet($this->formSpec, F_BS_NOTE_COLUMNS, 3, ''); + + Support::setIfNotSet($this->formSpec, F_SUBMIT_BUTTON_TEXT, ''); + Support::setIfNotSet($this->formSpec, F_EXTRA_DELETE_FORM, ''); + + // Set F_FINAL_DELETE_FORM + $this->formSpec[F_FINAL_DELETE_FORM] = $this->formSpec[F_EXTRA_DELETE_FORM] != '' ? $this->formSpec[F_EXTRA_DELETE_FORM] : $this->formSpec[F_NAME]; + + // Take default from config.ini + $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_FORM_PILL, STORE_SYSTEM); + $class = $class ? $class : 'qfq-color-grey-1'; + Support::setIfNotSet($this->formSpec, F_CLASS_PILL, $class); + + // Take default from config.ini + $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_FORM_BODY, STORE_SYSTEM); + $class = $class ? $class : 'qfq-color-grey-2'; + Support::setIfNotSet($this->formSpec, F_CLASS_BODY, $class); // Clear $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM); // FE: Action - $this->feSpecAction = $this->eval->parseArray($this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, - ['no', $this->formSpec["id"], 'action'])); +// $this->feSpecAction = $this->eval->parseArray($this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, +// ['no', $this->formSpec["id"], 'action'])); + $this->feSpecAction = $this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']); HelperFormElement::explodeParameterInArrayElements($this->feSpecAction); // FE: Native & Container @@ -335,7 +428,12 @@ class QuickFormQuery { ['no', $this->formSpec["id"], 'native']); break; + case FORM_DELETE: + $this->feSpecNative = array(); + break; + default: + break; } HelperFormElement::explodeParameterInArrayElements($this->feSpecNative); @@ -360,11 +458,11 @@ class QuickFormQuery { * * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE * @param string $foundInStore - * @return array|bool|mixed|null|string Formname (Form.name) or FALSE, if no formname found. + * @return bool|string Formname (Form.name) or FALSE (if no formname found) * @throws CodeException * @throws UserFormException */ - private function getFormName($mode, &$foundInStore = '') { + public function getFormName($mode, &$foundInStore = '') { $dummy = array(); switch ($mode) { @@ -373,7 +471,8 @@ class QuickFormQuery { break; case FORM_SAVE: case FORM_UPDATE: - $store = STORE_SIP; + case FORM_DELETE: + $store = STORE_SIP; break; default: throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE); @@ -382,8 +481,12 @@ class QuickFormQuery { $storeFormName = $this->store->getVar(SIP_FORM, $store, '', $foundInStore); $formName = $this->eval->parse($storeFormName, 0, $dummy, $foundInStore); +// if($mode===FORM_DELETE && $formName===false) { +// return ""; +// } + // If the formname is '': no formname name. - if ($formName === '') + if ($formName === '' || $foundInStore === '') return false; // If the formname is surrounded by single ticks: the token (typically 'form') has not been replaced by a value. @@ -394,6 +497,27 @@ class QuickFormQuery { return $formName; } + /** + * Depending on $mode various formSpec fields might be adjusted. + * E.g.: the form title is not important during a delete. + * + * @param $mode + * @param array $form + * @return array + */ + private function modeAdjustFormConfig($mode, array $form) { + + switch ($mode) { + case FORM_DELETE: + $form[F_TITLE] = ''; + break; + default: + break; + } + + return $form; + } + /** * Check if loading of the given form is permitted. If not, throw an exception. * @@ -403,11 +527,18 @@ class QuickFormQuery { * @throws UserFormException * @internal param $foundInStore */ - private function validateForm($formNameFoundInStore) { + private function validateForm($formNameFoundInStore, $formMode) { // Retrieve record_id either from SIP (prefered) or via URL $r = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT, '', $recordIdFoundInStore); + // Set missing 'r'. + if ($r === false) { + $r = 0; + $this->store->setVar(TYPO3_RECORD_ID, $r, STORE_TYPO3); + $recordIdFoundInStore = STORE_TYPO3; + } + // If there is a record_id>0: EDIT else NEW: 'sip','logged_in','logged_out','always','never' $permitMode = ($r > 0) ? $this->formSpec['permitEdit'] : $this->formSpec['permitNew']; @@ -450,10 +581,31 @@ class QuickFormQuery { throw new UserFormException("MultiMode selected, but MultiSQL missing", ERROR_MULTI_SQL_MISSING); } + if ($formMode !== FORM_DELETE) { + $sipArray = $this->store->getStore(STORE_SIP); + // Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId' + $param = explode(',', $this->formSpec[F_REQUIRED_PARAMETER]); + foreach ($param AS $name) { + + $name = explode('#', $name, 2); + $name = trim($name[0]); + + if ($name === '') { + continue; + } + + if (!isset($sipArray[$name])) { + throw new UserFormException("Missing required SIP parameter: $name", ERROR_MISSING_REQUIRED_PARAMETER); + } + } + } + return $sipFound; } /** + * Load record $id from $table. + * * @param string $table * @param string $recordId * @throws CodeException @@ -465,44 +617,29 @@ class QuickFormQuery { $record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]); $this->store->setVarArray($record, STORE_RECORD, true); } - } /** - * @param $sipArray + * Update current SIP Store with new $recordId and update SESSION store. + * * @param $recordId + * @throws CodeException + * @throws UserFormException */ - private function newRecordCreateSip($sipArray, $recordId) { - - $tmpParam = array(); + private function newRecordUpdateSip($recordId) { + // Update current SIP store with new RecordID + $sipArray = $this->store->getStore(STORE_SIP); - foreach ($sipArray as $key => $value) { - switch ($key) { - case SIP_SIP: - case SIP_URLPARAM: - case SIP_TABLE: - continue; - - case SIP_RECORD_ID: - $tmpParam[SIP_RECORD_ID] = $recordId; - break; - default: - // further vars stored in old SIP (form, maybe default values) - $tmpParam[$key] = $value; - break; - } + if (isset($sipArray[SIP_RECORD_ID]) && $sipArray[SIP_RECORD_ID] > 0) { + throw new CodeException('Attemp to overwrite existing record id: SIP(otf)=' . $sipArray[SIP_SIP] . " existing_r=" . $sipArray[SIP_RECORD_ID] . " new_r=" . $recordId, ERROR_OVERWRITE_RECORD_ID); } - // Construct fake urlparam - $tmpUrlparam = OnArray::toString($tmpParam); + $sipArray[SIP_RECORD_ID] = $recordId; + $this->store->setVarArray($sipArray, STORE_SIP, true); - // Create a SIP which has never been passed by URL - further processing might expect this to exist. - $sip = store::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP); - $this->store->setVar(CLIENT_SIP, $sip, STORE_CLIENT); + // Update SIP urlparam + store::getSipInstance()->updateSipToSession($sipArray); - // Overwrite SIP Store - $tmpParam[SIP_SIP] = $sip; - $this->store->setVarArray($tmpParam, STORE_SIP, true); } /** @@ -511,9 +648,9 @@ class QuickFormQuery { * @return string */ private function doReport() { - $report = new Report($this->t3data, $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM), $this->eval, $this->phpUnit); + $report = new Report($this->t3data, $this->eval, $this->phpUnit); - $html = $report->process(); + $html = $report->process($this->t3data['bodytext']); return $html; @@ -547,23 +684,54 @@ class QuickFormQuery { /** * Delete a record (tablename and recordid are given) or process a 'delete form' * + * @return bool * @throws CodeException - * @throws DbException - * @throws UserFormException */ public function delete() { - #TODO: implement 'delete form' + $this->doForm(FORM_DELETE); + + return true; + } - // simple delete: table and recordId are given - $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); - $table = $this->store->getVar(SIP_TABLE, STORE_SIP); + /** + * Based on the given SIP, create a new uniqe SIP by copying the relevant old params and taking the new recordId.. + * + * @param $sipArray + * @param $recordId + */ + private function newRecordCreateSip($sipArray, $recordId) { + + $tmpParam = array(); + + foreach ($sipArray as $key => $value) { + switch ($key) { + case SIP_SIP: + case SIP_URLPARAM: + case SIP_TABLE: + continue; // do not copy these params to the new SIP - if ($recordId === false || $recordId < 1 || $table === false || $table === '') { - throw new UserFormException("Invalid or missing parameter: recordId=$recordId, table=$table", ERROR_INVALID_OR_MISSING_PARAMETER); + case SIP_RECORD_ID: + // set the new recordId + $tmpParam[SIP_RECORD_ID] = $recordId; + break; + default: + // copy further vars stored in old SIP (form, maybe default values) + $tmpParam[$key] = $value; + break; + } } - $this->db->sql("DELETE FROM $table WHERE id = ? LIMIT 1", ROW_REGULAR, [$recordId]); + // Construct fake urlparam + $tmpUrlparam = OnArray::toString($tmpParam); + + // Create a SIP which has never been passed by URL - further processing might expect this to exist. + $sip = store::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP); + $this->store->setVar(CLIENT_SIP, $sip, STORE_CLIENT); + + // Overwrite SIP Store + $tmpParam[SIP_SIP] = $sip; + $this->store->setVarArray($tmpParam, STORE_SIP, true); } } \ No newline at end of file diff --git a/extension/qfq/qfq/Save.php b/extension/qfq/qfq/Save.php index 7e3e58c083ea1f7a1cf6879f4f95bb8da65b521c..0c3204d3e274a4144efb969f1a213c9c8bbb5548 100644 --- a/extension/qfq/qfq/Save.php +++ b/extension/qfq/qfq/Save.php @@ -9,6 +9,7 @@ namespace qfq; require_once(__DIR__ . '/../qfq/store/Store.php'); +require_once(__DIR__ . '/../qfq/store/Sip.php'); require_once(__DIR__ . '/../qfq/Constants.php'); require_once(__DIR__ . '/../qfq/Evaluate.php'); //require_once(__DIR__ . '/../qfq/exceptions/UserException.php'); @@ -77,59 +78,146 @@ class Save { * @throws UserFormException */ public function elements($recordId) { + $columnCreated = false; + $columnModified = false; $newValues = array(); $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') + if ($column === 'id') { continue; + } - // Get related formElement. - $formElement = $this->getFormElementByName($column); - if ($formElement === false) + // Skip Upload Elements: those will be processed later. + if ($this->isColumnUploadField($column)) { continue; + } - // Some modes means: do not save this column. - switch ($formElement[FE_MODE]) { - case FE_MODE_READONLY: - case FE_MODE_HIDDEN: - continue 2; // 1 for switch, 2 for continue foreach. - default: - break; + if ($column === COLUMN_CREATED) { + $columnCreated = true; } + if ($column === COLUMN_MODIFIED) { + $columnModified = true; + } - // Preparation for Log, Debug - $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); + // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement. + if (!isset($formValues[$column])) { + continue; + } + + $this->store->setVar(SYSTEM_FORM_ELEMENT, "Column: $column", STORE_SYSTEM); Support::setIfNotSet($formValues, $column); $newValues[$column] = $formValues[$column]; } + if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) { + $newValues[COLUMN_MODIFIED] = date('YmdHis'); + } + if ($recordId == 0) { - $rc = $this->insertRecord($this->formSpec['tableName'], $newValues); + if ($columnCreated && !isset($newValues[COLUMN_CREATED])) { + $newValues[COLUMN_CREATED] = date('YmdHis'); + } + $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues); + } else { - $this->updateRecord($this->formSpec['tableName'], $newValues, $recordId); + $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId); $rc = $recordId; } return $rc; } + /* + * Checks if there is a formElement with name '$feName' of type 'upload' + * + * @param $feName + * @return bool + */ + private function isColumnUploadField($feName) { + + foreach ($this->feSpecNative AS $formElement) { + if ($formElement[FE_NAME] === $feName && $formElement[FE_TYPE] == 'upload') + return true; + } + return false; + } + + /** + * Insert new record in table $this->formSpec['tableName']. + * + * @param array $values + * @return int last insert id + * @throws DbException + */ + public function insertRecord($tableName, array $values) { + + if (count($values) === 0) + return 0; // nothing to write, last insert id=0 + + $paramList = str_repeat('?, ', count($values)); + $paramList = substr($paramList, 0, strlen($paramList) - 2); + $columnList = '`' . implode('`, `', array_keys($values)) . '`'; + + $sql = 'INSERT INTO ' . $tableName . ' ( ' . $columnList . ' ) VALUES ( ' . $paramList . ' )'; + + $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); + + return $rc; + } + + /** + * @param string $tableName + * @param array $values + * @param int $recordId + * @return bool|int false if $values is empty, else affectedrows + * @throws CodeException + * @throws DbException + */ + public function updateRecord($tableName, array $values, $recordId) { + + if (count($values) === 0) + return 0; // nothing to write, 0 rows affected + + if ($recordId === 0) + throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN); + +// $paramList = str_repeat('?, ', count($values)); +// $paramList = substr($paramList, 0, strlen($paramList) - 2); + + $sql = 'UPDATE `' . $tableName . '` SET '; + + foreach ($values as $column => $value) { + + $sql .= '`' . $column . '` = ?, '; + } + + $sql = substr($sql, 0, strlen($sql) - 2) . ' WHERE id = ?'; + $values[] = $recordId; + + $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); + + 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) { + public function processAllUploads($recordId) { + + $sip = new Sip(false); + $newValues = array(); + + $formValues = $this->store->getStore(STORE_FORM); foreach ($this->feSpecNative AS $formElement) { // skip non upload formElements @@ -141,17 +229,23 @@ class Save { $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); $column = $formElement['name']; - $file = $this->doUpload($formElement, $formValues[$column]); + $file = $this->doUpload($formElement, $formValues[$column], $sip); if ($file !== false) { - $formValues[$column] = $file; + $newValues[$column] = $file; } } + + if (count($newValues) > 0) { + $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId); + } } /** * 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. * + * Check also: doc/CODING.md + * * @param $formElement * @param $sipUpload * @return string|false New filename or false on error @@ -159,8 +253,7 @@ class Save { * @throws UserFormException * @internal param $recordId */ - private function doUpload($formElement, $sipUpload) { - + private function doUpload($formElement, $sipUpload, Sip $sip) { // Status information about upload file $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); @@ -177,7 +270,8 @@ class Save { // Delete existing old file. if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') { - $oldFile = $this->store->getVar($formElement['name'], STORE_RECORD); + $arr = $sip->getVarsFromSip($sipUpload); + $oldFile = $arr[EXISTING_PATH_FILE_NAME]; if (file_exists($oldFile)) { if (!unlink($oldFile)) { throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK); @@ -197,6 +291,10 @@ class Save { } /** + * Copy uploaded file from temporary location to final location. + * + * Check also: doc/CODING.md + * * @param array $formElement * @param array $statusUpload * @return array|mixed|null|string @@ -206,17 +304,22 @@ class Save { private function copyUploadFile(array $formElement, array $statusUpload) { $pathFileName = ''; - if (isset($formElement[FE_PATH_FILE_NAME])) { + if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') { + // nothing to upload: e.g. user has deleted a previous uploaded file. + return ''; + } + + if (isset($formElement[FE_FILE_DESTINATION])) { // 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]); + $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]); } if ($pathFileName === '') { - throw new UserFormException("Upload failed, no target '" . FE_PATH_FILE_NAME . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME); + throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME); } if (file_exists($pathFileName)) { @@ -232,6 +335,7 @@ class Save { return $pathFileName; } + /** * Get the complete FormElement for $name * @@ -239,68 +343,13 @@ class Save { * @return bool|array if found the FormElement, else false. */ private function getFormElementByName($name) { + foreach ($this->feSpecNative as $formElement) { if ($formElement['name'] === $name) return $formElement; } - return false; - } - /** - * Insert new record in table $this->formSpec['tableName']. - * - * @param array $values - * @return int last insert id - * @throws DbException - */ - public function insertRecord($tableName, array $values) { - - if (count($values) === 0) - return 0; // nothing to write, last insert id=0 - - $paramList = str_repeat('?, ', count($values)); - $paramList = substr($paramList, 0, strlen($paramList) - 2); - $columnList = '`' . implode('`, `', array_keys($values)) . '`'; - - $sql = 'INSERT INTO ' . $tableName . ' ( ' . $columnList . ' ) VALUES ( ' . $paramList . ' )'; - - $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); - - return $rc; - } - - /** - * @param string $tableName - * @param array $values - * @param int $recordId - * @return bool|int false if $values is empty, else affectedrows - * @throws CodeException - * @throws DbException - */ - public function updateRecord($tableName, array $values, $recordId) { - - if (count($values) === 0) - return 0; // nothing to write, 0 rows affected - - if ($recordId === 0) - throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN); - -// $paramList = str_repeat('?, ', count($values)); -// $paramList = substr($paramList, 0, strlen($paramList) - 2); - - $sql = 'UPDATE `' . $tableName . '` SET '; - - foreach ($values as $column => $value) { - - $sql .= '`' . $column . '` = ?, '; - } - - $sql = substr($sql, 0, strlen($sql) - 2) . ' WHERE id = ?'; - $values[] = $recordId; - - $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); - - return $rc; + return false; } } \ No newline at end of file diff --git a/extension/qfq/qfq/exceptions/AbstractException.php b/extension/qfq/qfq/exceptions/AbstractException.php index 29ebe7f1bfc6f7fe00bf8b90b5bea2f44be5d39b..b48a72ed35379c69404698942b27e107a5c6e782 100644 --- a/extension/qfq/qfq/exceptions/AbstractException.php +++ b/extension/qfq/qfq/exceptions/AbstractException.php @@ -21,8 +21,6 @@ class AbstractException extends \Exception { protected $file = ''; protected $line = ''; - - /** * @return string */ @@ -41,36 +39,36 @@ class AbstractException extends \Exception { $this->messageArray['Line'] = $this->getLine(); $this->messageArray['Message'] = $this->getMessage(); $this->messageArray['Code'] = $this->getCode(); - + $this->messageArray['Timestamp'] = date('Y.m.d H:i:s O'); $this->messageArray['Stacktrace'] = '<pre>' . $this->getTraceAsString() . '</pre>'; if ($store !== null) { $this->messageArray['Page Id'] = $store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); $this->messageArray['Content Id'] = $store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3); } - $html .= "Code: " . $this->messageArray['Code'] . "<br>"; - $html .= "Message: " . Support::wrapTag("<strong>", $this->messageArray['Message']) . "</br>"; + $html .= "Code: " . htmlspecialchars($this->messageArray['Code']) . "<br>"; + $html .= "Message: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray['Message'])) . "</br>"; // Form if (isset($this->messageArray['Form'])) { - $html .= "Form: " . Support::wrapTag("<strong>", $this->messageArray['Form']) . "</br>"; + $html .= "Form: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray['Form'])) . "</br>"; } if (isset($this->messageArray['Form Element'])) { - $html .= "Form Element: " . Support::wrapTag("<strong>", $this->messageArray['Form Element']) . "</br>"; + $html .= "Form Element: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray['Form Element'])) . "</br>"; } // Report if (isset($this->messageArray[SYSTEM_REPORT_COLUMN_INDEX])) { - $html .= "Column index: " . Support::wrapTag("<strong>", $this->messageArray[SYSTEM_REPORT_COLUMN_INDEX]) . "</br>"; + $html .= "Column index: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray[SYSTEM_REPORT_COLUMN_INDEX])) . "</br>"; } if (isset($this->messageArray[SYSTEM_REPORT_COLUMN_NAME])) { - $html .= "Column name: " . Support::wrapTag("<strong>", $this->messageArray[SYSTEM_REPORT_COLUMN_NAME]) . "</br>"; + $html .= "Column name: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray[SYSTEM_REPORT_COLUMN_NAME])) . "</br>"; } if (isset($this->messageArray[SYSTEM_REPORT_COLUMN_VALUE])) { - $html .= "Column value: " . Support::wrapTag("<strong>", $this->messageArray[SYSTEM_REPORT_COLUMN_VALUE]) . "</br>"; + $html .= "Column value: " . Support::wrapTag("<strong>", htmlspecialchars($this->messageArray[SYSTEM_REPORT_COLUMN_VALUE])) . "</br>"; } $html = "<h2>Error</h2>" . Support::wrapTag('<p>', $html); @@ -78,6 +76,8 @@ class AbstractException extends \Exception { if ($store !== null && $store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes') { + $this->messageArray['current sip'] = $store->getStore(STORE_SIP); + // Layout $debug = '<tr bgcolor="#dddddd"><td colspan="2">Exception</td></tr>'; foreach ($this->messageArray as $key => $value) { @@ -86,7 +86,7 @@ class AbstractException extends \Exception { } if ($value !== '' && $value !== false) - $debug .= "<tr>" . "<td>$key</td>" . "<td>$value</td>" . "</tr>"; + $debug .= "<tr>" . "<td>$key</td>" . "<td>" . htmlspecialchars($value) . "</td>" . "</tr>"; } $debug = "<table border=1>" . $debug . "</table>"; } diff --git a/extension/qfq/qfq/exceptions/ErrorHandler.php b/extension/qfq/qfq/exceptions/ErrorHandler.php index 4af286be985c420d462fb73309b1c5f225fd5cc3..409e22b269ecb80918752d40b18b0fe2fd365cf9 100644 --- a/extension/qfq/qfq/exceptions/ErrorHandler.php +++ b/extension/qfq/qfq/exceptions/ErrorHandler.php @@ -13,6 +13,7 @@ class ErrorHandler { public static function exception_error_handler($severity, $message, $file, $line) { + if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting return false; diff --git a/extension/qfq/qfq/form/FormAction.php b/extension/qfq/qfq/form/FormAction.php new file mode 100644 index 0000000000000000000000000000000000000000..673ea19f37b5c348a4a1f5a58153ccb3205dd04f --- /dev/null +++ b/extension/qfq/qfq/form/FormAction.php @@ -0,0 +1,448 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 5/29/16 + * Time: 5:24 PM + */ + +namespace qfq; + +require_once(__DIR__ . '/../Constants.php'); +require_once(__DIR__ . '/../Database.php'); +require_once(__DIR__ . '/../store/Store.php'); +require_once(__DIR__ . '/../Evaluate.php'); +require_once(__DIR__ . '/../report/Sendmail.php'); + +/** + * Class formAction + * @package qfq + */ +class FormAction { + +// private $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form + /** + * @var Evaluate instantiated class + */ + protected $evaluate = null; // copy of the loaded form + private $formSpec = array(); + private $primaryTableName = ''; + /** + * @var Database + */ + private $db = null; + /** + * @var Store + */ + private $store = null; + + /** + * @param array $formSpec + * @param Database $db + * @param bool|false $phpUnit + */ + public function __construct(array $formSpec, Database $db, $phpUnit = false) { + $this->formSpec = $formSpec; + $this->primaryTableName = Support::setIfNotSet($formSpec, F_TABLE_NAME); + + $this->db = $db; + + $this->store = Store::getInstance('', $phpUnit); + + $this->evaluate = new Evaluate($this->store, $this->db); + + } + + /** + * @param integer $recordId + * @param array $feSpecAction + * @param string $feTypeList + * On FormLoad: FE_TYPE_BEFORE_LOAD, FE_TYPE_AFTER_LOAD + * Before Save: FE_TYPE_BEFORE_SAVE, FE_TYPE_BEFORE_INSERT, FE_TYPE_BEFORE_UPDATE, FE_TYPE_BEFORE_DELETE + * After Save: FE_TYPE_AFTER_SAVE, FE_TYPE_AFTER_INSERT, FE_TYPE_AFTER_UPDATE, FE_TYPE_AFTER_DELETE + * @return bool: true if there are potential changes on the DB like fired SQL statements, else false. + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + public function elements($recordId, array $feSpecAction, $feTypeList) { + + $flagModified = false; + + // Iterate over all Action FormElements + foreach ($feSpecAction as $fe) { + + $fe = $this->initActionFormElement($fe); + + // Only process FE elements of types listed in $feTypeList. Skip all other + if (false === Support::findInSet($fe[FE_TYPE], $feTypeList)) { + continue; + } + + // Preparation for Log, Debug + $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); + + switch ($fe[FE_TYPE]) { + case FE_TYPE_BEFORE_LOAD: + case FE_TYPE_AFTER_LOAD: + case FE_TYPE_AFTER_DELETE: # Main record is already deleted. Do not try to load it again. + break; + default: + // Always work on recent data: previous actions might have modified the data. + $this->fillStoreRecord($this->primaryTableName, $recordId); + } + + if (!$this->checkRequiredList($fe)) { + continue; + } + + if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) { + $this->sendMail($fe); + //no further processing of current element necessary. + continue; + } + + $this->validate($fe); + + $this->doSlave($fe, $recordId); + + $flagModified = true; + } + + return $flagModified; + } + + /** + * Set all necessary keys - subsequent 'isset()' are not necessary anymore. + * + * @param array $fe + * @return array + */ + private function initActionFormElement(array $fe) { + + $list = [FE_TYPE, FE_SLAVE_ID, FE_SQL_VALIDATE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE, FE_EXPECT_RECORDS, + FE_REQUIRED_LIST, FE_MESSAGE_FAIL, FE_SENDMAIL_TO, FE_SENDMAIL_CC, FE_SENDMAIL_BCC, FE_SENDMAIL_FROM, FE_SENDMAIL_SUBJECT, FE_SENDMAIL_REPLY_TO, + FE_SENDMAIL_FLAG_AUTO_SUBMIT, FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID]; + + foreach ($list as $key) { + Support::setIfNotSet($fe, $key); + } + + return $fe; + } + + /** + * Copy the current primary record to STORE_RECORD + * + * @param $table + * @param $recordId + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + private function fillStoreRecord($table, $recordId) { + + if (!is_string($table) || $table === '') { + throw new UserFormException(""); + } + 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); + } + } + + /** + * Process all FormElements given in the `requiredList` identified be their name. + * If none is empty in STORE_FORM return true, else false. + * If none FormElement is specified, return true. + * + * @param array $fe + * @return bool true if none FE is specified or all specified are non empty. + */ + private function checkRequiredList(array $fe) { + + if (!isset($fe[FE_REQUIRED_LIST]) || $fe[FE_REQUIRED_LIST] === '') { + return true; + } + + $arr = explode(',', $fe[FE_REQUIRED_LIST]); + foreach ($arr as $key) { + + $key = trim($key); + $val = $this->store->getVar($key, STORE_FORM); + + if ($val === false || $val === '' || $val === '0') { + return false; + } + } + + return true; + } + + /** + * @param array $feSpecAction + */ + private function sendMail(array $feSpecAction) { + + $mail[SENDMAIL_IDX_RECEIVER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]); + $mail[SENDMAIL_IDX_SENDER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]); + $mail[SENDMAIL_IDX_SUBJECT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]); + $mail[SENDMAIL_IDX_BODY] = $this->evaluate->parse($feSpecAction[FE_VALUE]); + $mail[SENDMAIL_IDX_REPLY_TO] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]); + $mail[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'on' ? 'on' : 'off'; + $mail[SENDMAIL_IDX_GR_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]); + $mail[SENDMAIL_IDX_X_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]); + $mail[SENDMAIL_IDX_RECEIVER_CC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]); + $mail[SENDMAIL_IDX_RECEIVER_BCC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]); + $mail[SENDMAIL_IDX_SRC] = "FormId: " . $feSpecAction['formId'] . ", FormElementId: " . $feSpecAction['id']; + + // Mail: send + new Sendmail($mail); + } + + /** + * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them. + * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS. + * If match: everything is fine, do nothing. + * Else throw UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL + * + * @param array $fe + * @throws UserFormException + */ + private function validate(array $fe) { + + // Is there something to check? + if ($fe[FE_SQL_VALIDATE] === '') { + return; + } + + $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]); + + if ($fe[FE_MESSAGE_FAIL] === '') { + throw new UserFormException("Missing error message. Column: " . FE_MESSAGE_FAIL, ERROR_MISSING_MESSAGE_FAIL); + } + + // Do the check + $result = $this->evaluate->parse($fe[FE_SQL_VALIDATE]); + if (!is_array($result)) { + throw new UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY); + } + + // If there is at least one record count given, who matches: return 'check succeeded' + $countRecordsArr = explode(',', $expect); + foreach ($countRecordsArr AS $count) { + if (count($result) == $count) { + return; // check succesfully passed + } + } + + $msg = $this->evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts + + // Throw user defineable error message + throw new UserFormException($msg, ERROR_REPORT_FAILED_ACTION); + } + + /** + * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or `sqlInsert` + * + * @param array $fe + * @return int + * @throws CodeException + * @throws UserFormException + */ + private function doSlave(array $fe, $recordId) { + + // Get the slaveId + $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]); + + if ($slaveId === '' && $fe[FE_NAME] !== '') { + // if the current action element has the same name as a real master record column: take that value as an id + $slaveId = $this->store->getVar($fe[FE_NAME], STORE_RECORD); + } + + if ($slaveId === '' || $slaveId === false) { + $slaveId = 0; + } + + // Store the slaveId: it's used and replaced in the update statement. + $this->store->setVar(ACTION_KEYWORD_SLAVE_ID, $slaveId, STORE_VAR, true); + + // Fire slave query + if ($slaveId == 0) { + $slaveId = $this->evaluate->parse($fe[FE_SQL_INSERT]); + // Store the slaveId: it's used and replaced in the update statement. + $this->store->setVar(ACTION_KEYWORD_SLAVE_ID, $slaveId, STORE_VAR, true); + } else { + $this->evaluate->parse($fe[FE_SQL_UPDATE]); + } + + // Check if there is a column with the same name as the 'action'-FormElement. + if (false !== $this->store->getVar($fe[FE_NAME], STORE_RECORD)) { + // After an insert or update, propagate the (new) slave id to the master record. + $this->db->sql("UPDATE " . $this->primaryTableName . " SET " . $fe[FE_NAME] . " = $slaveId WHERE id = ? LIMIT 1", ROW_REGULAR, [$recordId]); + } + + // If given: fire a delete query + $this->evaluate->parse($fe[FE_SQL_DELETE]); + + return $slaveId; + } +} +// +///******************************************************** +// * doAddNUpdate +// * RC: TRUE ok +// * FALSE,$err bei Fehler +// *********************************************************/ +//function doAddNUpdate($formId, $masterId, $tableName, &$err) { +// global $FeId; +// +// if ($this->DebugLevel > 3) t3lib_div::debug("doAddNUpdate"); +// +// // Selektiere alle 'addNupdate'-DS des aktuellen Formulars +// $sql = "SELECT * FROM form_element WHERE form_id=" . $formId . " AND typ='addNupdate' AND active='Yes' ORDER BY ord"; +// if ($this->DebugLevel > 3) t3lib_div::debug($sql); +// if (!($formDs = mysql(MATH_DB_NAME, $sql))) return ($this->buildMySQLErrMsg($err, $sql, __FILE__, __LINE__)); +// +// // Falls nix zu tun ist (kein DS vorhanden), gleich zurueck. +// if (mysql_num_rows($formDs) == 0) return (TRUE); +// +// // Durchlaufe alle 'addNupdate' DS +// while ($formularDs = mysql_fetch_array($formDs, MYSQL_ASSOC)) { +// +// $FeId = $formularDs['id']; // Just for logging +// +// if ($this->DebugLevel > 3) t3lib_div::debug($formularDs); +// +// // Lese den zuvor gespeicherten Master DS +// // Der Master DS sollte nach jedem AddNUpdate neu gelesen werden, evtl. wurde etwas eingetragen +// +// $masterDs = $this->doQuerySingle("SELECT * FROM " . $tableName . " WHERE id=" . $masterId, $err); +// if ($err) return (FALSE); +// +// // Check ob addNUpadte ueberhaupt ausgefuehrt werden soll: Ist 'param' gesetzt ? +// // JA: dann auswerten ob die in 'param' aufgefuehrten Formularfelder gefuellt sind +// // Wenn nein, naechsten addNUpdate DS bearbeiten +// // NEIN: normal weiter machen +// if ($formularDs["param"]) { +// $arr = explode(",", $formularDs["param"]); +// $t = TRUE; +// foreach ($arr as $elem) { +// if ($GLOBALS[HTTP_POST_VARS][FRM . $elem]) { +// $t = FALSE; +// break; //foreach +// } +// } +// if ($t) { +// if ($this->DebugLevel > 3) t3lib_div::debug("doAddNUpdate() nicht ausfuehren, da param gesetzt ist und angegebene Felder leer sind."); +// continue; //while +// } +// } +// +// # Falls in 'value' ein select Statement angegeben ist, dieses ausführen. +// # Es sollte 0 oder 1 DS gefunden werden. +// # Die Spalte 'id' muss vorhanden sein und diese die slaveID angeben. +// if ($formularDs["value"]) { +// $sql = $this->substituteAssoc($formularDs["value"], $masterDs); +// $slaveDs = $this->doQuerySingle($sql, $err, EMPTY_IS_OK); +// $slaveId = $slaveDs["id"]; +// } else { +// +// // Name des aktuellen addNupdate Formularelementes, kann gleichzeitig eine ID im masterDs auf eine slaveDS sein +// // Ist so eine ID>0 muss ein Update durchgefuehrt werden, sonst ein insert. +// // Bsp: Formular 'publikation_mit_upload_new' - zur aktuellen Publikation wird ein Notiz DS mit dem Dateinamen der Publikation angelegt. +// if ($masterDs[$formularDs["name"]]) +// $slaveId = $masterDs[$formularDs["name"]]; +// else +// $slaveId = $GLOBALS[HTTP_POST_VARS][FRM . $formularDs["name"]]; // einige spezielle doAddNUpdate benutzen temporaere Variablen (z.B. Formular publikation_mit_upload: my_pid +// } +// +// if ($slaveId > 0) { +// +// // if($GLOBALS[HTTP_POST_VARS][FRM."id".$post]>0) { // Es existiert ein zugehoeriger DS (=Slave) +// +// // Check ob ein Update Statement existiert +// if (!$formularDs["sql_update"]) +// continue; +// +// // Ersetze Variablen in dem SQL update Statement. +// $sql = $this->substituteAssoc($formularDs["sql_update"], $masterDs); +// +// if ($this->DebugLevel > 0) echo("doAddNUpdate(update):" . $sql . "<BR>"); +// +// // Fuehre das Update auf den Slave DS aus. +// if ($this->DebugLevel > 3) t3lib_div::debug($sql); +// if (!($res = $this->doSQL(MATH_DB_NAME, $sql . " "))) return ($this->buildMySQLErrMsg($err, $sql . " ", __FILE__, __LINE__, $formularDs)); +// +// } else { // Es existiert noch kein zugehoeriger DS (=Slave) +// +// // Check ob ein Insert Statement existiert +// if (!$formularDs["sqlq"]) +// continue; +// +// // Ersetze Variablen in dem SQL insert Statement. +// $sql = $this->substituteAssoc($formularDs["sqlq"], $masterDs); +// +// // Fuehre das Update auf den Slave DS aus. +// if ($this->DebugLevel > 1) t3lib_div::debug($sql); +// if (!($res = $this->doSQL(MATH_DB_NAME, $sql))) { +// $this->buildMySQLErrMsg($err, $sql, __FILE__, __LINE__); +// return (FALSE); +// } +// +// // Bestimme den Tabellennamen der im Slave SQL Statement benutzt wird +// $arr = explode(" ", $sql); // +// if ("insert" == mb_strtolower(mb_substr(ltrim($sql), 0, 6))) { +// +// if (mysql_affected_rows() > 0) { +// // Lade gerade geschriebenen Record +// $slaveTableName = mb_strtolower($arr[1]) == "into" ? $arr[2] : $arr[1]; // sql: "insert into <table> ..... der dritte Parameter ist der Tabellenname +// +// // Lese die Id des neu angelegten DS +// $slaveId = mysql_insert_id(); +// +// // Bei einigen Tabellen gibt es keine Spalte 'id' - darum die gesuchte Spalte ueber die Definition von auto_increment bestimmen. I.d.R. 'id' +// // Bsp: einfuegen von fe_usern in die T3 Tabelle 'fe_users' +// $sql = "show fields from $slaveTableName where Extra like 'auto_increment'"; +// if (!($tmp = $this->doQuerySingle($sql, $err))) { +// $this->buildMySQLErrMsg($err, $sql, __FILE__, __LINE__); +// return (FALSE); +// } +// $colNameId = $tmp["Field"]; +// +// // Lade den durch addNupdate erzeugten record +// $sql = "select *, $colNameId as id from $slaveTableName where $colNameId = $slaveId "; +// if (!($slaveDs = $this->doQuerySingle($sql, $err))) { +// $this->buildMySQLErrMsg($err, $sql, __FILE__, __LINE__); +// return (FALSE); +// } +// } +// +// +// // Ersetze Variablen in dem SQL do after Statement +// $sql = $this->substituteAssoc($formularDs["sql_do_after"], $masterDs); // masterDs +// if ($sql) { +// +// if ($this->DebugLevel > 0) +// echo("doAddNUpdate/sql_do_after(new):" . $sql . "<BR>"); +// +// $sql = str_replace("~_", "~", $sql); +// $sql = $this->substituteAssoc($sql, $slaveDs); // slaveDs +// +// if ($this->DebugLevel > 0) t3lib_div::debug("doAddNUpdate/sql_do_after(insert):" . $sql); +// +// // Führe das Update auf den Slave DS aus. +// if ($this->DebugLevel > 1) t3lib_div::debug($sql); +// if (!($res = $this->doSQL(MATH_DB_NAME, $sql))) { +// $this->buildMySQLErrMsg($err, $sql, __FILE__, __LINE__); +// return (FALSE); +// } +// } +// } else { +// if ($this->DebugLevel > 0) echo("doAddNUpdate/sql_do_after - in sql kein select gefunden:" . $sql . "<BR>"); +// } +// } +// } // while() +// +// return (TRUE); +//} // doAddNUpdate diff --git a/extension/qfq/qfq/helper/HelperFormElement.php b/extension/qfq/qfq/helper/HelperFormElement.php index 958c4fcfad24d12fd94f60ad570c7fe9e20b824c..9645bf27f848b2557a5a3398aaf87f6b5f580b0b 100644 --- a/extension/qfq/qfq/helper/HelperFormElement.php +++ b/extension/qfq/qfq/helper/HelperFormElement.php @@ -21,6 +21,9 @@ class HelperFormElement { /** + * Expand column 'parameter' to row array as virtual columns. + * E.g.: 'detail=xId:grId\nShowEmptyAtStart=1` + * * @param array $elements * @throws \qfq\UserFormException */ @@ -61,13 +64,26 @@ class HelperFormElement } /** - * @param $field - * @param $id + * Build the FE name: <field>:<record index) + * + * @param string $field + * @param string $id * @return string */ - public static function buildFormElementId($field, $id) + public static function buildFormElementName($field, $id) { return ($field . ':' . $id); } + /** + * Checkboxen, belonging to one element, grouped together by name: <fe>_<field>_<index> + * + * @param string $field + * @param string $index + * @return string + */ + public static function prependFormElementIdCheckBoxMulti($field, $index) { + return ('_' . $index . '_' . $field); + } + } \ No newline at end of file diff --git a/extension/qfq/qfq/helper/KeyValueStringParser.php b/extension/qfq/qfq/helper/KeyValueStringParser.php index 690e8911a458c4409ba99c3f4481528b98cb99a0..b44f9ab9c46f7b682ebd433e46a51d690f0c8df6 100644 --- a/extension/qfq/qfq/helper/KeyValueStringParser.php +++ b/extension/qfq/qfq/helper/KeyValueStringParser.php @@ -37,6 +37,8 @@ require_once(__DIR__ . '/../../qfq/Constants.php'); class KeyValueStringParser { /** + * Builds a string based on kvp array. Concatenatet by the given delimiter. + * * @param array $keyValueArray * @param string $keyValueDelimiter * @param string $listDelimiter @@ -82,8 +84,8 @@ class KeyValueStringParser { * @param string $keyValueDelimiter * @param string $listDelimiter * @param string $valueMode - * * VALUE_GIVEN: If only a key is given, the value is ''. E.G. 'a,b' >> [ 'a' => '', 'b' => '' ] - * * IF_VALUE_EMPTY_COPY_KEY: If only a key is given, the value is the same as the key. E.G. 'a,b' >> [ 'a' => 'a', 'b' => 'b' ]. + * * KVP_VALUE_GIVEN: If only a key is given, the value is ''. E.G. 'a,b' >> [ 'a' => '', 'b' => '' ] + * * KVP_IF_VALUE_EMPTY_COPY_KEY: If only a key is given, the value is the same as the key. E.G. 'a,b' >> [ 'a' => 'a', 'b' => 'b' ]. * @return array associative array indexed by keys * @throws UserFormException Thrown if there is a value but no key. */ diff --git a/extension/qfq/qfq/helper/OnArray.php b/extension/qfq/qfq/helper/OnArray.php index d164fff57f91ba6fea35094ed7d752122ab0e248..a9b6389646504b3f6794187d2c87366d7d3fd3ab 100644 --- a/extension/qfq/qfq/helper/OnArray.php +++ b/extension/qfq/qfq/helper/OnArray.php @@ -11,6 +11,8 @@ namespace qfq; require_once(__DIR__ . '/../../qfq/Constants.php'); +const SUBSTITUTE = '#%SUB%#'; + /** * Class OnArray * @package qfq @@ -109,4 +111,31 @@ class OnArray { } return $arr; } + + /** + * Split Array around $str to $arr around $delimiter. Escaped $delimiter will be preserved. + * + * @param string $delimiter + * @param string $str + * @return array + * @throws UserReportException + */ + public static function explodeWithoutEscaped($delimiter, $str) { + + if (strpos($str, SUBSTITUTE) !== false) { + throw new UserReportException ("Can't replace token by SUBSTITUTE, cause SUBSTITUTE already exist", ERROR_SUBSTITUTE_FOUND); + } + + $encodedStr = str_replace('\\' . $delimiter, SUBSTITUTE, $str); + + $arr = explode($delimiter, $encodedStr); + + for ($ii = 0; $ii < count($arr); $ii++) { +// $arr[$ii] = str_replace(SUBSTITUTE, '\\' . $delimiter, $arr[$ii]); + $arr[$ii] = str_replace(SUBSTITUTE, $delimiter, $arr[$ii]); + } + + return $arr; + } + } \ No newline at end of file diff --git a/extension/qfq/qfq/helper/Sanitize.php b/extension/qfq/qfq/helper/Sanitize.php index 67e48f248daf370e6048defade3d8ae1b0493ebf..9995910dc74edfcb61ba3e1ce36bd6d5ff277baa 100644 --- a/extension/qfq/qfq/helper/Sanitize.php +++ b/extension/qfq/qfq/helper/Sanitize.php @@ -143,7 +143,7 @@ class Sanitize { SANITIZE_ALLOW_MIN_MAX => '', SANITIZE_ALLOW_MIN_MAX_DATE => '', SANITIZE_ALLOW_PATTERN => '', - SANITIZE_ALLOW_ALLBUT => '^[^\[\]{}%&\\#]+$', + SANITIZE_ALLOW_ALLBUT => '^[^\[\]{}%&\\\\#]*$', SANITIZE_ALLOW_ALL => '.*' ]; } diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index f3c1446a2976fcc11196a1d9a3f6f1dc977a0f97..e37cc6bbed0fa5b3ce494d85005c9b1120845565 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -11,6 +11,9 @@ namespace qfq; require_once(__DIR__ . '/../Constants.php'); require_once(__DIR__ . '/Sanitize.php'); +const LONG_CURLY_OPEN = '#/+open+/#'; +const LONG_CURLY_CLOSE = '#/+close+/#'; + class Support { /** @@ -89,11 +92,67 @@ class Support { * @param bool $flagOmitEmpty * @return string */ - public static function doAttribute($type, $value, $flagOmitEmpty = true) { - if ($flagOmitEmpty && $value === "") + public static function doAttribute($type, $value, $flagOmitEmpty = true, $modeEscape = ESCAPE_WITH_BACKSLASH) { + if ($flagOmitEmpty && $value === "") { return ''; + } + + switch (strtolower($type)) { + case 'size': + case 'maxlength': + // empty or '0' for attributes of type 'size' or 'maxlenght' result in unsuable input elements: skip this. + if ($value === '' || $value == 0) { + return ''; + } + break; + // Bad idea to do urlencode on this place: it will convert ?, &, ... which are necessary for a proper URL. + // Instead the value of a parameter needs to encode. Unfortunately, it's too late on this place. +// case 'href': +// $value = urlencode($value); +// break; + default: + break; + } + + $value = self::escapeDoubleTick(trim($value), $modeEscape); + + return $type . '="' . $value . '" '; - return $type . '="' . trim($value) . '" '; + } + + /** + * Escapes Double Ticks ("), which are not already escaped. + * modeEscape: ESCAPE_WITH_BACKSLASH | ESCAPE_WITH_HTML_QUOTE + * + * TinyMCE: Encoding JS Attributes (keys & values) for TinyMCE needs to be encapsulated in '"' instead of '\"'. + * + * @param $str + * @param string $modeEscape + * @return string + * @throws CodeException + */ + public static function escapeDoubleTick($str, $modeEscape = ESCAPE_WITH_BACKSLASH) { + $newStr = ''; + + for ($ii = 0; $ii < strlen($str); $ii++) { + if ($str[$ii] === '"') { + if ($ii === 0 || $str[$ii - 1] != '\\') { + switch ($modeEscape) { + case ESCAPE_WITH_BACKSLASH: + $newStr .= '\\'; + break; + case ESCAPE_WITH_HTML_QUOTE: + $newStr .= '"'; + continue 2; + default: + throw new CodeException('Unknown modeEscape=' . $modeEscape, ERROR_UNKNOWN_ESCAPE_MODE); + } + } + } + $newStr .= $str[$ii]; + } + + return $newStr; } /** @@ -114,9 +173,9 @@ class Support { } /** - * Search for the parameter $needle in $haystack. The argumenst has to be seperated by ','. + * Search for the parameter $needle in $haystack. The arguments has to be seperated by ','. * - * Returns the false if not found or index of found place. Be carefull: use unary operator to compare for 'false' + * Returns false if not found or index of found place. Be carefull: use unary operator to compare for 'false' * * @param string $needle * @param string $haystack @@ -126,7 +185,6 @@ class Support { $arr = explode(',', $haystack); return array_search($needle, $arr) !== false; - } /** @@ -427,10 +485,10 @@ class Support { $timePattern = ($formElement[FE_SHOW_SECONDS] == 1) ? 'hh:mm:ss' : 'hh:mm'; switch ($formElement[FE_TYPE]) { case 'date': - $placeholder = $formElement['dateFormat']; + $placeholder = $formElement[FE_DATE_FORMAT]; break; case 'datetime': - $placeholder = $formElement['dateFormat'] . ' ' . $timePattern; + $placeholder = $formElement[FE_DATE_FORMAT] . ' ' . $timePattern; break; case 'time': $placeholder = $timePattern; @@ -451,8 +509,8 @@ class Support { */ public static function encryptDoubleCurlyBraces($text) { - $text = str_replace('{{', '#&@[[@_#', $text); - $text = str_replace('}}', '#&@]]@_#', $text); + $text = str_replace('{{', LONG_CURLY_OPEN, $text); + $text = str_replace('}}', LONG_CURLY_CLOSE, $text); return $text; } @@ -464,8 +522,8 @@ class Support { * @return mixed */ public static function decryptDoubleCurlyBraces($text) { - $text = str_replace('#&@[[@_#', '{{', $text); - $text = str_replace('#&@]]@_#', '}}', $text); + $text = str_replace(LONG_CURLY_OPEN, '{{', $text); + $text = str_replace(LONG_CURLY_CLOSE, '}}', $text); return $text; } @@ -485,6 +543,8 @@ class Support { } /** + * Concatenate URL and Parameter. Depending of if there is a '?' in URL or not, append the param with '?' or '&'.. + * * @param string $url * @param string $param * @return string @@ -512,6 +572,9 @@ class Support { self::setIfNotSet($formElement, FE_SHOW_ZERO, '0'); self::setIfNotSet($formElement, FE_DATE_FORMAT, $store->getVar(SYSTEM_DATE_FORMAT, STORE_SYSTEM)); + if ($formElement[FE_MODE_SQL] != '') + $formElement[FE_MODE] = $formElement[FE_MODE_SQL]; + return $formElement; } @@ -520,6 +583,7 @@ class Support { * @param string $index * @param string $value * @param string|bool $overwriteThis If there is already something which is equal to $overwrite: take new default. + * @return mixed */ public static function setIfNotSet(array &$arr, $index, $value = '', $overwriteThis = false) { @@ -530,6 +594,8 @@ class Support { if ($overwriteThis !== false && $arr[$index] === $overwriteThis) { $arr[$index] = $value; } + + return $arr[$index]; } /** diff --git a/extension/qfq/qfq/report/Db.php b/extension/qfq/qfq/report/Db.php deleted file mode 100644 index 530f854a9345038b52d7a28fdda44885bfdaf0b9..0000000000000000000000000000000000000000 --- a/extension/qfq/qfq/report/Db.php +++ /dev/null @@ -1,373 +0,0 @@ -<?php -/*************************************************************** - * Copyright notice - * - * (c) 2010 Glowbase GmbH <support@glowbase.com> - * All rights reserved - * - * This script is part of the TYPO3 project. The TYPO3 project is - * free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * The GNU General Public License can be found at - * http://www.gnu.org/copyleft/gpl.html. - * - * This script is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * This copyright notice MUST APPEAR in all copies of the script! - ***************************************************************/ - -namespace qfq; - -//use qfq; - -require_once(__DIR__ . '/Define.php'); -require_once(__DIR__ . '/Error.php'); -require_once(__DIR__ . '/Log.php'); - - -class Db { - - public $t3_typo_db_host = ""; - /** - * @var string - */ - private $lastUsedDB = ""; - /** - * @var array - */ - private $arrDB = array(); - /** - * @var - */ - private $t3_typo_db_username, $t3_typo_db, $t3_typo_db_password; - /** - * @var Log - */ - private $log; - - // Emulate global variable: will be set much earlier in other functions. Will be shown in error messages. - private $fr_error; - - - /** - * Constructor: - * - * @param Log fully created for logging. - * - */ - - public function __construct($log) { - // CR 25.4.11: require_once does not work here. No idea why - require(PATH_typo3conf . 'localconf.php'); - $this->t3_typo_db_host = $typo_db_host; - $this->t3_typo_db_username = $typo_db_username; - $this->t3_typo_db = $typo_db; - $this->t3_typo_db_password = $typo_db_password; - - $this->log = $log; - } - - /** - * Set Array fr_error: setter function to set most recent values, especially fr_erro['row']. - * Will be shown in error messages. - * - * @param array $fr_error uid, pid, row, column_idx, full_level - */ - public function set_fr_error(array $fr_error) { - $this->fr_error = $fr_error; - } - - /** - * doQueryKeys: See doQuery - * Difference: fake Array for $keys - * - * @param string $dbAlias - * @param string $sql - * @param array $result - * @param string string $expect - * @param string $merge - * @return bool - * @throws CodeReportException - * @throws SqlReportException - * @throws SyntaxReportException - */ - public function doQuery($dbAlias, $sql, array &$result, $expect = ROW_REGULAR, $merge = MERGE_NONE) { - return ($this->doQueryKeys($dbAlias, $sql, $result, $fake, $expect, $merge, MYSQL_ASSOC)); - } - - /** - * doQueryKeys: fires a show, select, insert, update or delete and collects result. - * insert, update and delete will produce a log entry. - * If: $expect==EXPECT_SQL_OR_STRING, '$sql' can be anything which won't be fired if it's not a SQL statement. - * - * @param string $dbAlias Name of Database to be used. - * @param string $sql Select Query - * @param array $result content depends on $sql. - * $sql='insert ...': mysql_last_insert_id will be returned in $result. - * $sql='update ...' or 'delete ----': mysql_affected_rows will be returned in $result. - * $sql='select ...': all selected rows will be returned in $result. - * $result will be formatted like specified in $merge. - * Attention: with EXPECT_1|EXPECT_0_1 '$result' is a one dimensional array, else a two dimensional array. - * @param array $keys - * @param string $expect - * @param string $merge Applies different modes of merging - MERGE_NONE, MERGE_ROW, MERGE_ALL - * @param int $arrayMode - * @return bool true: all ok - * false: a) Number of rows don't match $expect - * b) $expect==EXPECT_SQL_OR_STRING and $sql is not an SQL expression (instead it's a regular string) - this is not bad, just to indicate that there was no query. - * @throws CodeReportException - * @throws SqlReportException - * @throws SyntaxReportException - */ - public function doQueryKeys($dbAlias, $sql, array &$result, array &$keys, $expect = ROW_REGULAR, $merge = MERGE_NONE, $arrayMode = MYSQL_NUM) { - $result = ""; - $tmp = ""; - $action = ""; - - $this->selectDB($dbAlias); - - if ($this->fr_error["debug_level"] >= DEBUG_SQL) { - // T3 function: debug() -// debug(array('SQL' => $sql)); - } - - // Extract first parameter to check if it is a SQL statement - $tmp = explode(" ", trim($sql), 2); - $action = strtolower($tmp[0]); - switch ($action) { - case "show" : - case "select": - case "insert": - case "update": - case "delete": - break; // SQL Statement: go further - default: - if ($expect == EXPECT_SQL_OR_STRING) { - $result = $sql; - return (false); // nothing bad, just to indicate $sql was not a SQL statement. - } else - throw new SyntaxReportException ("Unexpected SQL Statement: '$action'", "", __FILE__, __LINE__, array("DB:$dbAlias", "SQL:$sql"), $this->fr_error); - } - - // Fire SQL statement - if (!($res = mysql_query($sql))) { - // Escape query if in AJAX mode - $sql = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') ? addslashes($sql) : $sql; - throw new SqlReportException ("Did not get result for query.", $sql, __FILE__, __LINE__, $this->fr_error); - } - - - switch ($action) { - case "show" : - case "select": - $action = QUERY; // aggregate 'SLELECT' and 'SHOW' and ... - $num_rows = mysql_num_rows($res); - break; - - case "insert": - $num_rows = mysql_affected_rows(); - $result = mysql_insert_id(); - break; - case "update": - case "delete": - $num_rows = mysql_affected_rows(); - $result = $num_rows; - break; - - default: - throw new CodeReportException ("This error should be catched 20 lines above.", __FILE__, __LINE__); // can't be happen, should already be detected earlier. - } - - // Logging - if ($action != QUERY) { - // Not a query: write log and go home. - $this->log->log_sql('db', '', $num_rows, $result, $sql); - return (TRUE); - } else { - // Logging if localconf log_level >=D2 - $this->log->log_do('error', 'D2', '-', $sql); - } - - // Check $expect against real result. - switch ($expect) { - case ROW_EXPECT_0: - return ($num_rows == 0); - break; - case ROW_EXPECT_1: - if ($num_rows != 1) return (false); - break; - case ROW_EXPECT_0_1: - if ($num_rows > 1) return (false); - break; -// case ROW_EXPECT_GE_0: -// break; - case ROW_EXPECT_GE_1: - if ($num_rows == 0) return (false); - break; - case EXPECT_SQL_OR_STRING: - break; - default: - throw new CodeReportException ("Unknown 'expect' qualifier: $expect", __FILE__, __LINE__); - break; - } - - // Preparation to fetch all rows - $tmp = ""; - $fieldCount = 0; - - // Fetch all rows: - while ($row = mysql_fetch_array($res, $arrayMode)) { - foreach ($row as $key => $value) { - $row[$key] = stripslashes($value); - } - - switch ($merge) { - case MERGE_NONE: - $tmp[] = $row; - break; - case MERGE_ROW: - $tmp[] = implode($row); - break; - case MERGE_ALL: - $tmp .= implode($row); - break; - default: - throw new CodeReportException ("Unknown 'merge' qualifier: $merge", __FILE__, __LINE__); - break; - } - } - - // Collect 'keys' - if ($merge == MERGE_NONE) { - $keys = array(); - $numberfields = mysql_num_fields($res); - - for ($i = 0; $i < $numberfields; $i++) { - $keys[] = mysql_field_name($res, $i); - } - } - - // adjust Result Array: one or two dimensions. - switch ($expect) { - case ROW_EXPECT_0: - break; - case ROW_EXPECT_1: - case ROW_EXPECT_0_1: - case EXPECT_SQL_OR_STRING: - $result = $tmp[0]; - break; - default: - $result = $tmp; - } - - if ($merge == MERGE_ALL) - $result = $tmp; - - mysql_free_result($res); - - return (TRUE); - } // doQueryKeys() - - /** - * select DB - * - * @param string $dbAlias : Name of the dbname - * - * @return bool TRUE if ok, else exception. - */ - public function selectDB($dbAlias) { - - if (!$dbAlias) - throw new CodeReportException ("Failed: empty dbAlias", __FILE__, __LINE__); - - // If the db is still selected: do nothing. - if ($dbAlias == $this->lastUsedDB) - return true; - - // if the db is already open - just select it. - if (isset($this->arrDB[$dbAlias]['link'])) { - if (!mysql_select_db($this->arrDB[$dbAlias]['db'])) - throw new SqlReportException ("Failed: mysql_select_db($this->arrDB[$dbAlias]['db'])", "", __FILE__, __LINE__, $this->fr_error); - return true; - } - - $this->openDB($dbAlias); - - return true; - } // openDB() - - /** - * Open specified DB - * - * @param string $dbAlias Name of database to be opened - * @throws SqlReportException - */ - public function openDB($dbAlias) { - // TYPO3 globale Variablen -// global $typo_db_host,$typo_db_username,$typo_db,$typo_db_password; -// Du sollst kein global verwenden!! - - if ($dbAlias == T3) { - $host = $this->t3_typo_db_host; - $username = $this->t3_typo_db_username; - $db = $this->t3_typo_db; - $password = $this->t3_typo_db_password; - } else { -// require(PATH_typo3conf.'ext/formreport/ext_localconf.php'); - $host = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT][$dbAlias]['host']; - $username = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT][$dbAlias]['username']; - $db = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT][$dbAlias]['name']; - $password = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT][$dbAlias]['password']; - - } - - // If 't3' is specified or the custom DB is not fully specified, take credentials from localconf.php -# $host = $host ? $host : $typo_db_host; -# $username = $username ? $username : $typo_db_username; -# $db = $db ? $db : $typo_db; -# $password = $password ? $password : $typo_db_password; - - - // MySQL Connect - if (!($link = mysql_connect($host, $username, $password))) - throw new SqlReportException ("mysql_connect($host, $username)", "", __FILE__, __LINE__, $this->fr_error); - - // Set connection charset - if (!mysql_set_charset('utf8', $link)) - throw new SqlReportException ("mysql_set_charset('utf8', $link)", "", __FILE__, __LINE__, $this->fr_error); - - // MySQL select - if (!mysql_select_db($db, $link)) - throw new SqlReportException ("mysql_select_db($db)", "", __FILE__, __LINE__, $this->fr_error); - - // Remember that this DB has been opened. - $this->arrDB[$dbAlias]['link'] = $link; - $this->arrDB[$dbAlias]['db'] = $db; - - // Remember the new DB as 'last used' - $this->lastUsedDB = $dbAlias; - - } // selectDB() - - /** - * closeAllDB - * - * @return void - */ - public function closeAllDB() { - - foreach ($this->arrDB as $key => $value) { - if ($key != T3) { - mysql_close($value); - $arrDB[$key] = null; - } - } - - } // closeAllDB() -} diff --git a/extension/qfq/qfq/report/Define.php b/extension/qfq/qfq/report/Define.php index cf90fab5758d104fd5d12ec477918f5875697cee..45167bcfe4f5e3971bb8072228273c924b493501 100644 --- a/extension/qfq/qfq/report/Define.php +++ b/extension/qfq/qfq/report/Define.php @@ -58,7 +58,8 @@ define("DFLT_UPLOAD_BASE_DIR", "fileadmin"); define("DFLT_UPLOAD_TMP_DIR", "fileadmin/tempfiles"); define("DFLT_UPLOAD_TMP_TTL", "300"); -define("PATH_ICONS", "typo3conf/ext/qfq/Resources/Public/icons/"); +//define("PATH_ICONS", "typo3conf/ext/qfq/Resources/Public/icons/"); +const PATH_ICONS = 'typo3conf/ext/qfq/Resources/Public/icons'; // Definitions to allow successfull include of ext_localconf. //define( 'TYPO3_MODE', '1' ); //define( 'FORMREPORT', '1' ); diff --git a/extension/qfq/qfq/report/Link.php b/extension/qfq/qfq/report/Link.php index 407bd3d13a1b61c420f54ac4043e8a57bbb00e14..9a39d2ded05268bebc4bab412aba939b59cdaee4 100644 --- a/extension/qfq/qfq/report/Link.php +++ b/extension/qfq/qfq/report/Link.php @@ -27,10 +27,10 @@ namespace qfq; //use qfq; require_once(__DIR__ . '/Define.php'); -require_once(__DIR__ . '/Utils.php'); require_once(__DIR__ . '/../store/Store.php'); require_once(__DIR__ . '/../store/Sip.php'); require_once(__DIR__ . '/../exceptions/UserReportExtension.php'); +require_once(__DIR__ . '/../helper/KeyValueStringParser.php'); /* * u:url @@ -57,7 +57,7 @@ require_once(__DIR__ . '/../exceptions/UserReportExtension.php'); * P:picture [file] * C:checkbox [name] * R:right - * h:hash + * s:sip * * A: A:[u:p:m] * G: G:[N|..] @@ -82,12 +82,71 @@ const NAME_LINK_CLASS = 'linkClass'; const NAME_LINK_CLASS_DEFAULT = 'linkClassDefault'; const NAME_QUESTION = 'question'; const NAME_ENCRYPTION = 'encryption'; -const NAME_HASH = 'hash'; +const NAME_SIP = 'sip'; const NAME_URL_PARAM = 'param'; -const NAME_RIGHT_PICTURE_POSITION = 'picturePosition'; +const NAME_RIGHT = 'picturePositionRight'; +const NAME_ACTION_DELETE = 'actionDelete'; + +const FINAL_HREF = 'finalHref'; +const FINAL_ANCHOR = 'finalAnchor'; +const FINAL_CONTENT = 'finalContent'; +const FINAL_SYMBOL = 'finalSymbol'; +const FINAL_TOOL_TIP = 'finalToolTip'; +const FINAL_CLASS = 'finalClass'; +const FINAL_QUESTION = 'finalQuestion'; + +const TOKEN_URL = 'u'; +const TOKEN_MAIL = 'm'; +const TOKEN_PAGE = 'p'; +const TOKEN_TEXT = 't'; +const TOKEN_ALT_TEXT = 'a'; +const TOKEN_TOOL_TIP = 'o'; +const TOKEN_PICTURE = 'P'; +const TOKEN_BULLET = 'B'; +const TOKEN_CHECK = 'C'; +const TOKEN_DELETE = 'D'; +const TOKEN_EDIT = 'E'; +const TOKEN_HELP = 'H'; +const TOKEN_INFO = 'I'; +const TOKEN_NEW = 'N'; +const TOKEN_SHOW = 'S'; +const TOKEN_RENDER = 'r'; +const TOKEN_TARGET = 'g'; +const TOKEN_CLASS = 'c'; +const TOKEN_QUESTION = 'q'; +const TOKEN_ENCRYPTION = 'e'; +const TOKEN_SIP = 's'; +const TOKEN_URL_PARAM = 'U'; +const TOKEN_RIGHT = 'R'; + +const TOKEN_ACTION_DELETE = 'x'; +const TOKEN_ACTION_DELETE_AJAX = 'a'; +const TOKEN_ACTION_DELETE_REPORT = 'r'; +const TOKEN_ACTION_DELETE_CLOSE = 'c'; + +const TOKEN_CLASS_NONE = 'n'; +const TOKEN_CLASS_INTERNAL = 'i'; +const TOKEN_CLASS_EXTERNAL = 'e'; + +const LINK_ANCHOR = 'linkAnchor'; +const LINK_PICTURE = 'linkPicture'; const NO_CLASS = 'no_class'; +const DEFAULT_BULLET_COLOR = 'green'; +const DEFAULT_CHECK_COLOR = 'green'; +const DEFAULT_RENDER_MODE = '0'; +const DEFAULT_QUESTION_TEXT = 'Please confirm'; +const DEFAULT_QUESTION_LEVEL = 'info'; +const DEFAULT_ACTION_DELETE = 'r'; + +const QUESTION_INDEX_TEXT = 0; +const QUESTION_INDEX_LEVEL = 1; +const QUESTION_INDEX_BUTTON_OK = 2; +const QUESTION_INDEX_BUTTON_FALSE = 3; +const QUESTION_INDEX_TIMEOUT = 4; +const QUESTION_INDEX_FLAG_MODAL = 5; + /** * Class Link * @package qfq @@ -107,86 +166,96 @@ class Link { */ private $store = null; - // Simulate global variable: will be set much earlier in other functions. Will be shown in error messages. - private $fr_error; - /** - * @var Utils + * @var bool */ - private $utils; + private $phpUnit; + /** - * @var array + * @var Utils */ + private $renderControl = array(); - private $linkClassSelector = array("i" => "internal", "e" => "external"); + private $linkClassSelector = array(TOKEN_CLASS_INTERNAL => "internal ", TOKEN_CLASS_EXTERNAL => "external "); private $cssLinkClassInternal = ''; private $cssLinkClassExternal = ''; private $callTable = [ - 'u' => 'buildUrl', - 'm' => 'buildMail', - 'p' => 'buildPage', - 'o' => 'buildToolTip', - 'P' => 'buildPicture', - 'B' => 'buildBullet', - 'C' => 'buildCheck', - 'D' => 'buildDelete', - 'E' => 'buildEdit', - 'H' => 'buildHelp', - 'I' => 'buildInfo', - 'N' => 'buildNew', - 'S' => 'buildShow', + TOKEN_URL => 'buildUrl', + TOKEN_MAIL => 'buildMail', + TOKEN_PAGE => 'buildPage', + TOKEN_TOOL_TIP => 'buildToolTip', + TOKEN_PICTURE => 'buildPicture', + TOKEN_BULLET => 'buildBullet', + TOKEN_CHECK => 'buildCheck', + TOKEN_DELETE => 'buildDeleteIcon', + TOKEN_ACTION_DELETE => 'buildActionDelete', + TOKEN_EDIT => 'buildEdit', + TOKEN_HELP => 'buildHelp', + TOKEN_INFO => 'buildInfo', + TOKEN_NEW => 'buildNew', + TOKEN_SHOW => 'buildShow', ]; private $tableVarName = [ - 'u' => NAME_URL, - 'm' => NAME_MAIL, - 'p' => NAME_PAGE, - 't' => NAME_TEXT, - 'a' => NAME_ALT_TEXT, - 'o' => NAME_TOOL_TIP, - 'P' => NAME_IMAGE, - 'B' => NAME_IMAGE, - 'C' => NAME_IMAGE, - 'D' => NAME_IMAGE, - 'E' => NAME_IMAGE, - 'H' => NAME_IMAGE, - 'I' => NAME_IMAGE, - 'N' => NAME_IMAGE, - 'S' => NAME_IMAGE, - 'r' => NAME_RENDER, - 'g' => NAME_TARGET, - 'c' => NAME_LINK_CLASS, - 'q' => NAME_QUESTION, - 'e' => NAME_ENCRYPTION, - 'h' => NAME_HASH, - 'U' => NAME_URL_PARAM, - 'R' => NAME_RIGHT_PICTURE_POSITION, + TOKEN_URL => NAME_URL, + TOKEN_MAIL => NAME_MAIL, + TOKEN_PAGE => NAME_PAGE, + TOKEN_TEXT => NAME_TEXT, + TOKEN_ALT_TEXT => NAME_ALT_TEXT, + TOKEN_TOOL_TIP => NAME_TOOL_TIP, + TOKEN_PICTURE => NAME_IMAGE, + TOKEN_BULLET => NAME_IMAGE, + TOKEN_CHECK => NAME_IMAGE, + TOKEN_DELETE => NAME_IMAGE, + TOKEN_EDIT => NAME_IMAGE, + TOKEN_HELP => NAME_IMAGE, + TOKEN_INFO => NAME_IMAGE, + TOKEN_NEW => NAME_IMAGE, + TOKEN_SHOW => NAME_IMAGE, + TOKEN_RENDER => NAME_RENDER, + TOKEN_TARGET => NAME_TARGET, + TOKEN_CLASS => NAME_LINK_CLASS, + TOKEN_QUESTION => NAME_QUESTION, + TOKEN_ENCRYPTION => NAME_ENCRYPTION, + TOKEN_SIP => NAME_SIP, + TOKEN_URL_PARAM => NAME_URL_PARAM, + TOKEN_RIGHT => NAME_RIGHT, + TOKEN_ACTION_DELETE => NAME_ACTION_DELETE, ]; - private $varsDefault = array(); + private $tokenMapping = [ + TOKEN_URL => LINK_ANCHOR, + TOKEN_MAIL => LINK_ANCHOR, + TOKEN_PAGE => LINK_ANCHOR, + TOKEN_PICTURE => LINK_PICTURE, + TOKEN_BULLET => LINK_PICTURE, + TOKEN_CHECK => LINK_PICTURE, + TOKEN_DELETE => LINK_PICTURE, + TOKEN_EDIT => LINK_PICTURE, + TOKEN_HELP => LINK_PICTURE, + TOKEN_INFO => LINK_PICTURE, + TOKEN_NEW => LINK_PICTURE, + TOKEN_SHOW => LINK_PICTURE, + ]; /** * __construct * - * @param $fr_error * @param Sip $sip * @param bool $phpUnit */ - public function __construct($fr_error, Sip $sip, $phpUnit = false) { - $this->fr_error = $fr_error; + public function __construct(Sip $sip, $phpUnit = false) { + $this->phpUnit = $phpUnit; + + if ($phpUnit) { + $_SERVER['REQUEST_URI'] = 'localhost'; + } + $this->sip = $sip; $this->store = Store::getInstance('', $phpUnit); $this->cssLinkClassInternal = $this->store->getVar(SYSTEM_CSS_LINK_CLASS_INTERNAL, STORE_SYSTEM); $this->cssLinkClassExternal = $this->store->getVar(SYSTEM_CSS_LINK_CLASS_EXTERNAL, STORE_SYSTEM); - $this->utils = new Utils(); - - $this->varsDefault[NAME_QUESTION] = 'Please confirm'; -// $this->varsDefault[NAME_PAGE] = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); - $this->varsDefault[NAME_ENCRYPTION] = '0'; - $this->varsDefault[NAME_RIGHT_PICTURE_POSITION] = 'l'; - $this->varsDefault[NAME_RENDER] = 0; - $this->varsDefault[NAME_HASH] = 0; /* * mode: @@ -241,85 +310,20 @@ class Link { * @throws UserReportException */ public function renderLink($str) { - $link = ''; - - $vars = $this->initVars(); - - // str="u:http://www.example.com|c:i|t:delete" - $param = explode("|", $str); - - // Parse all parameter, fill variables - foreach ($param as $item) { - - if ($item == '') { - continue; - } - - // set class defaults - $this->parseItem($vars, $item); - } - - $necessaryDefaults = [NAME_RENDER, NAME_RIGHT_PICTURE_POSITION, NAME_HASH, NAME_ENCRYPTION]; - foreach ($necessaryDefaults AS $keyName) { - if ($vars[$keyName] == '') { - $vars[$keyName] = $this->varsDefault[$keyName]; - } - } - - - // if there is no url or mailto definition: {{global.pageId}} -// if ($vars[NAME_URL] == '' && $vars[NAME_MAIL] == '') { -// $vars[NAME_URL] = "?" . $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); -// } - $this->doCssClass($vars); + $tokenGiven = array(); - // Set default tooltip - if ($vars[NAME_TOOL_TIP] == '' && $vars[NAME_GLYPH_TITLE] !== '') { - $vars[NAME_TOOL_TIP] = $vars[NAME_GLYPH_TITLE]; - } + if (empty($str)) + return ''; - $htmlUrl = $this->doAnchor($vars); - $htmlImage = $this->doHtmlImageGlyph($vars); - -// Compose Image & Text - if ($htmlImage != '') { - $distance = ' '; - $vars[NAME_TEXT] = ($vars[NAME_RIGHT_PICTURE_POSITION] == "l") ? $htmlImage . $distance . $vars[NAME_TEXT] : $vars[NAME_TEXT] . $distance . $htmlImage; - } - -// ToolTip - $extraSpan = ['', '']; - if ($vars[NAME_TOOL_TIP] !== '') { - $extraSpan[0] = "<span " . $vars[NAME_TOOL_TIP_JS][0] . ">" . $vars[NAME_TOOL_TIP_JS][1]; - $extraSpan[1] = "</span>"; - } - -// Create 'fake' modes for encrypted 'mailto' - $prefix = ""; - if ($vars[NAME_MAIL] != '') { - $prefix = "1"; - $vars[NAME_URL] = "dummy"; - } -// Create 'fake' mode for ajax delete - if ($vars[NAME_DELETE]) { - $prefix = "2"; - } -// Compose URL -// get Render Mode via Array renderControl - $modeRender = $vars[NAME_RENDER]; - $modeUrl = $vars[NAME_URL] ? 1 : 0; - $modeText = $vars[NAME_TEXT] ? 1 : 0; + $vars = $this->fillParameter($str, $tokenGiven); + $vars = $this->processParameter($vars); + $mode = $this->getModeRender($vars); - if (isset($this->renderControl[$modeRender][$modeUrl][$modeText])) { - $mode = $prefix . $this->renderControl[$modeRender][$modeUrl][$modeText]; - } else { - throw new UserReportException ("Undefined combination of 'rendering mode' / 'url' / 'text': " . - $modeRender . ' / ' . $modeUrl . ' / ' . $modeText, ERROR_UNDEFINED_RENDER_CONTROL_COMBINATION); - } + $link = ''; -// 0-4 URL, plain email -// 10-14 encrypted email + // 0-4 URL, plain email + // 10-14 encrypted email switch ($mode) { // 0: No Output case '0': @@ -330,293 +334,428 @@ class Link { // 1: 'text' case '1': - $link = $extraSpan[0] . $vars[NAME_TEXT] . $extraSpan[1]; + $link = $vars[FINAL_CONTENT]; break; case '11': - $link = $extraSpan[0] . $this->encryptMailtoJS($vars, false) . $extraSpan[1]; + $link = $vars[FINAL_CONTENT]; +// $link = $this->encryptMailtoJS($vars, false); break; // 2: 'url' case '2': - $link = $extraSpan[0] . $vars[NAME_URL] . $extraSpan[1]; + $link = $vars[FINAL_HREF]; break; case '12': - $link = $extraSpan[0] . $this->encryptMailtoJS($vars, false) . $extraSpan[1]; + $link = $vars[FINAL_HREF]; +// $link = $this->encryptMailtoJS($vars, false); break; - // 3: <a href=url>url</a> + // 3: <a href=url ...>url</a> case '3': - $link = $htmlUrl . $vars[NAME_URL] . '</a>' . $vars[NAME_TOOL_TIP_JS][1]; +// $link = $htmlUrl . $vars[NAME_URL] . '</a>' . $vars[NAME_TOOL_TIP_JS][1]; + $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_HREF]); break; case '13': - $vars[NAME_TEXT] = $vars[NAME_MAIL]; - $link = $this->encryptMailtoJS($vars, TRUE); + $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_HREF]); +// $vars[NAME_TEXT] = $vars[NAME_MAIL]; +// $link = $this->encryptMailtoJS($vars, true); break; - // 4: <a href=url>Text</a> + // 4: <a href=url ...>Text</a> case '4': - $link = $htmlUrl . $vars[NAME_TEXT] . '</a>' . $vars[NAME_TOOL_TIP_JS][1]; + $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_CONTENT]); break; case '14': - $link = $this->encryptMailtoJS($vars, TRUE); + $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_CONTENT]); +// $link = $this->encryptMailtoJS($vars, true); break; case '21': case '22': case '23': case '24': //TODO: Alter Code, umstellen auf JS Client von RO. Vorlage koennte 'Delete' in Subrecord sein. - $link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',hash:'',forward:'" . + $link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',sip:'',forward:'" . $vars[NAME_PAGE] . "'});\" " . $vars[NAME_LINK_CLASS] . ">" . $vars[NAME_TEXT] . "</a>"; } return $link; - } /** - * Cleans the standard vars used every time to render a link. - * + * @param $str + * @param array $tokenGiven * @return array + * @throws UserReportException */ - private function initVars() { + private function fillParameter($str, array &$tokenGiven) { + + $vars = $this->initVars(); + $flagArray = array(); + + // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no" + $param = explode("|", $str); + + // Parse all parameter, fill variables + foreach ($param as $item) { + + if ($item === '') { + continue; + } - $vars = array(); - - $vars[NAME_MAIL] = ''; - $vars[NAME_URL] = ''; - $vars[NAME_PAGE] = ''; - - $vars[NAME_TEXT] = ''; - $vars[NAME_ALT_TEXT] = ''; - $vars[NAME_IMAGE] = ''; - $vars[NAME_IMAGE_TITLE] = ''; - $vars[NAME_GLYPH] = ''; - $vars[NAME_GLYPH_TITLE] = ''; - $vars[NAME_QUESTION] = ''; - $vars[NAME_TARGET] = ''; - $vars[NAME_TOOL_TIP] = ''; - $vars[NAME_TOOL_TIP_JS] = ['', '']; - $vars[NAME_URL_PARAM] = ''; - - $vars[NAME_RENDER] = ''; - $vars[NAME_RIGHT_PICTURE_POSITION] = ''; - $vars[NAME_HASH] = ''; - $vars[NAME_ENCRYPTION] = ''; - $vars[NAME_DELETE] = ''; - - $vars[NAME_LINK_CLASS] = ''; // class name - $vars[NAME_LINK_CLASS_DEFAULT] = ''; // Depending of 'as page' or 'as url'. Only used if class is not explizit set. + // u:www.example.com + $arr = explode(":", $item, 2); + $key = isset($arr[0]) ? $arr[0] : ''; + $value = isset($arr[1]) ? $arr[1] : ''; + + if (isset($tokenGiven[$key])) { + throw new UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION); + } + $tokenGiven[$key] = true; + + if (!isset($this->tableVarName[$key])) { + throw new UserReportException ("Unknown link qualifier: '$key' - do you forget the one character qualifier?", ERROR_UNKNOWN_LINK_QUALIFIER); + } + $keyName = $this->tableVarName[$key]; // convert token to name + + if ($value === '') { + $value = $this->checkEmptyValue($key, $value); + } + $value = $this->checkValue($key, $value); + + // Store value + $vars[$keyName] = $value; + + // Check for double anchor or picture definition + if (isset($this->tokenMapping[$key])) { + $type = $this->tokenMapping[$key]; + + if (isset($flagArray[$type])) { + throw new UserReportException ("Multiple definitions of url/mail/page or picture", ERROR_MULTIPLE_DEFINITION); + } + $flagArray[$type] = true; + +// if ($type === LINK_PICTURE) { +// $build = 'build' . strtoupper($keyName[0]) . substr($keyName, 1); +// $this->$build($vars, $value); +// } + } + + if (isset($this->callTable[$key])) { + $build = $this->callTable[$key]; + $vars = $this->$build($vars, $value); + } + } + + // Final Checks + $this->checkParam($tokenGiven); return $vars; } /** - * Parse Item of link string, fill class global variables. + * Cleans and make existing the standard vars used every time to render a link. * - * @param array $vars - * @param $item - * @throws CodeException - * @throws UserReportException + * @return array */ - private function parseItem(array &$vars, $item) { + private function initVars() { - $arr = explode(":", $item, 2); - $key = isset($arr[0]) ? $arr[0] : ''; - $value = isset($arr[1]) ? $arr[1] : ''; + return [ + NAME_MAIL => '', + NAME_URL => '', + NAME_PAGE => '', + + NAME_TEXT => '', + NAME_ALT_TEXT => '', + NAME_IMAGE => '', + NAME_IMAGE_TITLE => '', + NAME_GLYPH => '', + NAME_GLYPH_TITLE => '', + NAME_QUESTION => '', + NAME_TARGET => '', + NAME_TOOL_TIP => '', + NAME_TOOL_TIP_JS => '', + NAME_URL_PARAM => '', + + NAME_RENDER => '0', + NAME_RIGHT => 'l', + NAME_SIP => '0', + NAME_ENCRYPTION => '0', + NAME_DELETE => '', + + NAME_LINK_CLASS => '', // class name + NAME_LINK_CLASS_DEFAULT => '', // Depending of 'as page' or 'as url'. Only used if class is not explizit set. + + NAME_ACTION_DELETE => '', + + FINAL_HREF => '', + FINAL_CONTENT => '', + FINAL_SYMBOL => '', + FINAL_TOOL_TIP => '', + FINAL_CLASS => '', + FINAL_QUESTION => '' + ]; - // Superclass - if ($key === 'A' || $key === 'G') { - $this->parseItem($vars, $value); - return; - } + } - if (!isset($this->tableVarName[$key])) { - throw new UserReportException ("Unknown link qualifier: '$key'", ERROR_UNKNOWN_LINK_QUALIFIER); + /** + * Verify Empty values. If appropriate, set defaults, if not throw anexception. + * + * @param $key + * @param $value + * @return string + * @throws UserReportException + */ + private function checkEmptyValue($key, $value) { + switch ($key) { + case TOKEN_URL: + throw new UserReportException ("Missing value for token '$key'", ERROR_MISSING_VALUE); + break; + case TOKEN_MAIL: + throw new UserReportException ("Missing value for token '$key'", ERROR_MISSING_VALUE); + break; + case TOKEN_PAGE: + $value = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); // If no pageid|pagealias is defined, take current page + break; + case TOKEN_RIGHT: + $value = 'r'; + break; + case TOKEN_ENCRYPTION: + $value = '1'; + break; + case TOKEN_SIP: + $value = '1'; + break; + case TOKEN_QUESTION: + $value = DEFAULT_QUESTION_TEXT; + break; + case TOKEN_RENDER: + $value = DEFAULT_RENDER_MODE; + break; + case TOKEN_ACTION_DELETE: + $value = DEFAULT_ACTION_DELETE; + break; + default: } - $keyName = $this->tableVarName[$key]; + return $value; + } - // A few keys do not have necessarily a value: fake the definition by manual creating a value. - if ($value == '') { - switch ($key) { - case 'R': - $value = 'r'; - break; - case 'h': - $value = '1'; - break; - case 'p': - $value = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3); // If no pageid|pagealias is defined, take current page - break; - default: - break; - } + /** + * Validate value for token + * + * @param $key + * @param $value + * @return mixed + * @throws UserReportException + */ + private function checkValue($key, $value) { + switch ($key) { + case TOKEN_ENCRYPTION: + case TOKEN_SIP: + if ($value !== '0' && $value !== '1') { + throw new UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE); + } + break; + case TOKEN_ACTION_DELETE: + switch ($value) { + case TOKEN_ACTION_DELETE_AJAX: + case TOKEN_ACTION_DELETE_REPORT: + case TOKEN_ACTION_DELETE_CLOSE: + break; + default: + throw new UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE); + } + break; + default: } - // Defaults - if ($value === '' && isset($this->varsDefault[$keyName])) { - $value = $this->varsDefault[$keyName]; - } + return $value; + } - // Check for empty values. Respect: some keys are allowed to be empty. - if ($value === '' && strpos('uENDHIS', $key) === false) { - throw new UserReportException ("Missing value for '$key'", ERROR_MISSING_VALUE); + /** + * Check for double definition. + * + * @param array $tokenGiven + * @throws UserReportException + */ + private function checkParam(array $tokenGiven) { + $countLinkAnchor = 0; + $countLinkPicture = 0; + + foreach ($tokenGiven as $token => $value) { + if (isset($this->tokenMapping[$token])) { + switch ($this->tokenMapping[$token]) { + case LINK_ANCHOR: + $countLinkAnchor++; + break; + case LINK_PICTURE: + $countLinkPicture++; + break; + default: + break; + } + } } - // Value already defined? - if ($vars[$keyName] !== '') { - throw new UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION); + if ($countLinkAnchor > 1) { + throw new UserReportException ("Multiple URL / PAGE and MAILTO definition", ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION); } - // Store value - $vars[$keyName] = $value; - - if ($vars[NAME_URL] && $vars[NAME_MAIL]) { - throw new UserReportException ("Multiple URL / PAGE and MAILTO definition", ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION); + if ($countLinkPicture > 1) { + throw new UserReportException ("Multiple definitions for token picture/bullet/check/edit...delete'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION); } - // Call special function - if (isset($this->callTable[$key])) { - $call = $this->callTable[$key]; - $this->$call($vars, $keyName, $value); + if (isset($tokenGiven[TOKEN_MAIL]) && isset($tokenGiven[TOKEN_TARGET])) { + throw new UserReportException ("Token Mail and Target at the same time not possible'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION); } } /** - * Parse CSS Class Settings + * Compute final link parameter. * - * @param array $vars - * @return void + * @param $vars + * @return string + * @throws UserReportException */ - private function doCssClass(array &$vars) { - $class = ''; + private function processParameter($vars) { - switch ($vars[NAME_LINK_CLASS]) { - case 'n': - $vars[NAME_LINK_CLASS] = ''; - break; - case 'i': - case 'e': - $vars[NAME_LINK_CLASS] = $this->linkClassSelector[$vars[NAME_LINK_CLASS]]; - break; - default: - $vars[NAME_LINK_CLASS] = ($vars[NAME_LINK_CLASS] == '') ? $vars[NAME_LINK_CLASS_DEFAULT] : $vars[NAME_LINK_CLASS]; - break; - } + $vars[FINAL_HREF] = $this->doHref($vars); // must be called before doToolTip() + $vars[FINAL_TOOL_TIP] = $this->doToolTip($vars); + $vars[FINAL_CLASS] = $this->doCssClass($vars); + $vars[FINAL_SYMBOL] = $this->doSymbol($vars); + $vars[FINAL_CONTENT] = $this->doContent($vars); // must be called after doSymbol() + $vars[FINAL_QUESTION] = $this->doQuestion($vars); + $vars[FINAL_ANCHOR] = $this->doAnchor($vars); - if ($vars[NAME_LINK_CLASS] === NO_CLASS) { - $vars[NAME_LINK_CLASS] = ''; - } + return $vars; } /** - * Create the HTML anchor. - * - <a href="mailto:info@example.com" title=".." class=".."> - * - <a href="http://example.com" title=".." class=".."> - * - If $this->modeHash is set, create a hash (sip) + * Concat final HREF string * * @param array $vars * @return string * @throws UserReportException */ - private function doAnchor(array &$vars) { - $attributes = ''; - // build URL - $anchorTitle = ''; + private function doHref(array &$vars) { + $urlNParam = ''; - // Link: URL - if ($vars[NAME_URL] !== '') { + if ($vars[NAME_ACTION_DELETE] !== '') { + $vars[NAME_URL_PARAM] = $this->adjustDeleteParameter($vars[NAME_ACTION_DELETE], $vars[NAME_URL_PARAM]); + } - if ($vars[NAME_HASH] === "1") { + if ($vars[NAME_MAIL] === '') { - $paramArray = $this->sip->queryStringToSip(Support::concatUrlParam($vars[NAME_URL], $vars[NAME_URL_PARAM]), RETURN_ARRAY); - $vars[NAME_URL] = $paramArray['_url']; - if ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes') { + if (substr($vars[NAME_URL], 0, 1) === '?') { + $vars[NAME_URL] = INDEX_PHP . $vars[NAME_URL]; + } - $vars[NAME_TOOL_TIP] .= PHP_EOL . PHP_EOL . OnArray::toString($paramArray, ' = ', PHP_EOL, "'"); - $this->buildToolTip($vars, 'o', $vars[NAME_TOOL_TIP]); - } + // Either NAME_URL is empty or NAME_PAGE is empty + $urlNParam = Support::concatUrlParam($vars[NAME_URL] . $vars[NAME_PAGE], $vars[NAME_URL_PARAM]); - } else { + if ($vars[NAME_SIP] === "1") { + $paramArray = $this->sip->queryStringToSip($urlNParam, RETURN_ARRAY); + $urlNParam = $paramArray['_url']; - if ($vars[NAME_URL_PARAM] != '') { - $vars[NAME_URL] = Support::concatUrlParam($vars[NAME_URL], $vars[NAME_URL_PARAM]); + if ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes') { + $vars[NAME_TOOL_TIP] .= PHP_EOL . PHP_EOL . OnArray::toString($paramArray, ' = ', PHP_EOL, "'"); } } - } - - // Link: MAILTO - if ($vars[NAME_MAIL] !== '') { - + // Link: MAILTO + } else { // If there is no encryption: handle the mailto as an ordinary URL - if ($vars[NAME_ENCRYPTION] === '0') { - $vars[NAME_URL] = "mailto:" . $vars[NAME_MAIL]; - $vars[NAME_MAIL] = ""; - } else { + if ($vars[NAME_ENCRYPTION] === '1') { throw new UserReportException ("Oops, sorry: encrypted mail not implemented ...", ERROR_NOT_IMPLEMENTED); + } else { + $urlNParam = "mailto:" . $vars[NAME_MAIL]; } } - if ($vars[NAME_GLYPH] !== '') { - $vars[NAME_LINK_CLASS] .= ' btn btn-default '; + return $urlNParam; + } -// if ($vars[NAME_GLYPH_TITLE] !== '') { -// $anchorTitle = $vars[NAME_GLYPH_TITLE]; -// } - } + /** + * @param $tokenActionDelete + * @param $nameUrlParam + * @return string + * @throws UserReportException + */ + private function adjustDeleteParameter($tokenActionDelete, $nameUrlParam) { - $attributes .= Support::doAttribute('href', $vars[NAME_URL]); - $attributes .= Support::doAttribute('class', $vars[NAME_LINK_CLASS]); - $attributes .= Support::doAttribute('target', $vars[NAME_TARGET]); -// $attributes .= Support::doAttribute('title', $anchorTitle); - $attributes .= Support::doAttribute('title', $vars[NAME_TOOL_TIP]); + $kvp = new KeyValueStringParser(); - if ($vars[NAME_QUESTION]) { - $attributes .= Support::doAttribute('onclick', 'confirm(\'' . $vars[NAME_QUESTION] . '\')'); - } + // Split in: [p => 'r=100&table=note&..', 'D' => ''... ], +// $param = $kvp->parse($nameUrlParam, ':', '|'); -// $anchor = '<a ' . $attributes . $vars[NAME_TOOL_TIP_JS][0] . '>'; - $anchor = '<a ' . $attributes . '>'; +// Support::setIfNotSet($param, TOKEN_URL_PARAM); - return ($anchor); + switch ($tokenActionDelete) { + case TOKEN_ACTION_DELETE_AJAX: + // TODO: Implement for AJAX (subrecord) + throw new UserReportException ("Not implemented!", ERROR_NOT_IMPLEMENTED); + break; + case TOKEN_ACTION_DELETE_REPORT: + $nameUrlParam .= '&' . SIP_MODE_ANSWER . '=' . MODE_HTML; + $nameUrlParam .= '&' . SIP_TARGET_URL . '=' . $_SERVER['REQUEST_URI']; + break; + case TOKEN_ACTION_DELETE_CLOSE: + // TODO: Implement for Form (primary Record wird geloescht) + throw new UserReportException ("Not implemented!", ERROR_NOT_IMPLEMENTED); + break; + default: + throw new UserReportException ("Invalid value for token '" . TOKEN_ACTION_DELETE . "': '$tokenActionDelete''", ERROR_INVALID_VALUE); + } + + return $nameUrlParam; } - // -//<a class="btn btn-default" href="index.php?id=2&s=56fbdd30a0cd2" title='comment'> -// <span class='glyphicon glyphicon-plus'></span> -//</a> -// -//<a href="index.php?id=2&s=56fb7f7703692" class="internal" > -// <img src="typo3conf/ext/qfq/Resources/Public/icons/edit.gif" title="Edit" > -//</a> + /** + * Return $vars[NAME_TOOL_TIP]. If $vars[NAME_TOOL_TIP] is empty, set $vars[NAME_GLYPH_TITLE] as tooltip. + * + * @param array $vars + * @return mixed + */ + private function doToolTip(array $vars) { + + // Set default tooltip + if ($vars[NAME_TOOL_TIP] == '') { + $vars[NAME_TOOL_TIP] = $vars[NAME_GLYPH_TITLE]; + } + + return $vars[NAME_TOOL_TIP]; + } /** - * Create a ToolTip: $toolTip[0] and $toolTip[1] have to inserted in HTML code accordingly. - * $vars[NAME_TOOL_TIP_JS][0]: JS to show '$toolTip[1]'. - * $vars[NAME_TOOL_TIP_JS][1]: '<span>...</span>' with the tooltip text. + * Parse CSS Class Settings * - * @param $vars - * @param $key - * @param $value + * @param array $vars + * @return string */ - private function buildToolTip(&$vars, $key, $value) { - static $count = 0; + private function doCssClass(array $vars) { - $toolTipIndex = 'tooltip.' . $GLOBALS["TSFE"]->currentRecord . '.' . ++$count; - $vars[NAME_TOOL_TIP_JS] = array(); + $class = ($vars[NAME_LINK_CLASS] === '') ? $vars[NAME_LINK_CLASS_DEFAULT] : $vars[NAME_LINK_CLASS]; - // Expample: <img src="fileadmin/icons/bullet-gray.gif" onmouseover="document.getElementById('gm167979').style. - // display='block';" onmouseout="document.getElementById('gm167979').style.display='none';" /> - $vars[NAME_TOOL_TIP_JS][0] = " onmouseover=\"document.getElementById('" . $toolTipIndex . - "').style.display='block';\" onmouseout=\"document.getElementById('" . $toolTipIndex . "').style.display='none';\""; + switch ($class) { + case TOKEN_CLASS_NONE: + $class = ''; + break; + case TOKEN_CLASS_INTERNAL: + case TOKEN_CLASS_EXTERNAL: + $class = $this->linkClassSelector[$vars[NAME_LINK_CLASS]] . ' '; + break; + default: + break; + } - // Example: <span id="gm167979" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0; - // padding:3px;">My pesonal tooltip</span> - $vars[NAME_TOOL_TIP_JS][1] = '<span id="' . $toolTipIndex . - '" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0; padding:3px;">' . - $value . '</span>'; + if ($class === NO_CLASS) { + $class = ''; + } + + if ($vars[NAME_GLYPH] !== '') { + $class .= 'btn btn-default '; + } - return; + return $class; } /** @@ -625,7 +764,7 @@ class Link { * @param array $vars * @return string */ - private function doHtmlImageGlyph(array $vars) { + private function doSymbol(array $vars) { $tags = ''; $html = ''; @@ -633,7 +772,7 @@ class Link { if ($vars[NAME_IMAGE] !== '') { $tags .= Support::doAttribute('alt', $vars[NAME_ALT_TEXT]); $tags .= Support::doAttribute('src', $vars[NAME_IMAGE]); - $tags .= Support::doAttribute('title', $vars[NAME_IMAGE_TITLE]); + $tags .= Support::doAttribute('title', $vars[FINAL_TOOL_TIP] === '' ? $vars[NAME_IMAGE_TITLE] : $vars[FINAL_TOOL_TIP]); $html .= '<img ' . $tags . '>'; } @@ -645,6 +784,139 @@ class Link { return $html; } + /** + * Concat Text and/or Image and or Glyph. $vars[NAME_RIGHT_PICTURE_POSITION] + * Concat Text and/or Image and or Glyph. Depending of $vars[NAME_RIGHT_PICTURE_POSITION], swapp the position, + * + * @param array $vars + * @return string + */ + private function doContent(array $vars) { + + $htmlImage = $vars[FINAL_SYMBOL]; + + // Compose Image & Text + if ($htmlImage === '') { + $content = $vars[NAME_TEXT]; + } elseif ($vars[NAME_TEXT] === '') { + $content = $htmlImage; + } else { + if ($vars[NAME_RIGHT] === "l") { + $content = implode(' ', [$htmlImage, $vars[NAME_TEXT]]); + } else { + $content = implode(' ', [$vars[NAME_TEXT], $htmlImage]); + } + } + + return $content; + } + + /** + * Build 'alert' JS. $vars['question'] = '<text>:<color>:<button ok>:<button fail>' + * Return JS Code to place in '<a>' tag. Be carefull: function creates a uniqe 'id' tag. + * + * @param array $vars + * @return string + */ + private function doQuestion(array $vars) { + + if ($vars[NAME_QUESTION] === '') { + return ''; + } + + $arr = OnArray::explodeWithoutEscaped(':', $vars[NAME_QUESTION]); + $arr = array_merge($arr, ['', '', '', '', '', '']); + + $id = ($this->phpUnit === true) ? '12345' : uniqid('a_'); + $content = Support::doAttribute('id', $id); + + $text = $arr[QUESTION_INDEX_TEXT] === '' ? DEFAULT_QUESTION_TEXT : $arr[QUESTION_INDEX_TEXT]; + $level = ($arr[QUESTION_INDEX_LEVEL] === '') ? DEFAULT_QUESTION_LEVEL : $arr[QUESTION_INDEX_LEVEL]; + $ok = ($arr[QUESTION_INDEX_BUTTON_OK] === '') ? 'Ok' : $arr[QUESTION_INDEX_BUTTON_OK]; + $cancel = ($arr[QUESTION_INDEX_BUTTON_FALSE] === '') ? 'Cancel' : $arr[QUESTION_INDEX_BUTTON_FALSE]; + $timeout = ($arr[QUESTION_INDEX_TIMEOUT] === '') ? '0' : $arr[QUESTION_INDEX_TIMEOUT] * 1000; + $flagModalStatus = ($arr[QUESTION_INDEX_FLAG_MODAL] === '') ? '1' : $arr[QUESTION_INDEX_FLAG_MODAL]; + $flagModal = ($flagModalStatus === "1") ? 'true' : 'false'; + + $js = <<<EOF +var alert = new QfqNS.Alert({ message: '$text', type: '$level', modal: $flagModal, timeout: $timeout, buttons: [ + { label: '$ok', eventName: 'ok' }, + { label: '$cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#$id').attr('href'); +}); + +alert.show(); +return false; +EOF; + + $content .= Support::doAttribute('onClick', $js); + + + return $content; + } + + /** + * Create the HTML anchor. + * - <a href="mailto:info@example.com" title=".." class=".."> + * - <a href="http://example.com" title=".." class=".."> + * + * @param array $vars + * @return string + * @throws UserReportException + */ + private function doAnchor(array $vars) { + + $attributes = ''; + + + $attributes .= Support::doAttribute('href', $vars[FINAL_HREF]); + $attributes .= Support::doAttribute('class', $vars[FINAL_CLASS]); + $attributes .= Support::doAttribute('target', $vars[NAME_TARGET]); + $attributes .= Support::doAttribute('title', $vars[FINAL_TOOL_TIP]); + $attributes .= $vars[FINAL_QUESTION]; + + $anchor = '<a ' . $attributes . '>'; + return $anchor; + } + + /** + * Get mode from table $this->renderControl, depending on $modeRender, $modeUrl, $modeText + * + * @param array $vars + * @param string $prefix + * @return string + * @throws UserReportException + */ + private function getModeRender(array $vars, $prefix = '') { + + $modeRender = $vars[NAME_RENDER]; + $modeUrl = ($vars[FINAL_HREF] === '') ? 0 : 1; + $modeText = ($vars[FINAL_CONTENT] === '') ? 0 : 1; + + // Create 'fake' modes for encrypted 'mailto' + $prefix = ""; + if ($vars[NAME_MAIL] != '') { + $prefix = "1"; +// $vars[NAME_URL] = "dummy"; + } + + // Create 'fake' mode for ajax/html delete + if ($vars[NAME_DELETE]) { + $prefix = "2"; + } + + if (isset($this->renderControl[$modeRender][$modeUrl][$modeText])) { + $mode = $prefix . $this->renderControl[$modeRender][$modeUrl][$modeText]; + } else { + throw new UserReportException ("Undefined combination of 'rendering mode' / 'url' / 'text': " . + $modeRender . ' / ' . $modeUrl . ' / ' . $modeText, ERROR_UNDEFINED_RENDER_CONTROL_COMBINATION); + } + + return $mode; + } + /** * Encrypt the mailto address via JS. * Email address protected against email crawler (as long as they don't interpret JS). @@ -662,7 +934,7 @@ class Link { * @param bool|TRUE $href TRUE: create a '<a>', false: just encrypt or show the email, no link. * @return string */ - private function encryptMailtoJS(array $vars, $href = TRUE) { + private function encryptMailtoJS(array $vars, $href = true) { // Split $mailto $tmp = $this->splitAndAddDelimter($vars[NAME_MAIL], "@"); @@ -732,186 +1004,288 @@ class Link { * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildUrl(&$vars, $key, $value) { + private function buildUrl($vars, $value) { $vars[NAME_LINK_CLASS_DEFAULT] = $this->cssLinkClassExternal; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildMail(&$vars, $key, $value) { + private function buildMail($vars, $value) { $vars[NAME_LINK_CLASS_DEFAULT] = $this->cssLinkClassExternal; - $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array * @throws UserReportException */ - private function buildPage(&$vars, $key, $value) { - - if ($vars[NAME_URL] != '') { - throw new UserReportException ("Multiple definitions for key 'p'", ERROR_MULTIPLE_DEFINITION); - } + private function buildPage($vars, $value) { if (substr($value, 0, 3) !== 'id=') { $value = 'id=' . $value; } - $vars[NAME_URL] = "?" . $value; + $vars[NAME_PAGE] = Support::concatUrlParam('', $value); $vars[NAME_LINK_CLASS_DEFAULT] = $this->cssLinkClassInternal; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildPicture(&$vars, $key, $value) { - $vars[NAME_ALT_TEXT] = "Grafic: " . $value; + private function buildPicture($vars, $value) { + if ($vars[NAME_ALT_TEXT] == '') { + $vars[NAME_ALT_TEXT] = $value; + } + $vars[NAME_IMAGE_TITLE] = $value; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildBullet(&$vars, $key, $value) { - $vars[NAME_IMAGE] = PATH_ICONS . "bullet-" . $value . '.gif'; + private function buildBullet($vars, $value) { + if ($value == '') { + $value = DEFAULT_BULLET_COLOR; + } + + if ($vars[NAME_ALT_TEXT] == '') { + $vars[NAME_ALT_TEXT] = "Bullet " . $value; + } + + $vars[NAME_IMAGE] = PATH_ICONS . "/bullet-" . $value . '.gif'; $vars[NAME_IMAGE_TITLE] = $value; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildCheck(&$vars, $key, $value) { - $vars[NAME_IMAGE] = PATH_ICONS . "checked-" . $value . '.gif'; + private function buildCheck($vars, $value) { + if ($value == '') { + $value = DEFAULT_CHECK_COLOR; + } + + if ($vars[NAME_ALT_TEXT] == '') { + $vars[NAME_ALT_TEXT] = "Checked " . $value; + } + + $vars[NAME_IMAGE] = PATH_ICONS . "/checked-" . $value . '.gif'; $vars[NAME_IMAGE_TITLE] = $value; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key - * @param $value + * @return array */ - private function buildDelete(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'delete.gif'; -// $vars[NAME_IMAGE_TITLE] = "Delete"; - $vars[NAME_DELETE] = true; + private function buildDeleteIcon($vars) { $vars[NAME_GLYPH] = GLYPH_ICON_DELETE; $vars[NAME_GLYPH_TITLE] = "Delete"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; - // Include Extjs library - $this->utils->loadJSlib($this->fr_error); + return $vars; + } + + /** + * Called by $this->callTable + * + * @param $vars + * @param $value + * @return array + * @throws UserReportException + */ + private function buildActionDelete($vars, $value) { + + // Minimal check for required parameter. + $this->checkDeleteParam($vars[NAME_URL_PARAM]); + + if ($vars[NAME_URL] == '') { + $vars[NAME_URL] = API_DIR . '/' . API_DELETE_PHP; + } + + if (!isset($vars[NAME_LINK_CLASS])) { + // no_class: By default a button will be rendered. NAME_URL typically implies class external. That does not match. + $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + } + + $vars[NAME_SIP] = "1"; + + return $vars; + } + + /** + * Check that at least SIP_RECORD_ID is given and SIP_TABLE or SIP_FORM. + * This check is only processed for COLUMN_PAGED & COLUMN_PPAGED. Not for COLOUMN_LINK, cause it's not known there. + * In case of missing parameter, throw an exception. + * + * @param $urlParam + * @throws UserReportException in case parameter is missing. + */ + private function checkDeleteParam($urlParam) { + + // Fill array 'found' with every given token + $found = KeyValueStringParser::parse($urlParam, '=', '&'); + + $flagRecordId = isset($found[SIP_RECORD_ID]) && $found[SIP_RECORD_ID] != '' && $found[SIP_RECORD_ID] > 0; + $flagTable = isset($found[SIP_TABLE]) && $found[SIP_TABLE] != ''; + $flagForm = isset($found[SIP_FORM]) && $found[SIP_FORM] != ''; + + if ($flagRecordId && ($flagTable || $flagForm)) { + return; + } + + throw new UserReportException ("Missing some qualifier/value for column " . COLUMN_PAGED . '/' . COLUMN_PPAGED . ": " . + SIP_RECORD_ID . ", " . SIP_FORM . " or " . SIP_TABLE, ERROR_MISSING_REQUIRED_DELETE_QUALIFIER); + } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildEdit(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'edit.gif'; -// $vars[NAME_IMAGE_TITLE] = "Edit"; + private function buildEdit($vars, $value) { + $vars[NAME_GLYPH] = GLYPH_ICON_EDIT; $vars[NAME_GLYPH_TITLE] = "Edit"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildHelp(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'help.gif'; -// $vars[NAME_IMAGE_TITLE] = "Help"; - $vars[NAME_GLYPH] = GLYPH_ICON_HELP; + private function buildHelp($vars, $value) { + + $vars[NAME_GLYPH] = 'glyphicon ' . GLYPH_ICON_HELP; $vars[NAME_GLYPH_TITLE] = "Help"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildInfo(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'info.gif'; -// $vars[NAME_IMAGE_TITLE] = "Information"; + private function buildInfo($vars, $value) { - $vars[NAME_GLYPH] = GLYPH_ICON_INFO; + $vars[NAME_GLYPH] = 'glyphicon ' . GLYPH_ICON_INFO; $vars[NAME_GLYPH_TITLE] = "Information"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildNew(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'new.gif'; -// $vars[NAME_IMAGE_TITLE] = "New"; + private function buildNew($vars, $value) { $vars[NAME_GLYPH] = GLYPH_ICON_NEW; $vars[NAME_GLYPH_TITLE] = "New"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + return $vars; } /** * Called by $this->callTable * * @param $vars - * @param $key * @param $value + * @return array */ - private function buildShow(&$vars, $key, $value) { -// $vars[NAME_IMAGE] = PATH_ICONS . 'show.gif'; -// $vars[NAME_IMAGE_TITLE] = "Details"; + private function buildShow($vars, $value) { - $vars[NAME_GLYPH] = GLYPH_ICON_SHOW; + $vars[NAME_GLYPH] = 'glyphicon ' . GLYPH_ICON_SHOW; $vars[NAME_GLYPH_TITLE] = "Details"; $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS; + return $vars; } + /** + * Create a ToolTip: $toolTip[0] and $toolTip[1] have to inserted in HTML code accordingly. + * $vars[NAME_TOOL_TIP_JS][0]: JS to show '$toolTip[1]'. + * $vars[NAME_TOOL_TIP_JS][1]: '<span>...</span>' with the tooltip text. + * + * @param $vars + * @param $value + * @return array + */ + private function buildToolTip($vars, $value) { + static $count = 0; + +// $toolTipIndex = 'tooltip.' . $GLOBALS["TSFE"]->currentRecord . '.' . ++$count; + $toolTipIndex = 'fake'; + + $vars[NAME_TOOL_TIP_JS] = array(); + + // Expample: <img src="fileadmin/icons/bullet-gray.gif" onmouseover="document.getElementById('gm167979').style. + // display='block';" onmouseout="document.getElementById('gm167979').style.display='none';" /> + $vars[NAME_TOOL_TIP_JS][0] = " onmouseover=\"document.getElementById('" . $toolTipIndex . + "').style.display='block';\" onmouseout=\"document.getElementById('" . $toolTipIndex . "').style.display='none';\""; + + // Example: <span id="gm167979" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0; + // padding:3px;">My pesonal tooltip</span> + $vars[NAME_TOOL_TIP_JS][1] = '<span id="' . $toolTipIndex . + '" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0; padding:3px;">' . + $value . '</span>'; + + return $vars; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/report/Log.php b/extension/qfq/qfq/report/Log.php deleted file mode 100644 index 8d2e282248f33954675971b9bee39009587bc1d5..0000000000000000000000000000000000000000 --- a/extension/qfq/qfq/report/Log.php +++ /dev/null @@ -1,214 +0,0 @@ -<?php -/*************************************************************** - * Copyright notice - * - * (c) 2010 Glowbase GmbH <support@glowbase.com> - * All rights reserved - * - * This script is part of the TYPO3 project. The TYPO3 project is - * free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * The GNU General Public License can be found at - * http://www.gnu.org/copyleft/gpl.html. - * - * This script is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * This copyright notice MUST APPEAR in all copies of the script! - ***************************************************************/ - -/* - * Howto - * - * 1) Specify Logfiles (incl. path) in ext_localconf.php in $TYPO3_CONF_VARS[$_EXTKEY]['log'][<name>]. - * <name> means a class of 'error', 'mail', 'sql', 'browser', ... - * 2) Call 'log_do($class, $status, $origin, $message)' to write one entry. - * Every entry will start with: [<status>|<origin>|<date/time>|<IP>|<fe.uid>|<fe.username>|<URL>]. - * Specify $message string or $message array. - * 3) For 'sql' and 'mail' log entries, use the wrapper log_sql() or log_mail(). - */ - -namespace qfq; - -//use qfq; - -class Log { - /** - * @var array - */ - private $config; // filled in __construct, Definition: tx_fr_variables->collectGlobalVariables - - /** - * @var array order to compare loglevel - */ - private $config_level = array('E' => 0, 'W' => 1, 'I' => 2, 'D1' => 3, 'D2' => 4, 'D3' => 5); - - /** - * @var array Emulate global variable: will be set much earlier in other functions. Will be shown in error messages. - */ - private $fr_error = array(); - - /** - * Initializes class - * - * @param array $tmp_config : Part of array 'fr_array': 'global.' - * - * @throws \Exception - */ - public function __construct(array $tmp_config) { - global $TYPO3_CONF_VARS; - $this->prepare_log_file_dir($TYPO3_CONF_VARS[EXTKEY]['log']['sql']); - $this->prepare_log_file_dir($TYPO3_CONF_VARS[EXTKEY]['log']['mail']); - $this->prepare_log_file_dir($TYPO3_CONF_VARS[EXTKEY]['log']['error']); - $this->prepare_log_file_dir($TYPO3_CONF_VARS[EXTKEY]['log']['browser']); - - $this->config = $tmp_config; - - } // __construct() - - /** - * Check if path of logfile exists. No: create it. - * Check if path of logfile is writeable. - * - * @param string $filename Name of logfile - * @throws \Exception - */ - private function prepare_log_file_dir($filename) { - $this->logFile = $filename; - - // extract optional path component - $path = dirname($filename); - - // If there is a path: check if directories really exist and are writeable. - if ($path) { - if (file_exists($path)) { - // Directory already exists: check if it is writeable. - if (!is_writable($path)) throw new \Exception ("Failed: directory '$path' of Logfile '$filename' is not writeable."); - } else { - // Directoy doesn't exist: create it. - if (!mkdir($path, 0775, true)) - throw new \Exception ("Failed to create directory '$path' for Logfile '$filename'"); - } - } - } - - /** - * Set Array fr_error: setter function to set most recent values. - * Will be shown in log messages. - * - * @param array $fr_error : uid, pid, row, column_idx, full_level - * - * @return void - */ - public function set_fr_error(array $fr_error) { - $this->fr_error = $fr_error; - } // prepare_log_file_dir() - - /** - * log_sql: wrapper for log_do() for sql logging - * - * @param string $origin : Sender of the logmessage. F.e.: 'form', 'save', 'report', 'extjs', ... - * @param string $msg : Only filled if there was an error - * @param int $affected_rows : Number of rows inserted, updated or deleted. - * @param int $new_id : last_insert_id() - * @param string $sql : SQL statement fired - * - * @return void - */ - public function log_sql($origin, $msg, $affected_rows, $new_id, $sql) { - - if ($msg) { - $status = 'E'; - } else { - $status = 'I'; - $msg = 'OK'; - } - - $text = "[$msg]"; - $text .= "[$affected_rows]"; - $text .= "[$new_id]"; - $text .= "[$sql]"; - - $this->log_do('sql', $status, $origin, $text); - } // log_do() - - /** - * log_do: format log entries, build fix part, append dynamic part, write. - * - * @param string $class : 'error', 'mail', 'sql', 'browser'. New classes has to be defined in ext_localconf.php - * @param string $status : 'E' (0:error), 'W' (1:warning), 'I' (2:information), 'D1' (3:debug verbose), 'D2' (4:debug very verbose), 'D3' (5:debug very very verbose) - * @param string $orign : Sender of the logmessage. F.e.: 'form', 'save', 'report', 'extjs', ... - * @param string /array $message: skalar or one dimensal array with logmessage(s). - * @throws CodeReportException - * @throws SyntaxReportException - */ - public function log_do($class, $status, $origin, $message) { - global $TYPO3_CONF_VARS; - $tmp = ''; - - // Check if loglevel should be respected and if 'yes' if loglevel is greater than this message: break - if (($class == 'error' || $class == 'browser') && $this->config_level[$status] > $TYPO3_CONF_VARS[EXTKEY]['log']['level']) - return; - - // Get filename - $filename = $TYPO3_CONF_VARS[EXTKEY]['log'][$class]; - if (!$filename) - throw new SyntaxReportException ("Missing logfile definition: Undefind TYPO3_CONF_VARS[" . EXTKEY . "]['log']['$class']", "", __FILE__, __LINE__); - - if ($this->fr_error['pid']) $tmp = '|' . $this->fr_error['pid']; - if ($this->fr_error['uid']) $tmp .= '|' . $this->fr_error['uid']; - if ($this->fr_error['full_level']) $tmp .= '|' . $this->fr_error['full_level']; - - // F.e.: [I|Form|2012.01.31-19:59:33|IP|fe.uid|fe.username|URL] - $text = '[' . $status . '|' . $origin . '|' . date('Y.m.d-H:i:s') . '|' . $this->config["REMOTE_ADDR"] . '|' . $this->config["fe_user_uid"] . '|' . $this->config["fe_user"] . $tmp . '|' . $this->config["url"] . ']'; - - // If 'message' is an array, wrap every element (but not the last) with '[', ']'. - if ($message && is_array($message)) { - $last = array_pop($message); - if ($last && $message) { - $text .= '[' . implode('][', $message) . ']'; - } - $message = $last; - } - $text .= $message; - - // Write whole entry - $this->log_write($filename, $text); - } // log_sql() - - /** - * Logs every email (failed or successfull) - * - * @param string $orign : Sender of the logmessage. F.e.: 'form', 'save', 'report', 'extjs', ... - * @param string $status : 'E' (0:error), 'W' (1:warning), 'I' (2:information), 'D1' (3:debug verbose), 'D2' (4:debug very verbose), 'D3' (5:debug very very verbose) - * @param string $msg : Message - * @param array $mailarr : Details of the mail - * - * $mailarr['sender'] Absender - * $mailarr['receiver'] Empfaenger, mehrere mit Komma getrennt - * $mailarr['subject'] Betreff - * $mailarr['body'] Mailinhalt - * $mailarr['src'] level oder form_element.id um herauszufinden wer die Mail gesendet hat - * - * @param string $mailarr : data to log - * @return The content that is displayed on the website - */ - - public function log_mail($origin, $status, $msg, $mailarr) { - - $text = '[' . $msg . "]"; - $text .= '[' . $mailarr["sender"] . ']'; - $text .= '[' . $mailarr["receiver"] . ']'; - $text .= '[' . $mailarr["subject"] . ']'; - $text .= '[' . $mailarr["body"] . ']'; - - $this->log_do('mail', $status, $origin, $text); - - } // log_mail() - -} diff --git a/extension/qfq/qfq/report/Report.php b/extension/qfq/qfq/report/Report.php index 4f73ad1d2413fc4b01d6a17a08a7ed7f36aa9e24..b07302760d26117e5e8e02a7ff5f66956ffa050a 100644 --- a/extension/qfq/qfq/report/Report.php +++ b/extension/qfq/qfq/report/Report.php @@ -12,16 +12,17 @@ namespace qfq; //use qfq; require_once(__DIR__ . '/Define.php'); -require_once(__DIR__ . '/Utils.php'); require_once(__DIR__ . '/Variables.php'); require_once(__DIR__ . '/Error.php'); -//require_once(__DIR__ . '/Db.php'); require_once(__DIR__ . '/../Database.php'); require_once(__DIR__ . '/Link.php'); require_once(__DIR__ . '/Sendmail.php'); require_once(__DIR__ . '/../exceptions/UserReportExtension.php'); require_once(__DIR__ . '/../Evaluate.php'); +require_once(__DIR__ . '/../helper/KeyValueStringParser.php'); +const DEFAULT_QUESTION = 'question'; +const DEFAULT_ICON = 'icon'; class Report { @@ -30,6 +31,11 @@ class Report { */ private $sip = null; + /** + * @var Link + */ + private $link = null; + /** * @var Store */ @@ -39,10 +45,6 @@ class Report { * @var string */ private $dbAlias = ''; - /** - * @var Log - */ - private $log = null; // frArray[10.50.5.sql][select ...] private $frArray = array(); @@ -54,33 +56,27 @@ class Report { // private $resultArray = array(); private $levelCount = 0; //private $counter = 0; + /** * @var Variables */ private $variables = null; /** - * @var Utils + * @var Database */ - private $utils = null; + private $db = null; /** - * @var Db + * @var array */ - private $db = null; + private $pageDefaults = array(); /** - * @var Sendmail + * @var array - Emulate global variable: will be set much earlier in other functions. Will be shown in error messages. */ - private $sendmail = null; - - private $page_control = array(); - - // Emulate global variable: will be set much earlier in other functions. Will be shown in error messages. private $fr_error = array('uid' => '', 'pid' => '', 'row' => '', 'debug_level' => '0', 'full_level' => ''); - private $t3data = array(); - private $phpUnit = false; private $showDebugInfo = false; @@ -89,48 +85,50 @@ class Report { * Report constructor. * * @param array $t3data + * @param Evaluate $eval + * @param bool $phpUnit */ - public function __construct(array $t3data, $sessionName, Evaluate $eval, $phpUnit = false) { + public function __construct(array $t3data, Evaluate $eval, $phpUnit = false) { $this->phpUnit = $phpUnit; - $this->t3data = $t3data; - $this->sip = new Sip($sessionName, $phpUnit); + Support::setIfNotSet($t3data, "uid", 0); + + $this->sip = new Sip($phpUnit); + if ($phpUnit) { + $this->sip->sipUniqId('badcaffee1234'); + //TODO Webserver Umgebung besser faken + $_SERVER['REQUEST_URI'] = 'localhost'; + } + + $this->link = new Link($this->sip, $phpUnit); + $this->store = Store::getInstance(); $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'); - $this->page_control["msgbox"]["pagec"] = "Please confirm!"; - - $this->page_control["hash"]["paged"] = "h"; - $this->page_control["hash"]["pagee"] = "h"; - $this->page_control["hash"]["pagen"] = "h"; + $this->pageDefaults[DEFAULT_QUESTION]["pagec"] = "Please confirm!:info"; + $this->pageDefaults[DEFAULT_QUESTION]["paged"] = "Do you really want to delete the record?:warning"; - $this->page_control["icon"]["paged"] = "D"; - $this->page_control["icon"]["pagee"] = "E"; - $this->page_control["icon"]["pageh"] = "H"; - $this->page_control["icon"]["pagei"] = "I"; - $this->page_control["icon"]["pagen"] = "N"; - $this->page_control["icon"]["pages"] = "S"; + $this->pageDefaults[DEFAULT_ICON]["paged"] = TOKEN_DELETE; + $this->pageDefaults[DEFAULT_ICON]["pagee"] = TOKEN_EDIT; + $this->pageDefaults[DEFAULT_ICON]["pageh"] = TOKEN_HELP; + $this->pageDefaults[DEFAULT_ICON]["pagei"] = TOKEN_INFO; + $this->pageDefaults[DEFAULT_ICON]["pagen"] = TOKEN_NEW; + $this->pageDefaults[DEFAULT_ICON]["pages"] = TOKEN_SHOW; $this->db = new Database(); - $this->utils = new Utils(); $this->variables = new Variables($eval, $t3data["uid"]); // Set static values, which won't change during this run. $this->fr_error["pid"] = isset($this->variables->resultArray['global.']['page_id']) ? $this->variables->resultArray['global.']['page_id'] : 0; - $this->fr_error["uid"] = $t3data["uid"]; + $this->fr_error["uid"] = $t3data['uid']; $this->fr_error["debug_level"] = 0; // Sanitize function for POST and GET Parameters. // Merged URL-Parameter (key1, id etc...) in resultArray. $this->variables->resultArray = array_merge($this->variables->resultArray, array("global." => $this->variables->collectGlobalVariables())); - // Create Logclass. - $this->log = new Log($this->variables->resultArray['global.']); - - // Create sendmail Class. Take care to prepare a fr_log instance. - $this->sendmail = new Sendmail($this->log); } /** @@ -138,12 +136,15 @@ class Report { * * @return string */ - public function process() { + public function process($bodyText) { - $this->log->set_fr_error($this->fr_error); + //phpUnit Test: clean environment + $this->frArray = array(); + $this->indexArray = array(); + $this->levelCount = 0; // Iteration over Bodytext - $ttLineArray = explode("\n", $this->t3data['bodytext']); + $ttLineArray = explode("\n", $bodyText); foreach ($ttLineArray as $index => $line) { // Fill $frArray, $indexArray, $resultArray @@ -164,6 +165,7 @@ class Report { * Example: 10.50.5.sql = select * from person * * @param string $ttLine : line to split in level, command, content + * @throws SyntaxReportException * @return void */ private function parseFRLine($ttLine) { @@ -171,11 +173,16 @@ class Report { // 10.50.5.sql = select ... $arr = explode("=", trim($ttLine), 2); + // no elements or only one: do nothing + if (count($arr) < 2) + return; + // 10.50.5.sql $key = strtolower(trim($arr[0])); // comment ? - if (substr($key, 0, 1) == "#") return; + if (empty($key) || $key[0] === "#") + return; // select ... $value = trim($arr[1]); @@ -186,6 +193,10 @@ class Report { // frCmd = "sql" $frCmd = $arrKey[count($arrKey) - 1]; + if (strpos('|' . strtolower(TOKEN_VALID_LIST) . '|', '|' . $frCmd . '|') === false) { + throw new SyntaxReportException ("Unknown token: $frCmd in Line '$ttLine''", ERROR_UNKNOWN_TOKEN, null, __FILE__, __LINE__, $this->fr_error); + } + // remove last item (cmd) unset($arrKey[count($arrKey) - 1]); @@ -212,20 +223,29 @@ class Report { // per sql command //pro sql cmd wir der Indexarray abgefüllt. Dieser wird später verwendet um auf den $frArray zuzugreifen //if(preg_match("/^sql/i", $frCmd) == 1){ - if ($frCmd == "sql" || $frCmd == "form") { + if ($frCmd === TOKEN_FORM || $frCmd === TOKEN_SQL) { // Remember max level $this->levelCount = max(substr_count($level, '.') + 1, $this->levelCount); // $indexArray[10][50][5] $this->indexArray[] = explode(".", $level); } + + // set defaults + if ($frCmd === TOKEN_SQL) { + $arr = explode('|', TOKEN_VALID_LIST); + foreach ($arr as $key) { + if (!isset($this->frArray[$level . "." . $key])) + $this->frArray[$level . "." . $key] = ''; + } + } } /** * Sorts the associative array. * - * @param array $ary : The unsorted Level Array - * @param string $clause : the sort argument 0 ASC, 1 ASC... according to the number of columns - * @return The content that is displayed on the website + * @param array $ary : The unsorted Level Array + * @param string $clause : the sort argument 0 ASC, 1 ASC... according to the number of columns + * @param bool|true $ascending */ private function sortIndexArray(array &$ary, $clause, $ascending = true) { @@ -272,7 +292,14 @@ class Report { if ($fnBody) { $sortFn = create_function('$a,$b', $fnBody); + + // TODO: at the moment, $sortFn() triggers some E_NOTICE warnings. We stop these here for a short time. + $errorSet = error_reporting(); + error_reporting($errorSet & ~E_NOTICE); + usort($ary, $sortFn); + + error_reporting($errorSet); } } @@ -288,7 +315,9 @@ class Report { for ($i = 0; $i < $this->levelCount; $i++) { $sortArg = $sortArg . $i . " ASC, "; } + $sortArg = substr($sortArg, 0, strlen($sortArg) - 2); + return $sortArg; } @@ -356,22 +385,22 @@ class Report { // $this->dbAlias = $this->getValueParentDefault("db", $full_super_level, $full_level, $cur_level, DB); // Set debug, if one is specified else keep the parent one. - $lineDebug = $this->getValueParentDefault("debug", $full_super_level, $full_level, $cur_level, 0); + $lineDebug = $this->getValueParentDefault(TOKEN_DEBUG, $full_super_level, $full_level, $cur_level, 0); // Prepare Error reporting - $this->store->setVar(SYSTEM_SQL_RAW, $this->frArray[$full_level . ".sql"], STORE_SYSTEM); + $this->store->setVar(SYSTEM_SQL_RAW, $this->frArray[$full_level . "." . TOKEN_SQL], STORE_SYSTEM); $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, $full_level, STORE_SYSTEM); // Prepare SQL: replace variables. Actual 'line.total' or 'line.count' will recalculated: don't replace them now! unset($this->variables->resultArray[$full_level . ".line."]["total"]); unset($this->variables->resultArray[$full_level . ".line."]["count"]); - $sql = $this->variables->doVariables($this->frArray[$full_level . ".sql"]); + $sql = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_SQL]); $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); //Execute SQL. All errors have been already catched. unset($result); - $result = $this->db->sqlKeys($sql, $keys, $stat); + $result = $this->db->sql($sql, ROW_KEYS, array(), '', $keys, $stat); // If an array is returned, $sql was a query, otherwise an 'insert', 'update', 'delete', ... // Query: total nummber of rows @@ -385,10 +414,14 @@ class Report { // HEAD: If there is at least one record, do 'head'. if ($rowTotal > 0) - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "head"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_HEAD]); // Prepare row alteration - $arrRbgd = explode("|", $this->frArray[$full_level . "." . "rbgd"]); + $arrRbgd = explode("|", $this->frArray[$full_level . "." . TOKEN_RBGD]); + if (count($arrRbgd) < 2) { + $arrRbgd[] = ''; + $arrRbgd[] = ''; + } if (is_array($result)) { //--------------------------------- @@ -407,32 +440,32 @@ class Report { // SEP set seperator (empty on first run) $content .= $columnValueSeperator; - $columnValueSeperator = $this->variables->doVariables($this->frArray[$full_level . "." . "rsep"]); + $columnValueSeperator = $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_RSEP]); // RBGD: even/odd rows - $content .= str_replace("rbgd", $arrRbgd[$rowIndex % 2], $this->frArray[$full_level . "." . "rbeg"]); + $content .= str_replace(TOKEN_RBGD, $arrRbgd[$rowIndex % 2], $this->frArray[$full_level . "." . TOKEN_RBEG]); //----------------------------- // COLUMNS: Collect all columns $content .= $this->collectRow($row, $keys, $full_level, $rowIndex); // REND - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "rend"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_REND]); // Trigger subqueries of this level $content .= $this->triggerReport($cur_level + 1, $this->indexArray[$counter], $counter + 1); // RENR - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "renr"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_RENR]); } } //Print althead or tail if ($rowTotal > 0) { - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "tail"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_TAIL]); } else { - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "althead"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_ALT_HEAD]); } ++$counter; @@ -462,7 +495,7 @@ class Report { */ private function getValueParentDefault($level_key, $full_super_level, $full_level, $cur_level, $default) { - if ($this->frArray[$full_level . "." . $level_key]) { + if (!empty($this->frArray[$full_level . "." . $level_key])) { $value = $this->frArray[$full_level . "." . $level_key]; } else { if ($cur_level == 1) { @@ -501,10 +534,10 @@ class Report { if ($flagOutput) { //prints $content .= $this->variables->doVariables($fsep); - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "fbeg"]); + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_FBEG]); $content .= $renderedColumn; - $content .= $this->variables->doVariables($this->frArray[$full_level . "." . "fend"]); - $fsep = $this->frArray[$full_level . "." . "fsep"]; + $content .= $this->variables->doVariables($this->frArray[$full_level . "." . TOKEN_FEND]); + $fsep = $this->frArray[$full_level . "." . TOKEN_FSEP]; } } return ($content); @@ -526,88 +559,104 @@ class Report { $flagControl = false; $flagOutput = true; - if (substr($columnName, 0, 1) == "_") { + // Empty columnsnames are allowed: check with isset + if (isset($columnName[0]) && $columnName[0] === "_") { $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); - $content .= $link->renderLink($columnValue); -# unset $link; + $content .= $this->link->renderLink($columnValue); break; + case "exec": $content .= $this->myExec($columnValue); break; - case "Page": - case "Pagec": - case "Paged": - case "Pagee": - case "Pageh": - case "Pagei": - case "Pagen": - case "Pages": - $linkValue = $this->doFixColPosPage($columnName, $columnValue); - - $link = new Link($this->fr_error, $this->sip); - $content .= $link->renderLink($linkValue); + // Uppercase 'P' + case COLUMN_PPAGE: + case COLUMN_PPAGEC: + case COLUMN_PPAGED: + case COLUMN_PPAGEE: + case COLUMN_PPAGEH: + case COLUMN_PPAGEI: + case COLUMN_PPAGEN: + case COLUMN_PPAGES: + $pageColumnName = strtolower($columnName); + $tokenizedValue = $this->doFixColPosPage($columnName, $columnValue); + $linkValue = $this->doPage($pageColumnName, $tokenizedValue); + $content .= $this->link->renderLink($linkValue); break; - case "page": - case "pagec": - case "paged": - case "pagee": - case "pageh": - case "pagei": - case "pagen": - case "pages": -#debug($columnValue); + // Lowercase 'P' + case COLUMN_PAGE: + case COLUMN_PAGEC: + case COLUMN_PAGED: + case COLUMN_PAGEE: + case COLUMN_PAGEH: + case COLUMN_PAGEI: + case COLUMN_PAGEN: + case COLUMN_PAGES: $linkValue = $this->doPage($columnName, $columnValue); -// debug($linkValue); - - $link = new Link($this->fr_error, $this->sip); - $content .= $link->renderLink($linkValue); + $content .= $this->link->renderLink($linkValue); break; case "bullet": - if ($columnValue === '') + if ($columnValue === '') { break; + } - $linkValue = "r:3|B:" . $columnValue; - $link = new Link($this->fr_error, $this->sip); - $content .= $link->renderLink($linkValue); + // r:3|B: + $linkValue = TOKEN_RENDER . ":3|" . TOKEN_BULLET . ":" . $columnValue; + $content .= $this->link->renderLink($linkValue); break; case "check": - if ($columnValue === '') + if ($columnValue === '') { break; + } - $linkValue = "r:3|C:" . $columnValue; - $link = new Link($this->fr_error, $this->sip); - $content .= $link->renderLink($linkValue); + // "r:3|C: + $linkValue = TOKEN_RENDER . ":3|" . TOKEN_CHECK . ":" . $columnValue; + $content .= $this->link->renderLink($linkValue); break; case "img": // "<path to image>|[alttext]|[text behind]" renders to: <img src="<path to image>" alt="[alttext]">[text behind] - if (empty($columnValue)) break; - $tmp = explode("|", $columnValue, 3); - if ($tmp[0] == "") break; - $content .= '<img src="' . $tmp[0] . '" alt="' . $tmp[1] . '">' . $tmp[2]; + if (empty($columnValue)) { + break; + } + + $mailarr = explode("|", $columnValue, 3); + + // Fake values for tmp[1], tmp[2] to suppress access errors. + $mailarr[] = ''; + $mailarr[] = ''; + + if (empty($mailarr[0])) { + break; + } + $attribute = Support::doAttribute('src', $mailarr[0]); + $attribute .= Support::doAttribute('alt', $mailarr[1]); + + $content .= '<img ' . $attribute . '>' . $mailarr[2]; break; case "mailto": // "<email address>|[Real Name]" renders to (encrypted via JS): <a href="mailto://<email address>"><email address></a> OR <a href="mailto://<email address>">[Real Name]</a> - $tmp = explode("|", $columnValue, 2); - if ($tmp[0] == "") break; + $mailarr = explode("|", $columnValue, 2); + if (empty($mailarr[0])) { + break; + } - $t1 = explode("@", $tmp[0], 2); + $t1 = explode("@", $mailarr[0], 2); $content .= "<script language=javascript><!--" . chr(10); - if (empty($tmp[1])) $tmp[1] = $tmp[0]; + if (empty($mailarr[1])) $mailarr[1] = $mailarr[0]; - $content .= 'var contact = "' . substr($tmp[1], 0, 2) . '"' . chr(10); - $content .= 'var contact1 = "' . substr($tmp[1], 2) . '"' . chr(10); + $content .= 'var contact = "' . substr($mailarr[1], 0, 2) . '"' . chr(10); + $content .= 'var contact1 = "' . substr($mailarr[1], 2) . '"' . chr(10); $content .= 'var email = "' . $t1[0] . '"' . chr(10); $content .= 'var emailHost = "' . $t1[1] . '"' . chr(10); @@ -617,15 +666,41 @@ class Report { break; case "sendmail": - // 'Absender|Empfaenger, mehrere mit Komma getrennt|Betreff|Mailinhalt' - $tmp = explode("|", $columnValue, 4); + // '<receiver1>,<receiver2>,...|<sender>|<subject>|<body>|<reply-to>|<flag autosubmit: on /off>' + $mailarr = explode("|", $columnValue); + if (count($mailarr) < 4) { + throw new SyntaxReportException ("Too few parameter for sendmail: $columnValue", ERROR_TOO_FEW_PARAMETER_FOR_SENDMAIL, null, __FILE__, __LINE__, $this->fr_error); + } - $mail['receiver'] = $tmp[0]; - $mail['sender'] = $tmp[1]; - $mail['subject'] = $tmp[2]; - $mail['body'] = $tmp[3]; + if (!isset($mailarr[SENDMAIL_IDX_REPLY_TO])) { + $mailarr[SENDMAIL_IDX_REPLY_TO] = ''; + } + + if (!isset($mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT])) { + $mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = 'off'; + } + + if (!isset($mailarr[SENDMAIL_IDX_GR_ID])) { + $mailarr[SENDMAIL_IDX_GR_ID] = '0'; + } + + if (!isset($mailarr[SENDMAIL_IDX_X_ID])) { + $mailarr[SENDMAIL_IDX_X_ID] = '0'; + } + + if (!isset($mailarr[SENDMAIL_IDX_RECEIVER_CC])) { + $mailarr[SENDMAIL_IDX_RECEIVER_CC] = ''; + } - $content = $this->sendmail->sendmail($mail); + if (!isset($mailarr[SENDMAIL_IDX_RECEIVER_BCC])) { + $mailarr[SENDMAIL_IDX_RECEIVER_BCC] = ''; + } + + $mailarr[SENDMAIL_IDX_SRC] = "Report: T3 pageId=" . $this->store->getVar('pageId', STORE_TYPO3) . + ", T3 ttcontentId=" . $this->store->getVar('ttcontentUid', STORE_TYPO3) . + ", Level=" . $full_level; + + new Sendmail($mailarr); break; case "vertical": @@ -637,11 +712,12 @@ class Report { # width $width = $arr[2] ? $arr[2] : "1em"; - $tmp = "width:$width; "; + $mailarr = "width:$width; "; # height - if ($arr[3]) - $tmp .= "height:" . $arr[3] . "; "; + if ($arr[3]) { + $mailarr .= "height:" . $arr[3] . "; "; + } # tag if ($arr[4]) { @@ -659,80 +735,6 @@ class Report { #$style = "line-height: 1.5em; background:#eee; display: block; white-space: nowrap; padding-left: 3px; writing-mode: tb-rl; filter: flipv fliph; transform: rotate(270deg) translate(-10em,0); transform-origin: 0 0; -moz-transform: rotate(270deg) translate(-10em,0); -moz-transform-origin: 0 0; -webkit-transform: rotate(270deg) translate(-10em,0); -webkit-transform-origin: 0 0;"; $content = $tag_open . 'style="' . $style . '">' . $arr[0] . $tag_close; - - - break; - - case "F": - $newColumnName = ""; - $newFinalColumnName = ""; - $remain = array(); - $striptags = false; - $tag = ""; - - # 'Q:mailto|Z|V:mail_first|support@example.com' - $arr = explode("|", $columnValue); - foreach ($arr as $value) { - $kv = explode(":", $value); - switch ($kv[0]) { - case "Q": - $newColumnName = $kv[1]; - #if(!$newColumnName) throw new syntaxException ( "Missing a 'reserved column name' for parameter 'Q' in column 'F': $columnValue","",__FILE__,__LINE__,$this->fr_error); - if ($newColumnName == 'F') - throw new SyntaxReportException ("Not allowed: 'F' as 'reserved column name' for parameter 'Q': $columnValue", - "", __FILE__, __LINE__, $this->fr_error); - break; - case "Z": -// $show = false; - break; - case "T": - $striptags = TRUE; - break; - case "X": - $tag = $kv[1]; - if (!$tag) - throw new SyntaxReportException ("Missing the 'tag' parameter for 'X'): $columnValue", - "", __FILE__, __LINE__, $this->fr_error); - break; - case "V": - $newFinalColumnName = $kv[1]; - if (!$newFinalColumnName) - throw new SyntaxReportException ("Missing the 'name' parameter for 'V': $columnValue", - "", __FILE__, __LINE__, $this->fr_error); - break; - case 'F': - throw new SyntaxReportException ("Qualifier 'F' is not allowed inside of a column with column name 'F': $columnValue", - "", __FILE__, __LINE__, $this->fr_error); - break; - # Save every non 'F' qualifier for later usage - default: - $remain[] = $value; - break; - } - } - - # Check for needed action - # if(!$newColumnName) throw new syntaxException ( "Missing parameter 'Q' in column 'F': $columnValue","",__FILE__,__LINE__,$this->fr_error); - if ($newColumnName) - $columnName = $newColumnName; - else - $columnName = "no column name defined"; - - # reconstruct remaining parameters - $arr = implode("|", $remain); - # render - $content = $this->renderColumn($columnIndex, $columnName, $arr, $full_level, $rowIndex, $flagOutput); - - if ($newFinalColumnName) - $columnName = $newFinalColumnName; - - # striptags - if ($striptags) $content = strip_tags($content); - # tag - if ($tag) { - $arr = explode(" ", $tag); - $content = "<" . $tag . ">" . $content . "</" . $arr[0] . ">"; - } break; default : @@ -787,62 +789,82 @@ class Report { * * $columnValue: * ------------- - * [<page id|alias>[¶m=value&...]] | [record id] | [text] | [tooltip] | [msgbox] | [class] | [target] | [render mode] | [create hash] + * [<page id|alias>[¶m=value&...]] | [text] | [tooltip] | [msgbox] | [class] | [target] | [render mode] * * param[0]: <page id|alias>[¶m=value&...] - * param[1]: record id * param[2]: text * param[3]: tooltip * param[4]: msgbox * param[5]: class * param[6]: target * param[7]: render mode - * param[8]: create hash * @throws SyntaxReportException */ private function doFixColPosPage($columnName, $columnValue) { - $link = ""; + $tokenList = ""; + + if (empty($columnName)) + return ''; // Split definition - $param = explode('|', $columnValue); - if (count($param) > 9) - throw new SyntaxReportException ("Too many parameter (max=9): $columnValue", "", __FILE__, __LINE__, $this->fr_error); + $allParam = explode('|', $columnValue); + if (count($allParam) > 8) + throw new SyntaxReportException ("Too many parameter (max=8): $columnValue", ERROR_TOO_MANY_PARAMETER, null, __FILE__, __LINE__, $this->fr_error); + + // First Parameter: Split PageId|PageAlias and URL Params + $firstParam = explode('&', $allParam[0], 2); + if (empty($firstParam[1])) { + $firstParam[] = ''; + } - // make first 'P' lowercase - $columnName = 'p' . substr($columnName, 1); + switch ($columnName) { + case COLUMN_PPAGED: + // no pageid /pagealias given. + $tokenList .= $this->composeLinkPart(TOKEN_URL_PARAM, $allParam[0]); + break; + default: + $tokenList .= $this->composeLinkPart(TOKEN_PAGE, $firstParam[0]); // -- PageID -- + $tokenList .= $this->composeLinkPart(TOKEN_URL_PARAM, $firstParam[1]); + } - // -- Page -- - // Split PageId|PageAlias and URL Params - $tmparr = explode('&', $param[0], 2); + if (isset($allParam[1]) && $allParam[1] !== '') { + $tokenList .= $this->composeLinkPart(TOKEN_TEXT, $allParam[1]); // -- Text -- + } - $link .= $this->composeLinkPart('p', $tmparr[0]); // -- PageID -- - $link .= $this->composeLinkPart('U', $tmparr[1]); // -- URL Params -- - $link .= $this->composeLinkPart('i', $param[1]); // -- record id -- - $link .= $this->composeLinkPart('t', $param[2]); // -- Text -- - $link .= $this->composeLinkPart('o', $param[3]); // -- tooltip -- - $link .= $this->composeLinkPart('q', $param[4], $this->page_control["msgbox"][$columnName]); // -- msgbox - $link .= $this->composeLinkPart('c', $param[5]); // -- class -- - $link .= $this->composeLinkPart('g', $param[6]); // -- target -- - $link .= $this->composeLinkPart('r', $param[7]); // -- render mode -- + if (isset($allParam[2]) && $allParam[2] !== '') { + $tokenList .= $this->composeLinkPart(TOKEN_TOOL_TIP, $allParam[2]); // -- tooltip -- + } - if (!$param[8]) $param[8] = $this->page_control["hash"][$columnName]; // if no hash behaviour defined, use default - if ($param[8] == "h") $link .= "h|"; + if (isset($allParam[3]) && $allParam[3] !== '') { + $text = isset($this->pageDefaults[DEFAULT_QUESTION][$columnName]) ? $this->pageDefaults[DEFAULT_QUESTION][$columnName] : ''; + $tokenList .= $this->composeLinkPart(TOKEN_QUESTION, $allParam[3], $text); // -- msgbox + } - if ($this->page_control["icon"][$columnName]) - $link .= $this->page_control["icon"][$columnName] . "|"; + if (isset($allParam[4]) && $allParam[4] !== '') { + $tokenList .= $this->composeLinkPart(TOKEN_CLASS, $allParam[4]); // -- class -- + } - return ($link); - } + if (isset($allParam[5]) && $allParam[5] !== '') { + $tokenList .= $this->composeLinkPart(TOKEN_TARGET, $allParam[5]); // -- target -- + } - /** - * The main method of the PlugIn - * - * @param string $content : The PlugIn content - * @param array $conf : The PlugIn configuration - * @return string The content that is displayed on the website - */ - //Check ob arr1 nur 1 Feld mehr hat als arr2 + if (isset($allParam[6]) && $allParam[6] !== '') { + $tokenList .= $this->composeLinkPart(TOKEN_RENDER, $allParam[6]); // -- render mode -- + } + + if (!isset($allParam[7])) { + $allParam[7] = '1'; // if no SIP behaviour defined: sip is set + } + + $tokenList .= $this->composeLinkPart(TOKEN_SIP, $allParam[7]); // -- SIP -- + + if (isset($this->pageDefaults[DEFAULT_ICON][$columnName])) { + $tokenList .= $this->pageDefaults[DEFAULT_ICON][$columnName] . "|"; + } + + return ($tokenList); + } /** * If there is a value (or a defaultValue): compose it together with qualifier and delimiter. @@ -855,16 +877,17 @@ class Report { */ private function composeLinkPart($qualifier, $value, $defaultValue = "") { - if (!$value) $value = $defaultValue; + if ($value === '') + $value = $defaultValue; - if ($value) + if ($value !== '') return ($qualifier . ":" . $value . "|"); return ''; } /** - * Renders pageX: extract token and determine if any default value has be applied + * Renders _pageX: extract token and determine if any default value has to be applied * * @param string $columnName * @param string $columnValue @@ -872,46 +895,72 @@ class Report { * @return string rendered link */ private function doPage($columnName, $columnValue) { + $defaultQuestion = ''; + $defaultActionDelete = ''; $param = explode('|', $columnValue); # get all defaultvalues, depending on the columnname - $defaultImage = $this->page_control["icon"][$columnName]; - $defaultHash = $this->page_control["hash"][$columnName]; + $defaultImage = isset($this->pageDefaults[DEFAULT_ICON][$columnName]) ? $this->pageDefaults[DEFAULT_ICON][$columnName] : ''; + $defaultSip = 's'; + if ($columnName === COLUMN_PAGED) { + $defaultActionDelete = TOKEN_ACTION_DELETE . ':' . TOKEN_ACTION_DELETE_REPORT; + } + # define defaultquestion only, if pagetype needs a question - if ($this->page_control["msgbox"][$columnName]) $defaultQuestion = 'q:' . $this->page_control["msgbox"][$columnName]; + if (!empty($this->pageDefaults[DEFAULT_QUESTION][$columnName])) { + $defaultQuestion = 'q:' . $this->pageDefaults[DEFAULT_QUESTION][$columnName]; + } foreach ($param as $key) { switch (substr($key, 0, 1)) { - case 'P': - case 'E': - case 'N': - case 'D': - case 'H': - case 'I': - case 'S': - case 'B': - case 'C': + case TOKEN_PICTURE: + case TOKEN_EDIT: + case TOKEN_NEW: + case TOKEN_DELETE: + case TOKEN_HELP: + case TOKEN_INFO: + case TOKEN_SHOW: + case TOKEN_BULLET: + case TOKEN_CHECK: $defaultImage = ''; // if any of the img token is given: no default break; - case 'h': - $defaultHash = ''; // if a hash definition is given: no default + case TOKEN_SIP: + $defaultSip = ''; // if a hash definition is given: no default break; - case 'q': + case TOKEN_QUESTION: $defaultQuestion = ''; // if a question is given: no default break; + case TOKEN_ACTION_DELETE: + $defaultActionDelete = ''; + default: + break; } } $columnValue .= "|"; - // append defaulst - if ($defaultImage) $columnValue .= $defaultImage . "|"; - if ($defaultHash) $columnValue .= $defaultHash . "|"; - if ($defaultQuestion) $columnValue .= $defaultQuestion . "|"; + // append defaults + if ($defaultActionDelete !== '') { + $columnValue .= $defaultActionDelete . "|"; + } + + if ($defaultImage !== '') { + $columnValue .= $defaultImage . "|"; + } + + if ($defaultSip !== '') { + $columnValue .= $defaultSip . "|"; + } + + if ($defaultQuestion !== '') { + $columnValue .= $defaultQuestion . "|"; + } -#debug($columnValue); +// if ($columnName === 'paged') { +// $columnValue = $this->adjustDeleteParameter($columnValue); +// } return ($columnValue); } diff --git a/extension/qfq/qfq/report/Sendmail.php b/extension/qfq/qfq/report/Sendmail.php index 7780b90bd394c0c5b9b23eb5a46bcd8a920d122d..02706543f66a580a9885992c610503c6bfa6b8d8 100644 --- a/extension/qfq/qfq/report/Sendmail.php +++ b/extension/qfq/qfq/report/Sendmail.php @@ -4,57 +4,102 @@ namespace qfq; //use qfq; -require_once(__DIR__ . '/Define.php'); -require_once(__DIR__ . '/Error.php'); - +require_once(__DIR__ . '/../Constants.php'); +require_once(__DIR__ . '/../Database.php'); class Sendmail { /** - * @var Log + * Sends a mail as specified in $mailarr. + * If there is no receiver specified as 'TO': no mail is sent. This is ok and no error. + * Logs every send mail as a record in table `mailLog`. Additionally a `grId` and a `xId` can be specified + * to assing the logentry to a specific action. + * The log record also contains some information which generates the mail (form/formelement or QFQ query). + * + * Structure mailarr: + * SENDMAIL_IDX_RECEIVER email address(es) + * SENDMAIL_IDX_SENDER email address + * SENDMAIL_IDX_SUBJECT string + * SENDMAIL_IDX_BODY string + * SENDMAIL_IDX_REPLY_TO optional: email address + * SENDMAIL_IDX_FLAG_AUTO_SUBMIT optional: 'on'|'off' + * SENDMAIL_IDX_GR_ID optional: integer + * SENDMAIL_IDX_X_ID optional: integer + * + * @param $mailarr + * @throws UserFormException */ - private $Log; + public function __construct(array $mailarr) { + + // If there is no 'Receiver': do not send a mail. + if (!isset($mailarr[SENDMAIL_IDX_RECEIVER]) || $mailarr[SENDMAIL_IDX_RECEIVER] === '') { + return; + } + + if (count($mailarr) < 4 || $mailarr[SENDMAIL_IDX_SENDER] === '' || $mailarr[SENDMAIL_IDX_SUBJECT] === '' || $mailarr[SENDMAIL_IDX_BODY] === '') { + throw new UserFormException("Error sendmail missing one of: receiver, sender, subject or body", ERROR_SENDMAIL_MISSING_VALUE); + } + + $header = $this->buildHeader($mailarr); + if (!(mb_send_mail($mailarr[SENDMAIL_IDX_RECEIVER], $mailarr[SENDMAIL_IDX_SUBJECT], $mailarr[SENDMAIL_IDX_BODY], $header, "-f " . $mailarr[SENDMAIL_IDX_SENDER]))) { + throw new UserFormException("Error sendmail failed.", ERROR_SENDMAIL); + } + + $this->mailLog($mailarr, $header); + } /** - * Constructor: - * - * @param Log $fr_log + * @param $mailarr + * @return string */ + private function buildHeader($mailarr) { + + // "\r\n" needs to be enclosed in double ticks to correctly converted to 0x0d 0x0a, + $header = "From: " . $mailarr[SENDMAIL_IDX_SENDER] . "\r\n"; + + if (isset($mailarr[SENDMAIL_IDX_REPLY_TO]) && $mailarr[SENDMAIL_IDX_REPLY_TO] != '') { + $header .= "Reply-To: " . $mailarr[SENDMAIL_IDX_REPLY_TO] . "\r\n"; + } + + if (isset($mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT]) && $mailarr[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] === 'on') { + $header .= "Auto-Submitted: auto-send\r\n"; + } - public function __construct(Log $fr_log) { + if (isset($mailarr[SENDMAIL_IDX_RECEIVER_CC]) && $mailarr[SENDMAIL_IDX_RECEIVER_CC] != '') { + $header .= "Cc: " . $mailarr[SENDMAIL_IDX_RECEIVER_CC] . "\r\n"; + } + + if (isset($mailarr[SENDMAIL_IDX_RECEIVER_BCC]) && $mailarr[SENDMAIL_IDX_RECEIVER_BCC] != '') { + $header .= "Bcc: " . $mailarr[SENDMAIL_IDX_RECEIVER_BCC] . "\r\n"; + } - $this->Log = $fr_log; + return $header; } /** - * Send an email. Mail delivery should work - else the mails might disappear - * Seperate lines in the body with '\r\n' - * RC: if RC==0 Returns Output, else 'RC - Output' + * Creates a new MailLog Record based on $mailArr / $header. * - * @param array $mailarr : $mailarr['receiver'] (multiple with comma), $mailarr['sender'], $mailarr['subject'], $mailarr['body'] - * @return string + * @param array $mailarr + * @param $headers + * @throws CodeException + * @throws DbException */ + private function mailLog(array $mailarr, $header) { - public function sendmail($mailarr) { - $status = 'E'; - - // sending only if there is a receiver ! - if ($mailarr['receiver']) { - // if(mail($receiver,$subject,$message, "From: ".$sender."\nX-Mailer: PHP/ . $phpversion()", "-f ".$sender)) - if (mail($mailarr['receiver'], $mailarr['subject'], $mailarr['body'], "X-Mailer: PHP/" . phpversion() . "\r\nFrom: " . $mailarr['sender'] . ".\r\n", "-f " . $mailarr['sender'])) { - $msg = "Mail has been sent"; - $status = 'I'; - } else { - $msg = "Sending Mail not accepted"; - } - } else { - $msg = "Mail not sent: missing receiver"; - } + $log = array(); - // Log every mail - $this->Log->log_mail("form", $status, $msg, $mailarr); + // Log + $log[SENDMAIL_IDX_RECEIVER] = $mailarr[SENDMAIL_IDX_RECEIVER]; + $log[SENDMAIL_IDX_SENDER] = $mailarr[SENDMAIL_IDX_SENDER]; + $log[SENDMAIL_IDX_SUBJECT] = $mailarr[SENDMAIL_IDX_SUBJECT]; + $log[SENDMAIL_IDX_BODY] = $mailarr[SENDMAIL_IDX_BODY]; + $log[4] = $header; + $log[5] = $mailarr[SENDMAIL_IDX_GR_ID]; + $log[6] = $mailarr[SENDMAIL_IDX_X_ID]; + $log[7] = $mailarr[SENDMAIL_IDX_SRC]; - return ($msg); - } // sendmail() + $db = new Database(); + $db->sql('INSERT INTO MailLog (`receiver`, `sender`, `subject`, `body`, `header`, `grId`, `xId`, `src`) VALUES ( ?, ? ,?, ?, ? ,?, ?, ? )', ROW_REGULAR, $log); + } } diff --git a/extension/qfq/qfq/report/Utils.php b/extension/qfq/qfq/report/Utils.php deleted file mode 100644 index e2fe65e05d55111eae03c436d6bb7741ba28b976..0000000000000000000000000000000000000000 --- a/extension/qfq/qfq/report/Utils.php +++ /dev/null @@ -1,132 +0,0 @@ -<?php -/*************************************************************** - * Copyright notice - * - * (c) 2010 Glowbase GmbH <support@glowbase.com> - * All rights reserved - * - ***************************************************************/ - -namespace qfq; - -//use qfq; - -require_once(__DIR__ . '/Define.php'); -require_once(__DIR__ . '/Db.php'); - - -class Utils { - /** - * @var Db - */ - private $db = null; - - /** - * @param $db - */ - public function __construct($db = null) { - //TODO: Im Original nachschauen woher die globale Variable $Db kommt??? Ubergangsweise hier im Konstruktor plaziert. Wird aber aktuell nicht initialisiert!!! - $this->db = null; - } - - /** - * If record locking has been enabled in ext_localconf.php, create a record in the lock table - * - * @param string $form - * @param int $record_id - * @param string $tablename - * @param string $dbalias - * @param $tx_db_pi1 - */ - function setLockRecord($form, $record_id, $tablename, $dbalias, &$tx_db_pi1) { - $result = ''; - $mode = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT]['lock_records']['mode']; - if ($mode == "warn" || $mode == "lock") { - $query = "INSERT INTO `" . FR_LOCK . "` (`phpsession_id`, `fe_user_uid`, `form`, `record_id`, `tablename`, `dbalias`) VALUES ('" . session_id() . "', '" . $GLOBALS["TSFE"]->fe_user->user["uid"] . "', '" . $form . "', '" . $record_id . "', '" . $tablename . "', '" . $dbalias . "')"; - $this->db->doQuery(DB, $query, $result, ROW_EXPECT_0); - } // if - } // randomAlphaNumUnique() - - /** - * If record locking has been enabled in ext_localconf.php, - * delete all expired locking records - * check if a record exists in the lock table for the currently edited record - * - * @param int $form form_id - * @param int $record_id record_id - * @param string $tablename tablename - * @param Db $dbalias Db class object - * @param $tx_db_pi1 - * @return array|bool information on locking mode, locking user and timestamp. false if not locked - */ - function checkLockRecord($form, $record_id, $tablename, $dbalias, &$tx_db_pi1) { - // Get config values from localconf or use default from define.php - $mode = $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT]['lock_records']['mode']; - $interval = ($GLOBALS['TYPO3_CONF_VARS'][FORMREPORT]['lock_records']['interval']) ?: LOCK_RECORDS_INTERVAL; - - if ($mode == "warn" || $mode == "lock") { - // Delete all expired locking records - $query = "DELETE FROM `" . FR_LOCK . "` WHERE timestamp + INTERVAL " . $GLOBALS['TYPO3_CONF_VARS'][FORMREPORT]['lock_records']['interval'] . " SECOND < NOW()"; - $this->db->doQuery(DB, $query, $result, ROW_EXPECT_0); - - // Check if locking records exist - $query = "SELECT fe_user_uid, phpsession_id, date_format(timestamp + INTERVAL " . $interval . " SECOND, \"%H:%i %d.%m.%Y\") as lock_endtime FROM `" . FR_LOCK . "` WHERE `record_id`='" . $record_id . "' and `tablename`='" . $tablename . "' and `dbalias`='" . $dbalias . "' LIMIT 1"; - $this->db->doQuery(DB, $query, $result, ROW_REGULAR); - - // If result is empty, return false - if (empty($result)) - return false; - - // If user is the same as the current one, return false - // Compare fe_user_uid and session-id - if ($result[0]['phpsession_id'] == session_id()) - return false; - - // Build array with locking information - will be used to create a warning/error message etc. - $arr = array(); - $arr['mode'] = $mode; - $arr['fe_user_uid'] = $result[0]['fe_user_uid']; - $arr['lock_endtime'] = $result[0]['lock_endtime']; - return $arr; - - } // if - // no locking configured - return false; - } // eo setLockRecord - - /** - * Returns username for a fe_user_uid - * - * @param int $uid fe_user_uid - * @param $tx_db_pi1 - * @return string username - */ - function getFEUserName($uid, &$tx_db_pi1) { - $query = "SELECT username FROM `fe_users` WHERE `uid`='" . $uid . "'"; - $this->db->doQuery(T3, $query, $result, ROW_EXPECT_1); - $username = ($result['username']) ?: "anonymous"; - return $username; - } // eo setLockRecord - - /** - * Create a unique directory in $path - * - * @param $path - * @return string - * @throws CodeReportException - */ - function createUniqueDir($path) { - // Try max. 20 times - for ($i = 0; $i < 20; $i++) { - $dirname = Support::randomAlphaNum(5); - $dirpath = $path . "/" . $dirname; - - if (!file_exists($dirpath)) { - mkdir($dirpath, 0700, true); - return $dirpath; - } - } - // Too many tries without success - throw new CodeReportException ("Could not create unique directory.", __FILE__, __LINE__); - } -} diff --git a/extension/qfq/qfq/report/Variables.php b/extension/qfq/qfq/report/Variables.php index 839417039520e5d2960a75e08179aeb78e475dc1..c4ee1e95ce3ceb410b08f7d6c725ffc48438a4a7 100644 --- a/extension/qfq/qfq/report/Variables.php +++ b/extension/qfq/qfq/report/Variables.php @@ -108,10 +108,23 @@ class Variables { public function collectGlobalVariables() { $arr = array(); - //TODO: Variablen sollten vom STORE_TYPO3 genommen werden - $arr["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"]; - $arr["HTTP_HOST"] = $_SERVER["HTTP_HOST"]; - $arr["REQUEST_URI"] = $_SERVER["REQUEST_URI"]; + if (isset($_SERVER["REMOTE_ADDR"])) { + //TODO: Variablen sollten vom STORE_TYPO3 genommen werden + $arr["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"]; + $arr["HTTP_HOST"] = $_SERVER["HTTP_HOST"]; + $arr["REQUEST_URI"] = $_SERVER["REQUEST_URI"]; + + $protocol = 'http'; + if (isset($_SERVER['HTTPS'])) { + if ($_SERVER["HTTPS"] != "off") + $protocol = 'https'; + } + $arr["url"] = $protocol . "://" . $_SERVER["HTTP_HOST"]; + if ($_SERVER["SERVER_PORT"] != 80) + $arr["url"] .= ":" . $_SERVER["SERVER_PORT"]; + $arr["url"] .= $arr["REQUEST_URI"]; + } + if (isset($GLOBALS["TSFE"]->fe_user)) { $arr["fe_user_uid"] = $GLOBALS["TSFE"]->fe_user->user["uid"] ?: '-'; $arr["fe_user"] = $GLOBALS["TSFE"]->fe_user->user["username"] ?: '-'; @@ -125,22 +138,14 @@ class Variables { $arr["page_type"] = $GLOBALS["TSFE"]->type; $arr["page_language_uid"] = $GLOBALS["TSFE"]->sys_language_uid; $arr["ttcontent_uid"] = $this->tt_content_uid; - $protocol = 'http'; - if (isset($_SERVER['HTTPS'])) { - if ($_SERVER["HTTPS"] != "off") - $protocol = 'https'; - } - $arr["url"] = $protocol . "://" . $_SERVER["HTTP_HOST"]; - if ($_SERVER["SERVER_PORT"] != 80) - $arr["url"] .= ":" . $_SERVER["SERVER_PORT"]; - $arr["url"] .= $arr["REQUEST_URI"]; // Add all variables from ext_localconf // database aliases can be used in form sql queries (e.g. select * from "{{global.t3_name}}".fe_users...) // localconf can be used to configure an application $tmp = array(); - if (isset($GLOBALS['TYPO3_CONF_VARS'])) + if (isset($GLOBALS['TYPO3_CONF_VARS'][FORMREPORT])) { $this->linearizeArray($GLOBALS['TYPO3_CONF_VARS'][FORMREPORT], $tmp); + } // remove anything that contains "_password" "_username" in the key to prevent the webmaster from doing something stupid foreach ($tmp as $key => $value) { diff --git a/extension/qfq/qfq/store/FillStoreForm.php b/extension/qfq/qfq/store/FillStoreForm.php index 34b959b53403aad6f83a05bfed50a38332853cd7..604f15ec3195c94819c11e1c32c982933b5ed909 100644 --- a/extension/qfq/qfq/store/FillStoreForm.php +++ b/extension/qfq/qfq/store/FillStoreForm.php @@ -31,13 +31,21 @@ class FillStoreForm { */ private $feSpecNative = array(); + /** + * @var Evaluate + */ + private $evaluate = null; + /** * */ public function __construct() { + $this->store = Store::getInstance(); $this->db = new Database(); $this->feSpecNative = $this->loadFormElementsBasedOnSIP(); + $this->evaluate = new Evaluate($this->store, $this->db); + } /** @@ -80,7 +88,7 @@ class FillStoreForm { // Retrieve SIP vars, e.g. for HIDDEN elements. $sipValues = $this->store->getStore(STORE_SIP); - // Copy SIP Values; not necessarily defines as FormElements. + // Copy SIP Values; not necessarily defined as a FormElement. foreach ($sipValues as $key => $value) { switch ($key) { case SIP_SIP: @@ -96,6 +104,11 @@ class FillStoreForm { } } + // Check if there is a 'new record already saved' situation: + // yes: the names of the input fields are submitted with '<fieldname>:0' instead of '<fieldname>:<id>' + // no: regular situation, take real 'recordid' + $fakeRecordId = isset($sipValues[SIP_MAKE_URLPARAM_UNIQ]) ? 0 : $sipValues[SIP_RECORD_ID]; + // Iterate over all formelements. Sanatize values. Built an assoc array $newValues. foreach ($this->feSpecNative AS $formElement) { @@ -106,88 +119,107 @@ class FillStoreForm { // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); - // evaluate current FormElement -// $evaluate = new Evaluate($this->store, $this->db); -// $formElement = $evaluate->parseArray($fe, $debugStack); + // Evaluate current FormElement: e.g. FE_MODE_SQL + $formElement = $this->evaluate->parseArray($formElement, $debugStack); - // Get related formElement. - // construct the field name used in the form - $clientFieldName = HelperFormElement::buildFormElementId($formElement['name'], $sipValues[SIP_RECORD_ID]); + // Get related formElement. Construct the field name used in the form. + $clientFieldName = HelperFormElement::buildFormElementName($formElement['name'], $fakeRecordId); // Some Defaults $formElement = Support::setFeDefaults($formElement); - // Preparation for Log, Debug -// $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM); - - if ($formElement[FE_TYPE] == 'hidden') { - // Hidden elements will be transferred by SIP + if ($formElement[FE_TYPE] === FE_TYPE_EXTRA) { + // Extra 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); + throw new CodeException("Missing the " . FE_TYPE_EXTRA . " field '" . $formElement['name'] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP); } $newValues[$formElement['name']] = $sipValues[$formElement['name']]; continue; } + // Checkbox Multi: collect values + if ($formElement[FE_TYPE] === 'checkbox') { + $clientValues[$clientFieldName] = $this->collectMultiValues($clientFieldName, $clientValues); + } + 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[FE_MODE]) { - case FE_MODE_REQUIRED: - case FE_MODE_SHOW: - if (isset($clientValues[$clientFieldName])) { - - // SELECT MULTI or CHECKBOX MULTI: delivered as array - implode them. - if (is_array($clientValues[$clientFieldName])) { - // E.g. Checkboxes needs a 'HIDDEN' HTML input to detect 'unset' of values. These 'HIDDEN' element - // needs to be removed, if there is at least one checkbox is checked (=submitted) - if (count($clientValues[$clientFieldName]) > 1) - array_shift($clientValues[$clientFieldName]); - - $clientValues[$clientFieldName] = implode(',', $clientValues[$clientFieldName]); - } - - switch ($formElement[FE_TYPE]) { - case 'date': - case 'datetime': - case 'time': - if ($clientValues[$clientFieldName] !== '') // do not check empty values - $newValues[$formElement['name']] = $this->doDateTime($formElement, $clientValues[$clientFieldName]); - break; - default: + // copy value to $newValues + if (isset($clientValues[$clientFieldName])) { + if ($formElement[FE_DYNAMIC_UPDATE] === 'yes' || $formElement[FE_MODE] === FE_MODE_REQUIRED || $formElement[FE_MODE] === FE_MODE_SHOW) { + switch ($formElement[FE_TYPE]) { + case 'date': + case 'datetime': + case 'time': + if ($clientValues[$clientFieldName] !== '') // do not check empty values + $newValues[$formElement['name']] = $this->doDateTime($formElement, $clientValues[$clientFieldName]); + break; + default: + // Check only if their is something + if($clientValues[$clientFieldName] !== '') { $newValues[$formElement['name']] = Sanitize::sanitize($clientValues[$clientFieldName], $formElement['checkType'], $formElement['checkPattern'], SANATIZE_EXCEPTION); - break; - } + } else { + $newValues[$formElement['name']] =''; + } + break; } - break; - - case FE_MODE_READONLY: - case FE_MODE_HIDDEN: - continue; - default: - throw new CodeException("Unknown mode: " . $formElement[FE_MODE], ERROR_UNKNOWN_MODE); + } } } $this->store->setVarArray($newValues, STORE_FORM, true); } + /** + * Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma seperated string (MYSQL ENUM type). + * If there is no element '_h_${clientFieldName}', than there no multi values - return the already given `$clientValues[$clientFieldName]`. + * + * @param $clientFieldName + * @param array $clientValues + * @return string + */ + private function collectMultiValues($clientFieldName, array $clientValues) { + + $checkboxKey = HelperFormElement::prependFormElementIdCheckBoxMulti($clientFieldName, 'h'); + + // Check there is a hidden value with naming in checkbox multi syntax + if (isset($clientValues[$checkboxKey])) { + $checkboxValue = $clientValues[$checkboxKey]; + + $pattern = '/' . HelperFormElement::prependFormElementIdCheckBoxMulti($clientFieldName, '\d+') . '/'; + foreach ($clientValues as $key => $value) { + if (1 === preg_match($pattern, $key)) { + $checkboxValue .= ',' . $value; + } + } + + if (isset($checkboxValue[0]) && $checkboxValue[0] === ',') { + $checkboxValue = substr($checkboxValue, 1); + } + + $clientValues[$clientFieldName] = $checkboxValue; + } + + return $clientValues[$clientFieldName]; + } + /** * Check $value as date/datime/time value and convert it to FORMAT_DATE_INTERNATIONAL. * - * @param array $formElement - if not set, set $formElement['dateFormat'] + * @param array $formElement - if not set, set $formElement[FE_DATE_FORMAT] * @param string $value - date/datetime/time value in format FORMAT_DATE_INTERNATIONAL or FORMAT_DATE_GERMAN * @return string - checked datetime string * @throws UserFormException */ private function doDateTime(array &$formElement, $value) { - $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement['dateFormat']); + $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT]); if (1 !== preg_match('/' . $regexp . '/', $value, $matches)) { $placeholder = Support::getDateTimePlaceholder($formElement); diff --git a/extension/qfq/qfq/store/Session.php b/extension/qfq/qfq/store/Session.php index 80a6f257099d51a54ce90da2f5c86ac769d54c09..9d0634de9c955b53ad5e6db9ffd54406f0d52e4f 100644 --- a/extension/qfq/qfq/store/Session.php +++ b/extension/qfq/qfq/store/Session.php @@ -10,12 +10,14 @@ namespace qfq; class Session { + private static $instance = null; private static $phpUnit = null; private static $sessionLocal = array(); /** * @param bool|false $phpUnit + * @throws CodeException */ private function __construct($phpUnit = false) { if (self::$phpUnit !== null) @@ -23,26 +25,52 @@ class Session { self::$phpUnit = $phpUnit; - if (self::$phpUnit) { + if (self::$phpUnit === true) { self::$sessionLocal = array(); } else { - session_name(); + session_name(SESSION_NAME); session_start(); } + self::checkFeUserUid(); } /** - * @param bool|false $phpUnit - * @return null|\qfq\Store + * Check if the feUserUid is stored in the session (even with 'false' which indicates not logged in user). + * If not, clear the session and save the feUser, feUserUid in the session. + * Check if the recent logged in feUserUid is equal to the one stored in session: If different, invalidate (clear) the session and + * save the new feUser, feUserUid in the session. + * If isset($GLOBALS["TSFE"]), than we're in a T3 environment, else we are called as API classes and need to fake + * feUser / feUserUid from previous stored session. + * It's neccessary to have feUser / feUserUid available in API classes, due to dynamic update which might reload data based on feUser / feUserUid. */ - public static function getInstance($phpUnit = false) { + private static function checkFeUserUid() { - // Design Pattern: Singleton - if (self::$instance === null) { - self::$instance = new self($phpUnit); + $feUserUidSession = Session::get(SESSION_FE_USER_UID); + $feUserSession = Session::get(SESSION_FE_USER); + $feUserGroup = false; + + if (isset($GLOBALS["TSFE"])) { + // if noone is logged in: 0 + $feUidLoggedIn = isset($GLOBALS["TSFE"]->fe_user->user["uid"]) ? $GLOBALS["TSFE"]->fe_user->user["uid"] : false; + $feUserSession = isset($GLOBALS["TSFE"]->fe_user->user["username"]) ? $GLOBALS["TSFE"]->fe_user->user["username"] : false; + $feUserGroup = isset($GLOBALS["TSFE"]->fe_user->user["usergroup"]) ? $GLOBALS["TSFE"]->fe_user->user["usergroup"] : false; + } else { + // If we are called through API there is no T3 environment. Assume nothing has changed, and fake the following check to always 'no change'. + $feUidLoggedIn = $feUserUidSession; } - return self::$instance; + if ($feUidLoggedIn !== $feUserUidSession) { + // destroy existing session store + Session::clearAll(); + + // save new feUserUid, feUserName + Session::set(SESSION_FE_USER_UID, $feUidLoggedIn); + Session::set(SESSION_FE_USER, $feUserSession); + Session::set(SESSION_FE_USER_GROUP, $feUserGroup); + +// throw new UserFormException("FYI: Session has been cleared. Reload this page. ". +// "feUserUidSession:'$feUserUidSession', feUserSession:'$feUserSession' isset(TSFE):'" . isset($GLOBALS["TSFE"]) ? 'true' : 'false' ); + } } /** @@ -57,8 +85,8 @@ class Session { else $value = false; } else { - if (isset($_SESSION[$key])) - $value = $_SESSION[$key]; + if (isset($_SESSION[SESSION_NAME][$key])) + $value = $_SESSION[SESSION_NAME][$key]; else $value = false; @@ -67,6 +95,18 @@ class Session { return $value; } + /** + * + */ + public static function clearAll() { + + if (self::$phpUnit) { + self::$sessionLocal = array(); + } else { + $_SESSION[SESSION_NAME] = array(); + } + } + /** * @param $key * @param $value @@ -76,19 +116,35 @@ class Session { if (self::$phpUnit) { self::$sessionLocal[$key] = $value; } else { - $_SESSION[$key] = $value; + $_SESSION[SESSION_NAME][$key] = $value; } } /** + * Unset the given $key + * + * @param $key */ - public static function clear() { + public static function unsetItem($key) { - if (self::$phpUnit) { - self::$sessionLocal = array(); - } else { - unset($_SESSION); + if (isset($_SESSION[SESSION_NAME][$key])) { + unset($_SESSION[SESSION_NAME][$key]); + } + + } + + /** + * @param bool|false $phpUnit + * @return Session class + */ + public static function getInstance($phpUnit = false) { + + // Design Pattern: Singleton + if (self::$instance === null) { + self::$instance = new self($phpUnit); } + + return self::$instance; } } \ No newline at end of file diff --git a/extension/qfq/qfq/store/Sip.php b/extension/qfq/qfq/store/Sip.php index 532ef6495c92c7108f6073699a40796d0e3ff4f8..748c0409eff83b9a60a33ce5372167e2309e1c87 100644 --- a/extension/qfq/qfq/store/Sip.php +++ b/extension/qfq/qfq/store/Sip.php @@ -25,36 +25,15 @@ require_once(__DIR__ . '/Session.php'); */ class Sip { -// $_SESSION['fe_user_uid'] = <fe_user_uid> -// $_SESSION[$sip] => <urlparam> >> $_SESSION['badcaffee1234'] => 'form=Person&r=1&z=5678' -// $_SESSION[$urlparam] => <sip> >> $_SESSION['form=Person&r=1&z=5678'] => 'badcaffee1234' - private $phpUnit = false; private $staticUniqId = false; - function __construct($sessionname, $phpUnit = false) { + /** + * @param bool|false $phpUnit + */ + function __construct($phpUnit = false) { $this->phpUnit = $phpUnit; - -// if ($sessionname == "") { -// throw new CodeException('Missing "sessionname"', ERROR_MISSING_SESSIONNAME); -// } -// -// session_name(); -// -// if ($phpUnit) { -// $_SESSION = null; -// } else { -// session_start(); -// } - - $feUserUid = Session::get(SESSION_FE_USER_UID); - - // Typo3: remember logged in FE User - if (isset($GLOBALS["TSFE"]->fe_user->user["uid"]) && $feUserUid === false) { - Session::set(SESSION_FE_USER_UID, $GLOBALS["TSFE"]->fe_user->user["uid"]); - } - } /** @@ -72,10 +51,7 @@ class Sip { * @throws CodeException * @throws UserFormException */ - public function queryStringToSip($queryString, $mode = RETURN_URL, $scriptName = 'index.php') { - - // Validate: Check if still the same fe_user is logged in. - $this->checkFeUserUid(); + public function queryStringToSip($queryString, $mode = RETURN_URL, $phpScriptName = INDEX_PHP) { $clientArray = array(); $sipArray = array(); @@ -83,30 +59,32 @@ class Sip { // Split URL parameter: $paramArray = KeyValueStringParser::parse($queryString, "=", "&"); + // If no 'r' is specified: define r=0 + if (!isset($paramArray[SIP_RECORD_ID])) { + $paramArray[SIP_RECORD_ID] = 0; + } + // Split parameter between Script, Client and SIP $script = $this->splitParamClientSip($paramArray, $clientArray, $sipArray); - // sort array to guarantee identical respresentation in $_SESSION. Param 'a, r, b, ...' should be saved as 'a, b, r, ..' - OnArray::sortKey($sipArray); - // Generate keyname for $_SESSION[] - $sipParamString = OnArray::toString($sipArray); + $sipParamString = $this->buildParamStringFromArray($sipArray); $sessionParamSip = Session::get($sipParamString); - if ($sessionParamSip !== false) { - $s = $sessionParamSip; - } else { + if ($sessionParamSip === false) { // Not found: create new entry - $s = $this->sipUniqId(); + $s = $this->sipUniqId('badcaffee1234'); Session::set($sipParamString, $s); Session::set($s, $sipParamString); + } else { + $s = $sessionParamSip; } // Append SIP to final parameter $clientArray[CLIENT_SIP] = $s; if ($script[0] === '?') - $script = $scriptName . $script; + $script = $phpScriptName . $script; $clientArray['_url'] = $script . OnArray::toString($clientArray); @@ -127,22 +105,6 @@ class Sip { return $rc; } - /** - * - */ - private function checkFeUserUid() { - - // Validate: Check if still the same fe_user is logged in. - if (isset($GLOBALS["TSFE"]->fe_user->user["uid"])) { - $feUserUid = Session::get(SESSION_FE_USER_UID); - - if ($feUserUid !== false && $feUserUid != $GLOBALS["TSFE"]->fe_user->user["uid"]) { - Session::clear(); - } - } - - } - /** * Splits the $paramArray in &$clientArray and &$sipArray. $sipArray contains all key/values pairs wich are not belong to Typo3. * @@ -182,7 +144,8 @@ class Sip { case CLIENT_SIP: throw new CodeException('SIP Parameter ist not allowed to be stored as a regular URL Parameter', ERROR_SIP_NOT_ALLOWED_AS_PARAM); default: - $sipArray[$key] = $value; + // Values in SIP should not urlencoded. + $sipArray[$key] = urldecode($value); break; } } @@ -218,6 +181,36 @@ class Sip { return $script; } + /** + * Takes the values form an array and creates a urlparamstring. Skip values which should not passed to the urlparamstring. + * - SIP_TARGET_URL is necessary for 'delete' links (via 'report') - may be unecessary in other situations. + * + * @param array $sipArray + * @return string + */ + private function buildParamStringFromArray(array $sipArray) { + $tmpArray = array(); + + foreach ($sipArray as $key => $value) { + switch ($key) { + case SIP_SIP: +// case SIP_MODE_ANSWER: +// case SIP_TABLE: + case SIP_URLPARAM: + break; + + case SIP_TARGET_URL: // Do not skip this param. Necessary for delete links (via 'report') - specifies the target where to jump after delete,php has been called (plain HTML, not AJAX) + default: + $tmpArray[$key] = $value; + break; + } + } + + OnArray::sortKey($tmpArray); + + return OnArray::toString($tmpArray); + } + /** * Returns a new uniqid, which will be used as a SIP identifier * @@ -235,6 +228,25 @@ class Sip { return uniqid(); } + /** + * Update the SIP in the Session according $sipArray. + * + * @param array $sipArray + */ + public function updateSipToSession(array $sipArray) { + $sip = $sipArray[SIP_SIP]; + + // Remove old entry, cause the the 'key' will change. + $sipParamStringOld = Session::get($sipArray[SIP_SIP]); + Session::unsetItem($sipParamStringOld); + + // Generate keyname for $_SESSION[] + $sipParamStringNew = $this->buildParamStringFromArray($sipArray); + + Session::set($sip, $sipParamStringNew); + Session::set($sipParamStringNew, $sip); + } + /** * Retrieve Params stored in $_SESSION[$s] * @@ -246,12 +258,12 @@ class Sip { public function getVarsFromSip($s) { # Check if parameter is manipulated - if (strlen($s) != 13) { + if (strlen($s) != SIP_TOKEN_LENGTH) { throw new UserFormException("Broken Parameter", ERROR_BROKEN_PARAMETER); } // Validate: Check if still the same fe_user is logged in. - $this->checkFeUserUid(); +// $this->checkFeUserUid(); # Check if index 's' exists. $sessionVar = Session::get($s); diff --git a/extension/qfq/qfq/store/Store.php b/extension/qfq/qfq/store/Store.php index d8313ea737812f6f394a33a3a44c9f022334b483..cffcd4cd5cdd956845e30345894a9b93b911539e 100644 --- a/extension/qfq/qfq/store/Store.php +++ b/extension/qfq/qfq/store/Store.php @@ -17,6 +17,7 @@ require_once(__DIR__ . '/../../qfq/helper/KeyValueStringParser.php'); require_once(__DIR__ . '/../../qfq/helper/Sanitize.php'); require_once(__DIR__ . '/../../qfq/Constants.php'); require_once(__DIR__ . '/../../qfq/store/Sip.php'); +//require_once(__DIR__ . '/../../qfq/store/Session.php'); require_once(__DIR__ . '/../../qfq/Database.php'); @@ -45,6 +46,11 @@ class Store { */ private static $sip = null; + /** + * @var Session Instance of class Session + */ +// private static $session = null; + /** * @var array Stores all indiviudal stores with the variable raw values * @@ -73,11 +79,14 @@ class Store { private static $phpUnit = false; + /** * @param string $bodytext */ private function __construct($bodytext = '') { +// self::$session = Session::getInstance(self::$phpUnit); + self::$sanitizeClass = [ // TYPO3_DEBUG_LOAD => SANITIZE_ALLOW_DIGIT, // TYPO3_DEBUG_SAVE => SANITIZE_ALLOW_DIGIT, @@ -141,7 +150,9 @@ class Store { STORE_TABLE_COLUMN_TYPES => false, STORE_CLIENT => true, STORE_TYPO3 => false, + STORE_VAR => false, STORE_ZERO => false, + STORE_EMPTY => false, STORE_SYSTEM => false, STORE_EXTRA => false ]; @@ -151,17 +162,34 @@ class Store { self::fillStoreClient(); self::fillStoreSip(); self::fillStoreExtra(); + + } /** + * Fills the system store. + * * @throws CodeException * @throws qfq\UserFormException */ private static function fillSystemStore() { + + // PHPUnit Path to CONFIG_INI + $configIni = __DIR__ . '/../../../' . CONFIG_INI; + if (!file_exists($configIni)) { + // Production Path to CONFIG_INI + $configIni = __DIR__ . '/../../../../../' . CONFIG_INI; + + if (!file_exists($configIni)) { + throw new qfq\UserFormException ("Config not found: " . getcwd() . "/" . $configIni, ERROR_IO_READ_FILE); + } + } + try { //TODO: Vernuenftige Fehlermeldung falls nicht auf qfq.ini zugegriffen werden kann. //TODO: sinnvollen Platz fuer qfq.ini bestimmen. In der Installationsdoku erwaehnen. - $config = parse_ini_file(__DIR__ . '/../../../' . CONFIG_INI, false); +// $config = parse_ini_file(__DIR__ . '/../../../' . CONFIG_INI, false); + $config = parse_ini_file($configIni, false); //TODO: auskommentiert weil dann die Unittests nicht mehr laufen. Sollte eigentlich wieder aktiviert werden. // $config['SQLLOG'] = Support::ifRelativePathPrependExtensionPath($config['SQLLOG']); @@ -207,11 +235,20 @@ class Store { $config[SYSTEM_SQL_LOG] = $config[SYSTEM_PATH_EXT] . '/' . $config[SYSTEM_SQL_LOG]; } + // Verify existence + $names = array('DB_USER', 'DB_SERVER', 'DB_PASSWORD', 'DB_NAME', 'SQL_LOG', 'SQL_LOG_MODE'); + foreach ($names as $name) { + if (!isset($config[$name])) { + throw new qfq\UserFormException ("Missing configuration in `config.ini`: $name", ERROR_MISSING_CONFIG_INI_VALUE); + } + } self::setVarArray($config, STORE_SYSTEM, true); } /** + * Set or overwrite a complete store. + * * @param array $dataArray * @param $store * @param bool|false $flagOverwrite @@ -219,11 +256,11 @@ class Store { * @throws \qfq\CodeException */ public static function setVarArray(array $dataArray, $store, $flagOverwrite = false) { + // Check valid Storename if (!isset(self::$sanitizeStore)) throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE); - if ($store === STORE_ZERO) throw new CodeException("setVarArray() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO); @@ -235,38 +272,64 @@ class Store { } /** + * Copy the BodyText as well as some T3 specific vars to STORE_TYPO3. + * Attention: if called through API, there is no T3 environment. The only values which are available are fe_user and fe_user_uid. + * * @param $bodytext * @throws CodeException */ private static function fillStoreTypo3($bodytext) { + // form=, showDebugBodyText=, 10.20.. $arr = KeyValueStringParser::parse($bodytext, "=", "\n"); - if (isset($GLOBALS["TSFE"]->fe_user->user["username"])) - $arr[TYPO3_FE_USER] = $GLOBALS["TSFE"]->fe_user->user["username"]; + if (isset($GLOBALS["TSFE"])) { - if (isset($GLOBALS["TSFE"]->fe_user->user["uid"])) - $arr[TYPO3_FE_USER_UID] = $GLOBALS["TSFE"]->fe_user->user["uid"]; + if (isset($GLOBALS["TSFE"]->fe_user->user["username"])) { + $arr[TYPO3_FE_USER] = $GLOBALS["TSFE"]->fe_user->user["username"]; + } + + if (isset($GLOBALS["TSFE"]->fe_user->user["uid"])) { + $feUid = $GLOBALS["TSFE"]->fe_user->user["uid"]; + $arr[TYPO3_FE_USER_UID] = $GLOBALS["TSFE"]->fe_user->user["uid"]; + } + + if (isset($GLOBALS["TSFE"]->fe_user->user["usergroup"])) { + $arr[TYPO3_FE_USER_GROUP] = $GLOBALS["TSFE"]->fe_user->user["usergroup"]; + } - if (isset($GLOBALS["TSFE"]->fe_user->user["usergroup"])) - $arr[TYPO3_FE_USER_GROUP] = $GLOBALS["TSFE"]->fe_user->user["usergroup"]; + if (isset($GLOBALS["TSFE"]->page["uid"])) { + $arr[TYPO3_TT_CONTENT_UID] = $GLOBALS["TSFE"]->page["uid"]; + } - if (isset($GLOBALS["TSFE"]->page["uid"])) - $arr[TYPO3_TT_CONTENT_UID] = $GLOBALS["TSFE"]->page["uid"]; + if (isset($GLOBALS["TSFE"]->id)) { + $arr[TYPO3_PAGE_ID] = $GLOBALS["TSFE"]->id; + } - if (isset($GLOBALS["TSFE"]->id)) - $arr[TYPO3_PAGE_ID] = $GLOBALS["TSFE"]->id; + if (isset($GLOBALS["TSFE"]->type)) { + $arr[TYPO3_PAGE_TYPE] = $GLOBALS["TSFE"]->type; + } - if (isset($GLOBALS["TSFE"]->type)) - $arr[TYPO3_PAGE_TYPE] = $GLOBALS["TSFE"]->type; + if (isset($GLOBALS["TSFE"]->sys_language_uid)) { + $arr[TYPO3_PAGE_LANGUAGE] = $GLOBALS["TSFE"]->sys_language_uid; + } - if (isset($GLOBALS["TSFE"]->sys_language_uid)) - $arr[TYPO3_PAGE_LANGUAGE] = $GLOBALS["TSFE"]->sys_language_uid; + } else { + + // NO T3 environment (called by API): restore from SESSION + foreach([ SESSION_FE_USER, SESSION_FE_USER_UID, SESSION_FE_USER_GROUP ] as $key) { + if (isset($_SESSION[SESSION_NAME][$key])) { + $arr[$key] = $_SESSION[SESSION_NAME][$key]; + } + } + } self::setVarArray($arr, STORE_TYPO3, true); } /** + * Fills the STORE_CLIENT + * * @throws CodeException */ private static function fillStoreClient() { @@ -285,13 +348,14 @@ class Store { } /** + * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP. + * * @throws CodeException * @throws UserFormException */ private static function fillStoreSip() { - $sessionName = self::getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM); - self::$sip = new Sip($sessionName); + self::$sip = new Sip(self::$phpUnit); $s = self::getVar(CLIENT_SIP, STORE_CLIENT); if ($s !== false) { @@ -370,18 +434,29 @@ class Store { } /** + * Fills the STORE_EXTRA. + * * @throws UserFormException * @throws \qfq\CodeException */ private static function fillStoreExtra() { + $value = Session::get(STORE_EXTRA); - if ($value === false) + + if (!isset($_SESSION[SESSION_NAME][STORE_EXTRA]) || $_SESSION[SESSION_NAME][STORE_EXTRA] === null) { + $value = false; + } + + if ($value === false) { self::setVarArray(array(), STORE_EXTRA, true); - else - self::setVarArray($_SESSION[STORE_EXTRA], STORE_EXTRA, true); + } else { + self::setVarArray($_SESSION[SESSION_NAME][STORE_EXTRA], STORE_EXTRA, true); + } } /** + * Returns a pointer to this Class. + * * @param string $bodytext * @param bool|false $phpUnit * @return null|\qfq\Store @@ -421,6 +496,8 @@ class Store { } /** + * Deletes a store assigning a new empty array to it. + * * @param $store * @throws UserFormException * @throws \qfq\CodeException @@ -440,6 +517,8 @@ class Store { } /** + * Set's a single $key/$value pair $store. + * * @param string $key * @param string|array $value * @param string $store @@ -456,34 +535,49 @@ class Store { throw new CodeException("setVar() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO); if ($overWrite === false && isset(self::$raw[$store][$key])) { - throw new UserFormException("Value of '$key' already be set in store '$store'.", ERROR_STORE_KEY_EXIST); + throw new UserFormException("Value of '$key' already set in store '$store'.", ERROR_STORE_KEY_EXIST); } self::$raw[$store][$key] = $value; // The STORE_EXTRA saves arrays and is persistent if ($store === STORE_EXTRA) { + $store = Session::get(STORE_EXTRA); - if ($store === false) + + if ($store === false) { $store = array(); + } + $store[$key] = $value; Session::set(STORE_EXTRA, $store); } - } /** + * Create a SIP after a form load. This is necessary on forms without a sip and on forms with r=0 (new record). + * * @param $formName * @throws CodeException */ public static function createSipAfterFormLoad($formName) { + $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT); if ($recordId === false) { $recordId = 0; } - $tmpParam = [SIP_RECORD_ID => $recordId, SIP_FORM => $formName]; + // If there are existing SIP param, keep them by copying to the new SIP Param Array + $tmpParam = self::getNonSystemSipParam(); + + $tmpParam[SIP_RECORD_ID] = $recordId; + $tmpParam[SIP_FORM] = $formName; + + if ($recordId == 0) { + // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter + $tmpParam[SIP_MAKE_URLPARAM_UNIQ] = uniqid(); + } // Construct fake urlparam $tmpUrlparam = OnArray::toString($tmpParam); @@ -494,18 +588,42 @@ class Store { // Store in SIP Store (cause it's empty until now). $tmpParam[SIP_SIP] = $sip; - self::setVarArray($tmpParam, STORE_SIP); + self::setVarArray($tmpParam, STORE_SIP, true); } /** - * @return null|Sip + * Return an array with non system SIP parameter. Take the whole STORE_SIP and search for non system parameter. + * @return array + * @throws UserFormException + * @throws \qfq\CodeException */ - public static function getSipInstance() { - return self::$sip; + private static function getNonSystemSipParam() { + $tmpParam = array(); + + $sipArray = self::getStore(STORE_SIP); + + foreach ($sipArray as $key => $value) { + if ($key[0] === '_') { + continue; + } + switch ($key) { + case SIP_SIP: + case SIP_RECORD_ID: + case SIP_FORM; + case SIP_URLPARAM: + continue; + default: + $tmpParam[$key] = $value; + } + } + + return $tmpParam; } /** + * Returns a complete $store. + * * @param $store * @return array * @throws UserFormException @@ -522,9 +640,18 @@ class Store { if (isset(self::$raw[$store])) { return self::$raw[$store]; } + return array(); } + /** + * Returns a pointer to this class. + * + * @return null|Sip + */ + public static function getSipInstance() { + return self::$sip; + } /** * Fills STORE_TABLE_DEFAULT and STORE_TABLE_COLUMN_TYPES diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index a6492971848731adbea0e5ee83e814223a62e213..9f92f63073f7974837bd59aae693d5cfb35fb231 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -1,4 +1,4 @@ -DROP TABLE IF EXISTS `Form`; +#DROP TABLE IF EXISTS `Form`; CREATE TABLE IF NOT EXISTS `Form` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL DEFAULT '', @@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS `Form` ( `permitNew` ENUM('sip', 'logged_in', 'logged_out', 'always', 'never') NOT NULL DEFAULT 'sip', `permitEdit` ENUM('sip', 'logged_in', 'logged_out', 'always', 'never') NOT NULL DEFAULT 'sip', - `render` ENUM('plain', 'table', 'bootstrap') NOT NULL DEFAULT 'plain', + `render` ENUM('plain', 'table', 'bootstrap') NOT NULL DEFAULT 'bootstrap', + `requiredParameter` VARCHAR(255) NOT NULL DEFAULT '', `showButton` SET('new', 'delete', 'close', 'save') NOT NULL DEFAULT 'new,delete,close,save', `multiMode` ENUM('none', 'horizontal', 'vertical') NOT NULL DEFAULT 'none', `multiSql` TEXT NOT NULL, @@ -37,6 +38,7 @@ CREATE TABLE IF NOT EXISTS `Form` ( DEFAULT CHARSET = utf8 AUTO_INCREMENT = 0; + #-- #-- Triggers `Form` #-- @@ -51,7 +53,7 @@ CREATE TABLE IF NOT EXISTS `Form` ( # ---------------------------------------- # FormElement -DROP TABLE IF EXISTS `FormElement`; +#DROP TABLE IF EXISTS `FormElement`; CREATE TABLE IF NOT EXISTS `FormElement` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `formId` INT(11) NOT NULL, @@ -63,13 +65,13 @@ CREATE TABLE IF NOT EXISTS `FormElement` ( `name` VARCHAR(255) NOT NULL DEFAULT '', `label` VARCHAR(255) NOT NULL DEFAULT '', - `mode` ENUM('show', 'required', 'readonly', 'hidden') NOT NULL DEFAULT 'show', + `mode` ENUM('show', 'required', 'readonly', 'hidden') NOT NULL DEFAULT 'show', + `modeSql` TEXT NOT NULL, `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', - 'before_load', 'before_save', 'before_insert', 'before_update', 'before_delete', 'after_load', - 'after_save', 'after_insert', 'after_update', 'after_delete', 'feGroup', - 'sendmail') NOT NULL DEFAULT 'text', + `type` ENUM('checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', 'gridJQW', 'text', + 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', + 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', + 'afterSave', 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail') NOT NULL DEFAULT 'text', `subrecordOption` SET('edit', 'delete', 'new') NOT NULL DEFAULT '', `checkType` ENUM('alnumx', 'digit', 'email', 'min|max', 'min|max date', 'pattern', 'allbut', 'all') NOT NULL DEFAULT 'alnumx', `checkPattern` VARCHAR(255) NOT NULL DEFAULT '', @@ -81,9 +83,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 '', + `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 '', @@ -118,6 +120,13 @@ CREATE TABLE IF NOT EXISTS `FormElement` ( #// #DELIMITER ; +# Delete previous FormElements (if exist) +DELETE FormElement FROM FormElement, Form +WHERE Form.name LIKE 'form' OR Form.name LIKE 'formElement' AND Form.id = FormElement.formId; + +# Delete previous Forms (if exist) +DELETE FROM Form +WHERE name LIKE 'form' OR name LIKE 'formElement'; # # FormEditor: Form @@ -125,109 +134,152 @@ INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, r ('form', 'Form Editor: {{SELECT id, " / ", name FROM Form WHERE id = {{r:S0}}}}', 'Please secure the form', 'Form', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=5\nclass=container-fluid'); -# FormEditor: FormElements -INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption) +# FormEditor: FormElements for 'form' +INSERT INTO FormElement (formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) VALUES - (1, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', '', 0, ''), - (1, 'permission', 'Permission', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), - (1, 'various', 'Various', 'show', 'pill', 'all', 'container', 30, 0, 0, '', '', '', '', '', '', 0, ''), - (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, 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, ''), - - (1, 'permitNew', 'Permit New', 'show', 'radio', 'all', 'native', 160, 0, 0, '', '', '', '', '', '', 2, ''), - (1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'all', 'native', 170, 0, 0, '', '', '', '', '', '', 2, ''), - (1, 'render', 'Render', 'show', 'radio', 'all', 'native', 190, 0, 0, '', '', '', '', '', '', 2, ''), - (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, 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, 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, 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, 0, 255, '', '', '', '', '', '', 4, - ''), - (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 250, 0, 255, '', '', - '', '', - '', '', 4, ''), + (1, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', '', 0, '', ''), + (1, 'access', 'Access', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, '', ''), + (1, 'various', 'Various', 'show', 'pill', 'all', 'container', 30, 0, 0, '', '', '', '', '', '', 0, '', ''), + (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, 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, '', ''), + + (1, 'requiredParameter', 'Required Parameter', 'show', 'text', 'all', 'native', 200, 0, 255, '', '', '', '', '', '', 2, '', ''), + (1, 'permitNew', 'Permit New', 'show', 'radio', 'all', 'native', 210, 0, 3, '', '', '', '', '', '', 2, '', ''), + (1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'all', 'native', 220, 0, 3, '', '', '', '', '', '', 2, '', ''), + (1, 'render', 'Render', 'show', 'radio', 'all', 'native', 230, 0, 3, '', '', '', '', '', '', 2, '', ''), + (1, 'showButton', 'Show button', 'show', 'checkbox', 'all', 'native', 240, 0, 5, '', '', '', '', '', 'checkBoxMode = multi\norientation=vertical', 2, '', ''), + + (1, 'forwardMode', 'Forward', 'show', 'radio', 'all', 'native', 300, 0, 0, '', '', '', '', '', '', 3, '', ''), + (1, 'forwardPage', 'Forward Page', 'show', 'text', 'all', 'native', 310, 0, 255, '', '', '', '', '', '', 3, '', ''), + (1, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 320, '40,3', 0, '', '', '', '', '', '', 3, '', ''), + (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 330, 0, 250, '', '', '', '', '', '', 3, '', ''), + (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 340, 0, 250, '', '', '', '', '', '', 3, '', ''), + (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 350, 0, 250, '', '', '', '', '', '', 3, '', ''), + (1, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 360, 0, 0, '', '', '', '', '', '', 3, '', ''), + (1, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 370, 0, 20, '', '', '', '', '', '', 3, '', ''), + (1, 'created', 'Created', 'readonly', 'text', 'all', 'native', 380, 0, 20, '', '', '', '', '', '', 3, '', ''), + + (1, 'multi', 'Multi', 'show', 'fieldset', 'all', 'native', 400, 0, 0, '', '', '', '', '', '', 4, '', ''), + (1, 'multiMode', 'Multi Mode', 'show', 'radio', 'all', 'native', 410, 0, 0, '', '', '', '', '', '', 4, '', ''), + (1, 'multiSql', 'Multi SQL', 'show', 'text', 'all', 'native', 420, '40,3', 0, '', '', '', '', '', '', 4, '', ''), + (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'all', 'native', 430, 0, 255, '', '', '', '', '', '', 4, + '', ''), + (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'all', 'native', 440, 0, 255, '', '', + '', '', '', '', 4, '', ''), (1, '', 'FormElements', 'show', 'subrecord', 'all', 'native', 500, 0, 0, '', '', '', - '{{!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'); + '{{!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, feC.ord, fe.ord, fe.id}}', + '', 'form=formElement\ndetail=id:formId', 5, 'new,edit,delete', ''); # # FormEditor: FormElement -INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES +INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter, requiredParameter) +VALUES ('formElement', 'Form Element Editor. Form : {{SELECT f.id, " / ", f.name FROM FormElement AS fe, Form AS f WHERE fe.id = {{r:S0}} AND fe.formId=f.id }}', 'Please secure the form', - 'FormElement', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=5'); + 'FormElement', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=5\nclassBody=qfq-color-blue-1', 'formId'); -# FormEditor: FormElements +# FormEditor: FormElements for 'formElement' INSERT INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, - sql1, sql2, parameter, feIdContainer, subrecordOption) + sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) VALUES - (100, 2, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', '', 0, ''), - (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), - (102, 2, 'layout', 'Layout', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), - (103, 2, 'value', 'Value', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), - (104, 2, 'info', 'Info', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''); + (100, 2, 'basic', 'Basic', 'show', 'pill', 'all', 'container', 10, 0, 0, '', '', '', '', '', '', 0, '', ''), + (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, '', + ''), + (102, 2, 'layout', 'Layout', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, '', ''), + (103, 2, 'value', 'Value', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, '', ''), + (104, 2, 'info', 'Info', 'show', 'pill', 'all', 'container', 20, 0, 0, '', '', '', '', '', '', 0, '', ''); -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) +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, modeSql) VALUES - (2, 'id', 'id', 'readonly', 'text', 'all', 'native', 100, 0, 11, '', '', '', '', '', '', 100, '', 'no', '', '', ''), - (2, 'formId', 'formId', 'readonly', 'text', 'all', 'native', 110, 0, 255, '', '', '', '', '', '', 100, '', 'no', '', '', ''), + (2, '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, '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, 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', '', - '', ''); + '{{!SELECT fe.id, CONCAT(fe.class, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{formId:S0}} 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, 'On change, this element will be updated and trigger other.', '', '', '', '', '', 100, '', 'no', '3', '2', '7', ''), + (2, 'name', 'Name', 'show', 'text', 'all', 'native', 140, 0, 255, '', '', '', '', '', '', 100, '', 'no', '', '', '', ''), + (2, 'label', 'Label', 'show', 'text', 'all', 'native', 150, 0, 255, '', '', '', '', '', '', 100, '', 'no', '', '', '', ''), + (2, 'mode', 'Mode', 'show', 'radio', 'all', 'native', 160, 0, 255, '', '', '', '', '', '', 100, '', 'no', '', '', '', ''), + (2, 'modeSql', 'Mode sql', 'show', 'text', 'all', 'native', 170, '70,2', 255, '', '', '', '', '', '', 100, '', 'no', '', '', '', ''), + (2, 'class', 'Class', 'show', 'select', 'all', 'native', 180, 0, 255, '', '', '{{class:FSRD0:alnumx}}', '', '', '', 100, '', 'yes', '', '', '', ''), + (2, 'type', 'Type', 'show', 'select', 'all', 'native', 190, 0, 255, '', '', '', '', '', + 'itemList={{SELECT IF( "{{class:FRD0:alnumx}}"="native","checkbox,date,time,datetime,dateJQW,datetimeJQW,extra,gridJQW,text,editor,note,password,radio,select,subrecord,upload", IF("{{class:FRD0:alnumx}}"="action","beforeLoad,beforeSave,beforeInsert,beforeUpdate,beforeDelete,afterLoad,afterSave,afterInsert,afterUpdate,afterDelete,sendMail", "fieldset,pill") ) }}', + 100, '', 'yes', '', '', '', ''), + (2, 'subrecordOption', 'Subrecord Option', 'show', 'checkbox', 'all', 'native', 200, 0, 0, '', '', '', '', '', '', 100, '', 'no', '', '', '', + ''), + (2, 'checkType', 'Check Type', 'show', 'select', 'all', 'native', 300, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', '', ''), + (2, 'checkPattern', 'Check Pattern', 'show', 'text', 'all', 'native', 310, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', '', ''), + (2, 'onChange', 'JS onChange', 'show', 'text', 'all', 'native', 320, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', '', ''), + (2, 'ord', 'Order', 'show', 'text', 'all', 'native', 330, 0, 255, '', '', + '{{SELECT IF({{ord:R0}}=0, MAX(IFNULL(fe.ord,0))+10,{{ord:R0}}) FROM (SELECT 1) AS a LEFT JOIN FormElement AS fe ON fe.formId={{formId:S0}} GROUP BY fe.formId}}', + '', '', '', 101, '', 'no', '', '', '', ''), + (2, 'tabindex', 'tabindex', 'show', 'text', 'all', 'native', 340, 0, 255, '', '', '', '', '', '', 101, '', 'no', '', '', '', ''), + (2, 'size', 'Size', 'show', 'text', 'all', 'native', 400, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'all', 'native', 410, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'all', 'native', 420, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'all', 'native', 430, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 440, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'note', 'note', 'show', 'text', 'all', 'native', 450, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 460, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 470, 0, 255, '', '', '', '', '', '', 102, '', 'no', '', '', '', ''), + (2, 'value', 'value', 'show', 'text', 'all', 'native', 500, '40,2', 255, '', '', '', '', '', '', 103, '', 'no', '', '', '', ''), + (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 510, '40,5', 255, '', '', '', '', '', '', 103, '', 'no', '', '', '', ''), + (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 520, '40,4', 255, 'MariaDB: <a href="https://mariadb.com/kb/en/mariadb/select/">Select</a>, <a href="https://mariadb.com/kb/en/mariadb/functions-and-operators/">Functions</a>', '', '', '', '', '', 103, '', + 'no', '', '', '', ''), + (2, 'clientJs', 'ClientJS', 'show', 'text', 'all', 'native', 530, 0, 255, '', '', '', '', '', '', 103, '', 'no', '', '', '', ''), + (2, 'feGroup', 'feGroup', 'show', 'text', 'all', 'native', 600, 0, 255, '', '', '', '', '', '', 104, '', 'no', '', '', '', ''), + (2, 'deleted', 'Deleted', 'show', 'checkbox', 'all', 'native', 610, 0, 0, '', '', '', '', '', '', 104, '', 'no', '', '', '', ''), + (2, 'modified', 'Modified', 'readonly', 'text', 'all', 'native', 620, 0, 20, '', '', '', '', '', '', 104, '', 'no', + '', '', '', ''), + (2, 'created', 'Created', 'readonly', 'text', 'all', 'native', 630, 0, 20, '', '', '', '', '', '', 104, '', 'no', '', + '', '', ''); + +# ---------------------------------------- +# MailLog + +DROP TABLE IF EXISTS `MailLog`; +CREATE TABLE `MailLog` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `grId` INT(11) NOT NULL DEFAULT '0', + `xId` INT(11) NOT NULL DEFAULT '0', + `receiver` TEXT NOT NULL DEFAULT '', + `sender` VARCHAR(255) NOT NULL DEFAULT '', + `subject` VARCHAR(255) NOT NULL DEFAULT '', + `body` TEXT NOT NULL DEFAULT '', + `header` VARCHAR(255) NOT NULL DEFAULT '', + `attach` VARCHAR(255) NOT NULL DEFAULT '', + `src` VARCHAR(255) NOT NULL DEFAULT '', + `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + + PRIMARY KEY (`id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8 + AUTO_INCREMENT = 0; + + +#-- +#-- Triggers `MailLog` +#-- +#DROP TRIGGER IF EXISTS `on_MailLog_update_modified`; +#DELIMITER // +#CREATE TRIGGER `on_MailLog_update_modified` BEFORE UPDATE ON `MailLog` +#FOR EACH ROW SET NEW.modified = +#current_timestamp() +#// +#DELIMITER ; diff --git a/extension/qfq/sql/testtables.sql b/extension/qfq/sql/testtables.sql index 877bb804cf52d6dde20cb8effe2db1d281ac03b6..ccb4c0ebcb1eb22ae3a3f65de3dd23fffa46a41e 100644 --- a/extension/qfq/sql/testtables.sql +++ b/extension/qfq/sql/testtables.sql @@ -1,11 +1,15 @@ DROP TABLE IF EXISTS Person; CREATE TABLE Person ( - 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' + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(128) NOT NULL DEFAULT '', + firstname VARCHAR(128) NOT NULL DEFAULT '', + pathFileName VARCHAR(128) NOT NULL DEFAULT '', + gender ENUM('', 'male', 'female') NOT NULL DEFAULT 'male', + groups SET('', 'a', 'b', 'c') NOT NULL DEFAULT '', + birthday DATE NOT NULL DEFAULT '0000-00-00', + noteId BIGINT NOT NULL DEFAULT 0, + modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' ); # @@ -17,12 +21,14 @@ INSERT INTO Person (id, name, firstname, gender, groups) VALUES DROP TABLE IF EXISTS PersFunction; CREATE TABLE PersFunction ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, + 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) + start DATE NOT NULL DEFAULT '0000-00-00', + end DATE NOT NULL DEFAULT '0000-00-00', + note VARCHAR(255), + modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' ); # --------------------------- @@ -31,20 +37,20 @@ REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEd (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) +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) 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, ''); + (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) +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) 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, ''); + (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 @@ -53,38 +59,75 @@ REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEd '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) +# FormEditor: FormElements person +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) 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, ''), + (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', 'radio', 'alnumx', 'native', 40, 0, 0, '', '', '', '', '', 'itemList=female,male', - 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, '', '', '', '', '', - 'pathFileName={{SELECT ''fileadmin/user/pictures/'', p.name, ''-{{_filename}}'' FROM Person AS p WHERE p.id={{r}} }}', - 0, ''); + '', ''), + (504, 5, 'zeit', 'Zeit', 'show', 'time', 'alnumx', 'native', 60, 0, 0, '', '', '', '', '', '', 0, '', ''), + (505, 5, 'pathFileName', 'Picture', 'show', 'upload', 'allbut', 'native', 70, 0, 0, '', '', '', '', '', + 'fileDestination={{SELECT ''fileadmin/user/pictures/'', p.name, ''-{{_filename}}'' FROM Person AS p WHERE p.id={{r}} }}', + 0, '', ''), + + (506, 5, '', 'Address', 'show', 'subrecord', 'all', 'native', 100, 0, 0, '', '', '', + '{{!SELECT a.id, a.street, a.city, a.country FROM Address AS a WHERE a.personId={{r:S0}} }}', + '', 'form=address\ndetail=id:personId', 0, 'new,edit,delete', ''); + + +# Form: Address +REPLACE INTO Form (id, name, title, noteInternal, tableName, permitNew, permitEdit, render, multiSql, parameter) VALUES + (6, 'address', + 'Person {{SELECT ": ", p.firstName, " ", p.name, " (", id, ")" FROM Person AS p WHERE p.id = {{personId:S0}}}}', + '', + 'Address', 'always', 'always', 'bootstrap', '', ''); + +# FormEditor: FormElements address +REPLACE INTO FormElement (id, formId, name, label, mode, type, checkType, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, subrecordOption, modeSql) +VALUES + + (600, 6, 'street', 'Street', 'show', 'text', 'all', 'native', 10, 0, 255, '', '', '', '', '', '', 0, '', ''), + (601, 6, 'city', 'City', 'show', 'text', 'all', 'native', 20, 0, 255, '', '', '', '', '', '', 0, '', ''), + (602, 6, 'country', 'Country', 'show', 'select', 'all', 'native', 30, 0, 255, '', '', '', '', '', '', 0, '', ''); + # ---------------------------------------------------------------------- # DROP TABLE IF EXISTS Address; CREATE TABLE Address ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - personId BIGINT, - street VARCHAR(128), - city VARCHAR(128), - country ENUM('Switzerland', 'Austria', 'France', 'Germany'), - gr_id_typ BIGINT + id BIGINT AUTO_INCREMENT PRIMARY KEY, + personId BIGINT NOT NULL DEFAULT 0, + street VARCHAR(128) NOT NULL DEFAULT '', + city VARCHAR(128) NOT NULL DEFAULT '', + country ENUM('Switzerland', 'Austria', 'France', 'Germany') NOT NULL, + grIdTyp BIGINT NOT NULL DEFAULT 0, + modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' ); -INSERT INTO Address (personId, street, city) VALUES - (1, 'Side Street', 'Zurich'), - (1, 'Park Street', 'Zurich'), - (1, 'Winter Street', 'Zurich'), - (2, 'Summer Street', 'Zurich'); +INSERT INTO Address (personId, street, city, country) VALUES + (1, 'Side Street', 'Zurich', 'Switzerland'), + (1, 'Park Street', 'Wien', 'Austria'), + (1, 'Winter Street', 'Paris', 'France'), + (2, 'Summer Street', 'Berlin', 'Germany'); + +#------------------------------------------------------------------------ +# +DROP TABLE IF EXISTS Note; +CREATE TABLE Note ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + note VARCHAR(128) NOT NULL DEFAULT '', + xId BIGINT NOT NULL DEFAULT 0, + grIdTyp BIGINT NOT NULL DEFAULT 0, + modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' +); + + diff --git a/extension/qfq/tests/phpunit/AbstractDatabaseTest.php b/extension/qfq/tests/phpunit/AbstractDatabaseTest.php index 07d2dc72bd9579f55a465296532ceaf8ff4953df..77f09c72692bdb35335f280902228079d95bc08f 100644 --- a/extension/qfq/tests/phpunit/AbstractDatabaseTest.php +++ b/extension/qfq/tests/phpunit/AbstractDatabaseTest.php @@ -11,24 +11,33 @@ use qfq\Store; require_once(__DIR__ . '/../../qfq/Database.php'); require_once(__DIR__ . '/../../qfq/store/Store.php'); +require_once(__DIR__ . '/../../qfq/store/Sip.php'); /** * Class AbstractDatabaseTest */ abstract class AbstractDatabaseTest extends PHPUnit_Framework_TestCase { + /** * @var null */ static protected $mysqli = null; + /** * @var qfq\Database */ protected $db = null; + /* * @var qfq\Store */ protected $store = null; + /* + * @var qfq\Sip + */ + protected $sip = null; + /** * @param $filename * @param $ignoreError @@ -56,13 +65,16 @@ abstract class AbstractDatabaseTest extends PHPUnit_Framework_TestCase { protected function setUp() { // Init the store also reads db credential configuration - $this->store = \qfq\Store::getInstance('', true); + $this->store = qfq\Store::getInstance('', true); + + $this->sip = new qfq\Sip('fakesessionname', true); + $this->sip->sipUniqId('badcaffee1234'); // SWITCH to TestDB $this->store->setVar(SYSTEM_DB_NAME, $this->store->getVar(SYSTEM_DB_NAME_TEST, STORE_SYSTEM), STORE_SYSTEM); if ($this->db === null) { - $this->db = new \qfq\Database(); + $this->db = new qfq\Database(); } /// Establish additional mysqli access diff --git a/extension/qfq/tests/phpunit/BodytextParserTest.php b/extension/qfq/tests/phpunit/BodytextParserTest.php index b398d1f07217c6ef1e74a3fe09853f9dd0cd3b45..42ac80c5773aec1f45e8274141717d59fcf474b9 100644 --- a/extension/qfq/tests/phpunit/BodytextParserTest.php +++ b/extension/qfq/tests/phpunit/BodytextParserTest.php @@ -14,6 +14,7 @@ require_once(__DIR__ . '/../../qfq/exceptions/UserFormException.php'); class BodytextParserTest extends \PHPUnit_Framework_TestCase { + public function testProcessPlain() { $btp = new BodytextParser(); @@ -47,8 +48,14 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase { $result = $btp->process($given); $this->assertEquals($expected, $result); - // Nested expression: one - $given = "10{\nsql = SELECT 'Hello World'}"; + // Nested expression: one. + $given = "10{\nsql = SELECT 'Hello World'\n}\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one. No LF at the end + $given = "10{\nsql = SELECT 'Hello World'\n}"; $expected = "10.sql = SELECT 'Hello World'"; $result = $btp->process($given); $this->assertEquals($expected, $result); @@ -97,6 +104,247 @@ class BodytextParserTest extends \PHPUnit_Framework_TestCase { } + public function testNestingToken() { + $btp = new BodytextParser(); + + // Nested expression: one level curly + $given = "10 { \n sql = SELECT 'Hello World' \n , 'next line', \n \n 'end' \n} \n"; + $expected = "10.sql = SELECT 'Hello World' , 'next line', 'end'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level angle + $given = "#<\n10 < \n sql = SELECT 'Hello World'\n>\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level angle and single curly + $given = "#<\n10 < \n head = data { \n '1','2','3' \n }\n>\n"; + $expected = "10.head = data { '1','2','3' }"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level angle and single curly + $given = " # < \n 10 < \n sql = SELECT 'Hello World' \n>\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level angle and single curly + $given = " # > \n 10 < \n sql = SELECT 'Hello World' \n>\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level round bracket + $given = " # ( \n 10 ( \n sql = SELECT 'Hello World' \n)\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level round bracket + $given = " # ) \n 10 ( \n sql = SELECT 'Hello World' \n)\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level square bracket + $given = " # [ \n 10 [ \n sql = SELECT 'Hello World' \n]\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level square bracket + $given = " # ] \n 10 [ \n sql = SELECT 'Hello World' \n]\n"; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: one level angle - garbage + $given = " # < \n 10 { \n sql = SELECT 'Hello World' \n}\n"; + $expected = "10 {\nsql = SELECT 'Hello World' }"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // Nested expression: '</script>' is allowed here - with bad implemented parsing, it would detect a nesting token end, which is not the meant. + $given = " # < \n 10 < \n sql = SELECT 'Hello World' \n head = <script> \n>\n20.tail=</script>\n\n\n30.sql=SELECT 'something'"; + $expected = "10.sql = SELECT 'Hello World'\n10.head = <script>\n20.tail=</script>\n30.sql=SELECT 'something'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + $open = '<'; + $close = '>'; + // muliple nesting, unnested rows inbetween + $given = <<<EOF + # $open + + 5.head = <h1> + + 3.sql = SELECT ... + + 10 $open + 20 $open + sql = SELECT 10.20 + head = <script> + tail = </script> + 30 $open + head = <div> + $close + $close + 30 $open + sql = SELECT 10.30 + $close + sql = SELECT 10 + 50 $open + 60 $open + tail = } + } + (: + } + ] + sql = SELECT 10.50.60 + head = { + { + ) + { + ; + [ + $close + sql = SELECT 10.50 + 65 $open + sql = SELECT 10.50.65 + $close + $close + 40 $open + head = <table> + $close + $close + 20.sql = SELECT 20 +EOF; + $expected = "5.head = <h1>\n3.sql = SELECT ...\n10.20.sql = SELECT 10.20\n10.20.head = <script>\n10.20.tail = </script>\n10.20.30.head = <div>\n10.30.sql = SELECT 10.30\n10.sql = SELECT 10\n10.50.60.tail = } } (: } ]\n10.50.60.sql = SELECT 10.50.60\n10.50.60.head = { { ) { ; [\n10.50.sql = SELECT 10.50\n10.50.65.sql = SELECT 10.50.65\n10.40.head = <table>\n20.sql = SELECT 20"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + } + + public function testVariousNestingToken() { + $btp = new BodytextParser(); + + $tokenList = '{}[]<>()'; + for ($idx = 0; $idx < 4; $idx++) { + $open = $tokenList[$idx * 2]; + $close = $tokenList[$idx * 2 + 1]; + + // level open + $given = <<<EOF + # $open + 10 $open + sql = SELECT 'Hello World' + $close +EOF; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // level \n alone + $given = <<<EOF + # $open + 10 + $open + sql = SELECT 'Hello World' + $close +EOF; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // various linebreaks + $given = <<<EOF + # $open + + 10 $open + + sql = SELECT 'Hello World' + + $close + +EOF; + $expected = "10.sql = SELECT 'Hello World'"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // multi line + $given = <<<EOF + # $open + 10 $open + sql = SELECT 'Hello World' + FROM Person + + ORDER BY id + + LIMIT 4 + head = <div> + $close + 10.tail = </div> +EOF; + $expected = "10.sql = SELECT 'Hello World' FROM Person ORDER BY id LIMIT 4\n10.head = <div>\n10.tail = </div>"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // mulitple nesting + $given = <<<EOF + # $open + 5.head = <h1> + 10 $open + sql = SELECT 'Hello World' + 20 $open + sql = SELECT 'Hi' + head = <script> + tail = </script> + 30 $open + head = <div> + $close + + $close + + $close + +EOF; + $expected = "5.head = <h1>\n10.sql = SELECT 'Hello World'\n10.20.sql = SELECT 'Hi'\n10.20.head = <script>\n10.20.tail = </script>\n10.20.30.head = <div>"; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + // muliple nesting, unnested rows inbetween + $given = <<<EOF + # $open + 5.head = <h1> + 10 $open + sql = SELECT 'Hello World' + 20 $open + sql = SELECT 'Hi' + head = <script> + tail = </script> + 30 $open + head = <div> + $close + $close + 30 $open + sql = SELECT 'After' + $close + $close + 20.sql = SELECT ... + +EOF; + $expected = "5.head = <h1>\n10.sql = SELECT 'Hello World'\n10.20.sql = SELECT 'Hi'\n10.20.head = <script>\n10.20.tail = </script>\n10.20.30.head = <div>\n10.30.sql = SELECT 'After'\n20.sql = SELECT ..."; + $result = $btp->process($given); + $this->assertEquals($expected, $result); + + } + + } + + /** * @expectedException \qfq\UserFormException * diff --git a/extension/qfq/tests/phpunit/BuildFormPlainTest.php b/extension/qfq/tests/phpunit/BuildFormPlainTest.php index 1b6d4dc263d39c70ffe85724e2abde55b27e498a..bf0b3762682622f8ff75b314f00da93c8671d01e 100644 --- a/extension/qfq/tests/phpunit/BuildFormPlainTest.php +++ b/extension/qfq/tests/phpunit/BuildFormPlainTest.php @@ -1,8 +1,11 @@ <?php +//use qfq\Store; + require_once(__DIR__ . '/../../qfq/BuildFormPlain.php'); require_once(__DIR__ . '/../../qfq/QuickFormQuery.php'); require_once(__DIR__ . '/AbstractDatabaseTest.php'); +require_once(__DIR__ . '/../../qfq/store/Store.php'); /** * Created by PhpStorm. @@ -12,6 +15,9 @@ require_once(__DIR__ . '/AbstractDatabaseTest.php'); */ class BuildFormPlainTest extends AbstractDatabaseTest { + /** + * + */ public function testGetProcessFilter() { $build = new \qfq\BuildFormPlain(array(), array(), array()); @@ -33,6 +39,9 @@ class BuildFormPlainTest extends AbstractDatabaseTest { } + /** + * + */ public function testWrapItem() { $build = new \qfq\BuildFormPlain(array(), array(), array()); @@ -49,6 +58,9 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $this->assertEquals('', $result); } + /** + * + */ public function testBuildLabel() { $build = new \qfq\BuildFormPlain(array(), array(), array()); @@ -56,46 +68,18 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $this->assertEquals('<label for="myLabel:123" class="control-label" >Hello World</label>', $result); } + /** + * + */ public function testBuildInput() { $form = array(); $formElement = array(); $json = array(); - $this->setFormFormElement($form, $formElement); + $this->templateFormNFormElement($form, $formElement); $build = new \qfq\BuildFormPlain($form, array(), [$formElement]); -// $formElement = $this->db->sql("SELECT * FROM FormElement AS fe WHERE fe.id=114", ROW_EXACT_1); -// $this->assertEquals('', $formElement); - -// $formElement = [ -// 'id' => 123, -// 'formId' => 2, -// 'feIdContainer' => 0, -// 'enabled' => 'yes', -// 'name' => 'name', -// 'label' => 'Name', -// 'mode' => 'show', -// 'class' => 'native', -// 'type' => 'input', -// 'value' => '', -// 'sql1' => '', -// 'parameter' => '', -// 'debug' => 'no', -// 'deleted' => 'no', -// -// 'size' => '', -// 'maxLength' => '', -// 'tooltip' => '', -// 'placeholder' => '', -// 'checkType' => '', -// 'checkPattern' => '', -// -// 'tabindex' => 0 -// ]; - - - // Defaults $result = $build->buildInput($formElement, 'name:1', '', $json); $this->assertEquals('<input name="name:1" class="form-control" type="input" maxlength="255" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => ''], $json); @@ -142,6 +126,32 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $this->assertEquals('<input name="name:1" class="form-control" type="input" size="40" maxlength="255" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => '', 'disabled' => false], $json); + // no size, no maxlength and column not in primary table + $formElement2 = $formElement; + $formElement2['maxLength'] = ''; + $formElement2['size'] = ''; + $formElement2['name'] = 'specialname'; + $result = $build->buildInput($formElement2, 'specialname:1', '', $json); + $this->assertEquals('<input name="specialname:1" class="form-control" type="input" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); + + // no size, given maxlength and column not in primary table + $formElement2['maxLength'] = '10'; + $result = $build->buildInput($formElement2, 'specialname:1', '', $json); + $this->assertEquals('<input name="specialname:1" class="form-control" type="input" maxlength="10" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); + + // size given, no maxlength and column not in primary table + $formElement2['maxLength'] = ''; + $formElement2['size'] = '10'; + $result = $build->buildInput($formElement2, 'specialname:1', '', $json); + $this->assertEquals('<input name="specialname:1" class="form-control" type="input" size="10" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); + + // size given, maxlength given and column not in primary table + $formElement2['maxLength'] = '20'; + $formElement2['size'] = '10'; + $result = $build->buildInput($formElement2, 'specialname:1', '', $json); + $this->assertEquals('<input name="specialname:1" class="form-control" type="input" size="10" maxlength="20" value="" data-hidden="no" data-disabled="no" data-required="no" ><div class="help-block with-errors"></div>', $result); + + // Explicit: further $formElement['tooltip'] = 'Nice Tooltip'; $formElement['placeholder'] = 'Please type a name'; @@ -161,7 +171,10 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $this->assertEquals([FE_MODE_HIDDEN => '', 'disabled' => false, FE_MODE_REQUIRED => '', 'form-element' => 'name:1', 'value' => 'Hello World', 'disabled' => false], $json); } - private function setFormFormElement(array &$form, array &$formElement) { + /** + * + */ + private function templateFormNFormElement(array &$form, array &$formElement) { $form = [ 'id' => '1', 'name' => 'form', @@ -201,9 +214,8 @@ class BuildFormPlainTest extends AbstractDatabaseTest { 'parameter' => '', 'debug' => 'no', 'deleted' => 'no', - 'size' => '', - 'maxLength' => '', + 'maxLength' => '255', 'tooltip' => '', 'placeholder' => '', 'checkType' => '', @@ -223,7 +235,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $formElement = array(); $json = array(); - $this->setFormFormElement($form, $formElement); + $this->templateFormNFormElement($form, $formElement); $build = new \qfq\BuildFormPlain($form, array(), [$formElement]); @@ -232,11 +244,14 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $result = $build->buildInput($formElement, 'name:1', '', $json); } + /** + * + */ public function testGetKeyValueListFromSqlEnumSpec() { $form = array(); $formElement = array(); - $this->setFormFormElement($form, $formElement); + $this->templateFormNFormElement($form, $formElement); $formElement['name'] = 'deleted'; $build = new \qfq\BuildFormPlain($form, array(), [$formElement]); @@ -341,7 +356,7 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $form = array(); $formElement = array(); - $this->setFormFormElement($form, $formElement); + $this->templateFormNFormElement($form, $formElement); $build = new \qfq\BuildFormPlain($form, array(), [$formElement]); @@ -349,6 +364,126 @@ class BuildFormPlainTest extends AbstractDatabaseTest { $build->getKeyValueListFromSqlEnumSpec($formElement, $keys, $values); } + + /** + * + */ + public function testBuildSubrecord() { + $form = array(); + $formElement = array(); + $json = array(); + + $this->templateFormNFormElement($form, $formElement); + + // CheckType + $build = new \qfq\BuildFormPlain($form, array(), [$formElement]); + + // id: 1, firstName: John, name: Doe + $formElement['sql1'] = $this->db->sql('SELECT id, name, firstName FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>id</th><th>name</th><th>firstName</th></tr><tr class="record" ><td>1</td><td>Doe</td><td>John</td></tr><tr class="record" ><td>2</td><td>Smith</td><td>Jane</td></tr></table>', $result); + + // _id: 1, name: Doe, + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: Doe,title='' + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "title=" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th></th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: Doe, column: _Person + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "unused|width=2|title=_Person", firstName FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>firstName</th></tr><tr class="record" ><td>John</td></tr><tr class="record" ><td>Jane</td></tr></table>', $result); + + // _id: 1, name: Doe, title: PERSON + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "PERSON" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>PERSON</th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smith</td></tr></table>', $result); + + // _id: 1, "This is a much longer text than necessary": Default max:20 + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", "This is a much longer text than necessary" FROM Person ORDER BY id LIMIT 1'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>This is a much longe</th></tr><tr class="record" ><td>This is a much longe</td></tr></table>', $result); + + // _id: 1, name: Jo (width:2) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "2" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th></th></tr><tr class="record" ><td>Do</td></tr><tr class="record" ><td>Sm</td></tr></table>', $result); + + // _id: 1, name: Jo (width:2) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "2|PERSON" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>PE</th></tr><tr class="record" ><td>Do</td></tr><tr class="record" ><td>Sm</td></tr></table>', $result); + + // _id: 1, name: Doe ('width':3) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "Name|width=3" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>Nam</th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smi</td></tr></table>', $result); + + // _id: 1, name: Doe (width:3, title:PERSON) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name AS "3|title=PERSON" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>PER</th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smi</td></tr></table>', $result); + + // _id: 1, name: <b>Doe</b> + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", CONCAT("<b>", name, "</b>") AS "Name" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>Name</th></tr><tr class="record" ><td>Doe</td></tr><tr class="record" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: <b>Doe</b>, width=2 + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", CONCAT("<b>", name, "</b>") AS "Name|2" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>Na</th></tr><tr class="record" ><td>Do</td></tr><tr class="record" ><td>Sm</td></tr></table>', $result); + + // _id: 1, name: <b>Doe</b> , nostrip + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", CONCAT("<b>", name, "</b>") AS "Name|nostrip" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>Name</th></tr><tr class="record" ><td><b>Doe</b></td></tr><tr class="record" ><td><b>Smith</b></td></tr></table>', $result); + + // _id: 1, icon: bullet-green.gif + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", "bullet-green.gif" AS "Status|icon" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>Status</th></tr><tr class="record" ><td><image src=\'typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif\'></td></tr><tr class="record" ><td><image src=\'typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif\'></td></tr></table>', $result); + + // _id: 1, mailto: john@doe.com + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", "john@doe.com" AS "EMail|mailto" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>EMail</th></tr><tr class="record" ><td><a href="mailto:john@doe.com" >john@doe.com</a></td></tr><tr class="record" ><td><a href="mailto:john@doe.com" >john@doe.com</a></td></tr></table>', $result); + + // _id: 1, url: www.uzh.ch + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", "www.uzh.ch" AS "URL|url" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>URL</th></tr><tr class="record" ><td><a href="www.uzh.ch" >www.uzh.ch</a></td></tr><tr class="record" ><td><a href="www.uzh.ch" >www.uzh.ch</a></td></tr></table>', $result); + + // _id: 1, name: Doe, _rowclass (text) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name, IF(id=1,"text-warning", "text-danger") AS _rowClass FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th></tr><tr class="record text-warning" ><td>Doe</td></tr><tr class="record text-danger" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: Doe, _rowClass (text & background) + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name, IF(id=1,"text-warning active", "text-danger success") AS _rowClass FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th></tr><tr class="record text-warning active" ><td>Doe</td></tr><tr class="record text-danger success" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: Doe, _rowTitle + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name, firstName AS _rowTitle FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th></tr><tr class="record" title="John" ><td>Doe</td></tr><tr class="record" title="Jane" ><td>Smith</td></tr></table>', $result); + + // _id: 1, name: Doe, title, width, nostrip + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name, "<b>This again is a very long text</b>" AS "title=Important|width=10|nostrip" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th><th>Important</th></tr><tr class="record" ><td>Doe</td><td><b>This ag</td></tr><tr class="record" ><td>Smith</td><td><b>This ag</td></tr></table>', $result); + + // _id: 1, name: Doe, link + $formElement['sql1'] = $this->db->sql('SELECT id AS "_id", name, CONCAT("s:1|p:form&form=person&r=" , id , "|t:", name) AS "link" FROM Person ORDER BY id LIMIT 2'); + $result = $build->buildSubrecord($formElement, 'name:1', '', $json); + $this->assertEquals('<table class="table table-hover"><tr><th>name</th><th></th></tr><tr class="record" ><td>Doe</td><td><a href="index.php?id=form&s=badcaffee1234" class="internal" >Doe</a></td></tr><tr class="record" ><td>Smith</td><td><a href="index.php?id=form&s=badcaffee1234" class="internal" >Smith</a></td></tr></table>', $result); + } + /** * @throws Exception */ @@ -358,20 +493,20 @@ class BuildFormPlainTest extends AbstractDatabaseTest { parent::setUp(); $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); -// $this->executeSQLFile(__DIR__ . '/fixtures/TestFormEditor.sql', true); - $GLOBALS["TSFE"] = new FakeTSFE(); + // Defaults + $this->store->setVar('name', 'varchar(255)', STORE_TABLE_COLUMN_TYPES, true); + $this->store->setVar('deleted', "enum('yes','no')", STORE_TABLE_COLUMN_TYPES, true); + + $GLOBALS["TSFE"] = new FakeTSFEBuildPlain(); -// $this->form = new \qfq\QuickFormQuery(['bodytext' => "form=form\nr=3", 'uid' => 1234], true); $form = new \qfq\QuickFormQuery(['bodytext' => "form=form\nr=3", 'uid' => 1234], true); - // this is necessary to initialize SIP -// $content = $this->form->process(); $form->process(); } } -class FakeTSFE { +class FakeTSFEBuildPlain { public $id = 1; public $type = 1; public $sys_language_uid = 1; diff --git a/extension/qfq/tests/phpunit/DatabaseTest.php b/extension/qfq/tests/phpunit/DatabaseTest.php index ef5b36bcfbbd2d3deedada29601514f5d13df72c..761d86b29e8606360d939ba385aa7cb1b0c2a644 100644 --- a/extension/qfq/tests/phpunit/DatabaseTest.php +++ b/extension/qfq/tests/phpunit/DatabaseTest.php @@ -38,14 +38,16 @@ class DatabaseTest extends AbstractDatabaseTest { [ 'id' => '1', 'name' => 'Doe', - 'firstname' => 'John', + 'firstName' => 'John', + 'adrId' => '0', 'gender' => 'male', 'groups' => 'c' ], [ 'id' => '2', 'name' => 'Smith', - 'firstname' => 'Jane', + 'firstName' => 'Jane', + 'adrId' => '0', 'gender' => 'female', 'groups' => 'a,c' ], @@ -228,14 +230,131 @@ class DatabaseTest extends AbstractDatabaseTest { /** * @throws \qfq\DbException */ - public function testGetLastInsertId() { + public function testSelectReturn() { + $dummy = array(); + $stat = array(); + + $sql = "SELECT name FROM Person ORDER BY id"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + $this->assertEquals([0 => ['name' => 'Doe'], 1 => ['name' => 'Smith']], $rc); + + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(2, $stat[DB_NUM_ROWS]); + } + + /** + * @throws \qfq\DbException + */ + public function testShowReturn() { + $dummy = array(); + $stat = array(); + + $sql = "SHOW TABLES"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(5, $stat[DB_NUM_ROWS]); + } + + /** + * @throws \qfq\DbException + */ + public function testExplainReturn() { + $dummy = array(); + $stat = array(); + + $sql = "EXPLAIN Person"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(6, $stat[DB_NUM_ROWS]); + } + + /** + * @throws \qfq\DbException + */ + public function testInsertReturn() { $dummy = array(); $stat = array(); $sql = "INSERT INTO Person (id, name, firstname, gender, groups) VALUES (NULL, 'Doe', 'Jonni', 'male','')"; - $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS $this->assertEquals(3, $stat[DB_INSERT_ID]); + $this->assertEquals(1, $stat[DB_AFFECTED_ROWS]); + + $this->assertEquals(3, $rc); + } + + /** + * @throws \qfq\DbException + */ + public function testUpdateReturn() { + $dummy = array(); + $stat = array(); + + $sql = "UPDATE Person SET groups = 'a'"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + + $this->assertEquals(2, $rc); + + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(2, $stat[DB_AFFECTED_ROWS]); + } + + /** + * @throws \qfq\DbException + */ + public function testDeleteReturn() { + $dummy = array(); + $stat = array(); + + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); + + $sql = "DELETE FROM Person WHERE id=2"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + + $this->assertEquals(1, $rc); + + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(1, $stat[DB_AFFECTED_ROWS]); + + } + + + /** + * @throws \qfq\DbException + */ + public function testReplaceReturn() { + $dummy = array(); + $stat = array(); + + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); + + $sql = "REPLACE INTO Person (id, name, firstname, gender, groups) VALUES (1, 'Doe', 'John', 'male','')"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(1, $stat[DB_INSERT_ID]); + $this->assertEquals(2, $stat[DB_AFFECTED_ROWS]); + + $this->assertEquals(1, $rc); + + + $sql = "REPLACE INTO Person (id, name, firstname, gender, groups) VALUES (100, 'Lincoln', 'Abraham', 'male','')"; + + $rc = $this->db->sql($sql, ROW_REGULAR, $dummy, 'fake', $dummy, $stat); + // DB_NUM_ROWS | DB_INSERT_ID | DB_AFFECTED_ROWS + $this->assertEquals(100, $stat[DB_INSERT_ID]); + $this->assertEquals(1, $stat[DB_AFFECTED_ROWS]); + + $this->assertEquals(100, $rc); } /** @@ -245,7 +364,8 @@ class DatabaseTest extends AbstractDatabaseTest { $expected = [ ['Field' => 'id', 'Type' => 'bigint(20)', 'Null' => 'NO', 'Key' => 'PRI', 'Default' => '', 'Extra' => 'auto_increment'], ['Field' => 'name', 'Type' => 'varchar(128)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => ''], - ['Field' => 'firstname', 'Type' => 'varchar(128)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => ''], + ['Field' => 'firstName', 'Type' => 'varchar(128)', 'Null' => 'YES', 'Key' => '', 'Default' => '', 'Extra' => ''], + ['Field' => 'adrId', 'Type' => 'int(11)', 'Null' => 'NO', 'Key' => '', 'Default' => '0', 'Extra' => ''], ['Field' => 'gender', 'Type' => "enum('','male','female')", 'Null' => 'NO', 'Key' => '', 'Default' => 'male', 'Extra' => ''], ['Field' => 'groups', 'Type' => "set('','a','b','c')", 'Null' => 'NO', 'Key' => '', 'Default' => '', 'Extra' => ''], ]; diff --git a/extension/qfq/tests/phpunit/DeleteTest.php b/extension/qfq/tests/phpunit/DeleteTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bf70a1ee47ebe784c8143fc6c2bd12ec787838b1 --- /dev/null +++ b/extension/qfq/tests/phpunit/DeleteTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 1/15/16 + * Time: 8:24 AM + */ + +namespace qfq; + +require_once(__DIR__ . '/AbstractDatabaseTest.php'); +require_once(__DIR__ . '/../../qfq/Delete.php'); + +class DeleteTest extends \AbstractDatabaseTest { + + /** + * @expectedException \qfq\CodeException + */ + public function testProcessException() { + + $delete = new Delete(true); + + // empty 'form' not allowed + $delete->process(array(), 123); + } + + /** + * @expectedException \qfq\CodeException + */ + public function testProcessException1() { + + $delete = new Delete(true); + + // 'form' with empty tablename not allowed + $form[F_TABLE_NAME] = ''; + $delete->process($form, 123); + } + + /** + * @expectedException \qfq\CodeException + */ + public function testProcessException2() { + + $delete = new Delete(true); + + // empty record id not allowed + $form[F_TABLE_NAME] = 'Person'; + $delete->process($form, ''); + } + + /** + * @expectedException \qfq\CodeException + */ + public function testProcessException3() { + + $delete = new Delete(true); + $form[F_TABLE_NAME] = 'Person'; + + // record id = 0 not allowed + $delete->process($form, 0); + } + + /** + * @expectedException \qfq\CodeException + */ + public function testProcessException4() { + + $delete = new Delete(true); + + // unknown table not allowed + $form[F_TABLE_NAME] = 'UnknownTable'; + $delete->process($form, 0); + } + + /** + */ + public function testProcessRecordNotFound() { + + $delete = new Delete(true); + + // unknown table not allowed + $form[F_TABLE_NAME] = 'Person'; +// $rc = $delete->process($form, 100); +// +// $this->assertEquals(false, $rc); +// +// $expect = ['content' => "Record 100 not found in table 'Person'.", 'errorCode' => 1066]; +// $this->assertEquals($expect); + } + + /** + */ + public function testProcess() { + + $delete = new Delete(true); + + // unknown table not allowed + $form[F_TABLE_NAME] = 'Person'; +// $rc = $delete->process($form, 1); +// +// $this->assertEquals(true, $rc); +// +// +// $count = $this->db->sql('SELECT COUNT(id) FROM Person', ROW_IMPLODE_ALL); +// $this->assertEquals('1', $count); + + } + + + protected function setUp() { + + $this->store = Store::getInstance('form=TestFormName', true); + parent::setUp(); + + $this->store->setVar('form', 'TestFormName', STORE_TYPO3); + $this->store->setVar(SYSTEM_SITE_PATH, '/tmp', STORE_SYSTEM, true); + + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); + } + +} diff --git a/extension/qfq/tests/phpunit/EvaluateTest.php b/extension/qfq/tests/phpunit/EvaluateTest.php index 6230ed5b4600005a6c1d4986989d3842c7b46277..359835be86dd36e822ad710ee84ef777f8fea597 100644 --- a/extension/qfq/tests/phpunit/EvaluateTest.php +++ b/extension/qfq/tests/phpunit/EvaluateTest.php @@ -42,20 +42,21 @@ class EvaluateTest extends \AbstractDatabaseTest { $eval->parse('These are the variables > m1:{{m1:C:all}}, m2:{{m2:C:all}}, m3:{{m3:C:all}} - end outer line')); } - public function testDb() { + public function testParse() { $eval = new \qfq\Evaluate($this->store, $this->db); // database: lower case - $this->assertEquals('1DoeJohnmalec2SmithJanefemalea,c', $eval->parse('{{select * from Person where id < 3 order by id}}')); + $this->assertEquals('1DoeJohn0malec2SmithJane0femalea,c', $eval->parse('{{select * from Person where id < 3 order by id}}')); // database: upper case - $this->assertEquals('1DoeJohnmalec2SmithJanefemalea,c', $eval->parse('{{SELECT * FROM Person WHERE id < 3 ORDER BY id}}')); + $this->assertEquals('1DoeJohn0malec2SmithJane0femalea,c', $eval->parse('{{SELECT * FROM Person WHERE id < 3 ORDER BY id}}')); $this->store->setVar('sql', 'SELECT * FROM Person WHERE id < 3 ORDER BY id', 'C'); $this->assertEquals('SELECT * FROM Person WHERE id < 3 ORDER BY id', $eval->parse('{{sql:C:all}}')); - $this->assertEquals('1DoeJohnmalec2SmithJanefemalea,c', $eval->parse('{{{{sql:C:all}}}}')); + $this->assertEquals('1DoeJohn0malec2SmithJane0femalea,c', $eval->parse('{{{{sql:C:all}}}}')); + // Get 2 row Array $expected = [ [ 'id' => '1', @@ -68,7 +69,10 @@ class EvaluateTest extends \AbstractDatabaseTest { ]; $this->assertEquals($expected, $eval->parse('{{!SELECT id, name FROM Person WHERE id < 3 ORDER BY id}}')); - // INSERT: Use eval() to write a record + // Get empty array + $this->assertEquals(array(), $eval->parse('{{!SELECT id, name FROM Person WHERE id=0}}')); + + // INSERT: Use 'Eval' to write a record $eval->parse('{{INSERT INTO Person (name, firstname) VALUES (\'Sinatra\', \'Frank\')}}'); $this->assertEquals('Frank Sinatra', $eval->parse('{{SELECT firstname, " ", name FROM Person WHERE name="Sinatra" AND firstname="Frank"}}')); @@ -82,7 +86,7 @@ class EvaluateTest extends \AbstractDatabaseTest { $this->assertEquals('1', $eval->parse('{{SELECT count(id) FROM Person WHERE name="Zappa" AND firstname="Frank"}}')); // SHOW tables - $expected = "idbigint(20)NOPRIauto_incrementnamevarchar(128)YESfirstnamevarchar(128)YESgenderenum('','male','female')NOmalegroupsset('','a','b','c')NO"; + $expected = "idbigint(20)NOPRIauto_incrementnamevarchar(128)YESfirstNamevarchar(128)YESadrIdint(11)NO0genderenum('','male','female')NOmalegroupsset('','a','b','c')NO"; $this->assertEquals($expected, $eval->parse('{{SHOW COLUMNS FROM Person}}')); @@ -96,7 +100,6 @@ class EvaluateTest extends \AbstractDatabaseTest { $this->store->setVar('a', '{{b:C:all}}', 'C'); $this->store->setVar('b', 'Holiday', 'C'); $this->assertEquals('Holiday', $eval->parse('{{SELECT name FROM Person WHERE name=\'{{a:C:all}}\'}}')); - } public function testParseArray() { @@ -112,8 +115,6 @@ class EvaluateTest extends \AbstractDatabaseTest { ]; $this->assertEquals($expected, $eval->parseArray($data)); - - } public function testParseArrayOfArray() { @@ -142,6 +143,173 @@ class EvaluateTest extends \AbstractDatabaseTest { $this->assertEquals($expected, $eval->parseArray($data)); } + public function testSubstituteSql() { + $eval = new \qfq\Evaluate($this->store, $this->db); + + $expectArr = [0 => ['name' => 'Holiday', 'firstname' => 'Billie'], 1 => ['name' => 'Holiday', 'firstname' => 'Billie']]; + + $eval->parse('{{INSERT INTO Person (name, firstname) VALUES (\'Holiday\', \'Billie\')}}'); + $eval->parse('{{INSERT INTO Person (name, firstname) VALUES (\'Holiday\', \'Billie\')}}'); + $this->store->setVar('a', '{{b:C:all}}', 'C'); + $this->store->setVar('b', 'Holiday', 'C'); + + // Fire query: empty + $this->assertEquals('', $eval->substitute('SELECT name FROM Person WHERE id=0', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query + $this->assertEquals('Holiday', $eval->substitute('SELECT name FROM Person WHERE name LIKE "Holiday" LIMIT 1', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, surrounding whitespace + $this->assertEquals('Holiday', $eval->substitute(' SELECT name FROM Person WHERE name LIKE "Holiday" LIMIT 1 ', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, lowercase + $this->assertEquals('Holiday', $eval->substitute(' SELECT name FROM Person WHERE name LIKE "Holiday" LIMIT 1', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, join two records + $this->assertEquals('HolidayBillieHolidayBillie', $eval->substitute('SELECT name, firstname FROM Person WHERE name LIKE "Holiday" LIMIT 2', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, two records as assoc + $this->assertEquals($expectArr, $eval->substitute('!SELECT name, firstname FROM Person WHERE name LIKE "Holiday" LIMIT 2', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, no result + $this->assertEquals(array(), $eval->substitute('!SELECT name, firstname FROM Person WHERE id=0', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + + // Fire query, two records as assoc, surrounding whitespace + $this->assertEquals($expectArr, $eval->substitute(' !select name, firstname FROM Person WHERE name LIKE "Holiday" LIMIT 2 ', $foundInStore)); + $this->assertEquals(TOKEN_FOUND_IN_STORE_QUERY, $foundInStore); + } + + public function testSubstituteVar() { + $eval = new \qfq\Evaluate($this->store, $this->db); + + // Retrieve in PRIO `FSRD` + $this->store->setVar('a', '1234', STORE_TABLE_DEFAULT, true); + $this->assertEquals('1234', $eval->substitute('a', $foundInStore)); + $this->assertEquals(STORE_TABLE_DEFAULT, $foundInStore); + + $this->store->setVar('a', '12345', STORE_RECORD, true); + $this->assertEquals('12345', $eval->substitute('a', $foundInStore)); + $this->assertEquals(STORE_RECORD, $foundInStore); + + $this->store->setVar('a', '123456', STORE_SIP, true); + $this->assertEquals('123456', $eval->substitute('a', $foundInStore)); + $this->assertEquals(STORE_SIP, $foundInStore); + + $this->store->setVar('a', '1234567', STORE_FORM, true); + $this->assertEquals('1234567', $eval->substitute('a', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals(false, $eval->substitute('notFound', $foundInStore)); + $this->assertEquals('', $foundInStore); + + // Specific Store + $this->assertEquals('1234', $eval->substitute('a:D', $foundInStore)); + $this->assertEquals(STORE_TABLE_DEFAULT, $foundInStore); + + $this->assertEquals('12345', $eval->substitute('a:R', $foundInStore)); + $this->assertEquals(STORE_RECORD, $foundInStore); + + $this->assertEquals('123456', $eval->substitute('a:S', $foundInStore)); + $this->assertEquals(STORE_SIP, $foundInStore); + + $this->assertEquals('1234567', $eval->substitute('a:F', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals(false, $eval->substitute('a:V', $foundInStore)); + $this->assertEquals('', $foundInStore); + + // Sanatize Class: digits + $this->assertEquals('1234567', $eval->substitute('a:F:digit', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('1234567', $eval->substitute('a:F:alnumx', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('1234567', $eval->substitute('a:F:allbut', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('1234567', $eval->substitute('a:F:all', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // Sanatize Class: text + $this->store->setVar('a', 'Hello world @-_.,;: /()', STORE_FORM, true); + + $this->assertEquals('', $eval->substitute('a:F:digit', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('Hello world @-_.,;: /()', $eval->substitute('a:F:alnumx', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('Hello world @-_.,;: /()', $eval->substitute('a:F:allbut', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + $this->assertEquals('Hello world @-_.,;: /()', $eval->substitute('a:F:all', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + } + + public function testSubstituteVarEscape() { + $eval = new \qfq\Evaluate($this->store, $this->db); + + // No escape + $this->store->setVar('a', 'hello', STORE_FORM, true); + $this->assertEquals('hello', $eval->substitute('a:F:all', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // None, Single Tick + $this->store->setVar('a', 'hello', STORE_FORM, true); + $this->assertEquals('hello', $eval->substitute('a:F:all:s', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // None, Double Tick + $this->store->setVar('a', 'hello', STORE_FORM, true); + $this->assertEquals('hello', $eval->substitute('a:F:all:d', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + + // ', Single Tick + $this->store->setVar('a', 'hel\'lo', STORE_FORM, true); + $this->assertEquals('hel\\\'lo', $eval->substitute('a:F:all:s', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // ' Double Tick + $this->store->setVar('a', 'hel\'lo', STORE_FORM, true); + $this->assertEquals('hel\'lo', $eval->substitute('a:F:all:d', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + + // ", Single Tick + $this->store->setVar('a', 'hel"lo', STORE_FORM, true); + $this->assertEquals('hel"lo', $eval->substitute('a:F:all:s', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // ", Double Tick + $this->store->setVar('a', 'hel"lo', STORE_FORM, true); + $this->assertEquals('hel\"lo', $eval->substitute('a:F:all:d', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + + // Multi ', Single Tick + $this->store->setVar('a', "h\"e' 'l\"lo ' ", STORE_FORM, true); + $this->assertEquals("h\"e\' \'l\"lo \' ", $eval->substitute('a:F:all:s', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + // Multi , Double Tick + $this->store->setVar('a', 'h"e\' \'l"lo \' ', STORE_FORM, true); + $this->assertEquals('h\"e\' \'l\"lo \' ', $eval->substitute('a:F:all:d', $foundInStore)); + $this->assertEquals(STORE_FORM, $foundInStore); + + + } + + protected function setUp() { $this->store = Store::getInstance('form=TestFormName', true); diff --git a/extension/qfq/tests/phpunit/FormActionTest.php b/extension/qfq/tests/phpunit/FormActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f39950f249223a1301b649836deff5844194b949 --- /dev/null +++ b/extension/qfq/tests/phpunit/FormActionTest.php @@ -0,0 +1,305 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 1/15/16 + * Time: 8:24 AM + */ + +namespace qfq; + +require_once(__DIR__ . '/AbstractDatabaseTest.php'); +require_once(__DIR__ . '/../../qfq/form/FormAction.php'); + +class FormActionTest extends \AbstractDatabaseTest { + + public function testBeforeLoad() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + // Nothing to do: should not throw an exception + $formAction->elements(0, array(), ''); + $formAction->elements(0, array(), FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + + // Fire sqlValidate with one record, expect 1 record + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 1}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '1'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + + // Fire sqlValidate with one record, expect 0-2 records + $feSpecAction[FE_EXPECT_RECORDS] = '0,1,2'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + + // Fire sqlValidate with one record, expect 0-2 records + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1,2'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + + // Check with more classes + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_LOAD; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1,2'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD); + } + + /** + * Expect 0 recrod, but get 1 + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException1() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 1}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '0'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + } + + /** + * Expect 1 recrod, but get 0 + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException0() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 0}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '1'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + } + + /** + * Expect '0,1', but get 2 records + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException2() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_BEFORE_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD); + } + + /** + * Expect '0,1', but get 2 records + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException3() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_UPDATE; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + } + + /** + * Necessary FE is empty > don't process check + **/ + public function testBeforeLoadException4() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_UPDATE; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1'; + + // one FE in list + $this->store->setVar('street', 'Street', STORE_FORM, true); + $this->store->setVar('city', '', STORE_FORM, true); + $feSpecAction[FE_REQUIRED_LIST] = 'city'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + + // three FE in list. one is set, one not, one dont exist + $this->store->setVar('city', '', STORE_FORM, true); + $feSpecAction[FE_REQUIRED_LIST] = 'street,city,downtown'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + } + + /** + * Expect '0,1', but get 2 records + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException5() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $this->store->setVar('city', 'New York', STORE_FORM, true); + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_UPDATE; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '0,1'; + $formAction->elements(0, [$feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + } + + /** + * Do check for 2 action records + **/ + public function testBeforeLoad2() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '2'; + $formAction->elements(0, [$feSpecAction, $feSpecAction], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + } + + /** + * Do check for 2 action records, fail on second. + * @expectedException \qfq\UserFormException + **/ + public function testBeforeLoadException6() { + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_LOAD; + $feSpecAction[FE_MESSAGE_FAIL] = 'error'; + $feSpecAction[FE_SQL_VALIDATE] = '{{!SELECT id FROM Person LIMIT 2}}'; + $feSpecAction[FE_EXPECT_RECORDS] = '2'; + $feSpecAction2 = $feSpecAction; + $feSpecAction2[FE_EXPECT_RECORDS] = '0'; + $formAction->elements(0, [$feSpecAction, $feSpecAction2], FE_TYPE_BEFORE_LOAD . ',' . FE_TYPE_AFTER_LOAD . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); + } + + /** + * Process INSERT + **/ + public function testInsert() { + + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_SAVE; + $feSpecAction[FE_SQL_INSERT] = '{{ INSERT INTO Address (city, personId) VALUES ("Downtown", {{r}}) }} '; + $feSpecAction[FE_SQL_UPDATE] = '{{ UPDATE Address SET city="invalid" WHERE id={{r}} }} '; + $feSpecAction[FE_SQL_DELETE] = '{{ DELETE FROM Address WHERE personId={{r}} AND id=0 }} '; + + $this->store->setVar('r', '2', STORE_SIP, true); + + + // slaveId: 0 + $feSpecAction[FE_SLAVE_ID] = '0'; + $result = $this->db->sql('TRUNCATE Address'); + $formAction->elements(2, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + $result = $this->db->sql('SELECT id, city, personId FROM Address', ROW_IMPLODE_ALL); + $this->assertEquals('1Downtown2', $result); + + + // slaveId: SELECT ... >> '' + $feSpecAction[FE_SLAVE_ID] = '{{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}'; + $result = $this->db->sql('TRUNCATE Address'); + $formAction->elements(2, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + $result = $this->db->sql('SELECT id, city, personId FROM Address', ROW_IMPLODE_ALL); + $this->assertEquals('1Downtown2', $result); + + + // slaveId: slaveId through column in master record & update Master record with slaveId + $this->store->setVar('adrId', '0', STORE_RECORD); + $feSpecAction[FE_NAME] = 'adrId'; + $feSpecAction[FE_SLAVE_ID] = ''; + + $result = $this->db->sql('TRUNCATE Address'); + $formAction->elements(2, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + // get the new slave record + $result = $this->db->sql('SELECT id, city, personId FROM Address', ROW_IMPLODE_ALL); + $this->assertEquals('1Downtown2', $result); + // get the updated id in the master record + $result = $this->db->sql('SELECT id, name, adrId FROM Person WHERE id=2', ROW_IMPLODE_ALL); + $this->assertEquals('2Smith1', $result); + } + + /** + * Process UPDATE + **/ + public function testUpdate() { + + $formSpec[F_TABLE_NAME] = 'Person'; + $formAction = new FormAction($formSpec, $this->db, true); + + $masterId = 2; + + $feSpecAction[FE_NAME] = ''; + $feSpecAction[FE_TYPE] = FE_TYPE_AFTER_SAVE; + $feSpecAction[FE_SQL_INSERT] = "{{ INSERT INTO Address (city, personId) VALUES ('invalid', {{r}}) }} "; + $feSpecAction[FE_SQL_UPDATE] = "{{ UPDATE Address SET city='Uptown' WHERE id={{slaveId:V}} }} "; + $feSpecAction[FE_SQL_DELETE] = "{{ DELETE FROM Address WHERE personId={{r}} AND id=0 }} "; + + $result = $this->db->sql("TRUNCATE Address"); + $result = $this->db->sql("INSERT INTO Address (city, personId) VALUES ('Downtown1', 1)"); + $result = $this->db->sql("INSERT INTO Address (city, personId) VALUES ('Downtown2', 1)"); + $result = $this->db->sql("INSERT INTO Address (city, personId) VALUES ('Downtown3', $masterId)"); + + $this->store->setVar('r', "$masterId", STORE_SIP, true); + + // slaveId: 1 - hard coded + $feSpecAction[FE_SLAVE_ID] = '3'; + $formAction->elements($masterId, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + $result = $this->db->sql("SELECT id, city, personId FROM Address WHERE personId=$masterId", ROW_IMPLODE_ALL); + $this->assertEquals('3Uptown' . $masterId, $result); + + + // slaveId: SELECT ... >> '' + $feSpecAction[FE_SLAVE_ID] = "{{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}"; + $formAction->elements($masterId, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + $result = $this->db->sql("SELECT id, city, personId FROM Address WHERE personId=$masterId", ROW_IMPLODE_ALL); + $this->assertEquals('3Uptown' . $masterId, $result); + + + // slaveId: column in master record + $this->db->sql("UPDATE Person SET adrId=3 WHERE id=$masterId", ROW_IMPLODE_ALL); +// $this->store->setVar('adrId', '3', STORE_RECORD); + $feSpecAction[FE_NAME] = 'adrId'; + $feSpecAction[FE_SLAVE_ID] = ''; + + $formAction->elements($masterId, [$feSpecAction], FE_TYPE_AFTER_SAVE); + + $result = $this->db->sql("SELECT id, city, personId FROM Address WHERE personId=$masterId", ROW_IMPLODE_ALL); + $this->assertEquals('3Uptown' . $masterId, $result); + $result = $this->db->sql("SELECT id, name, adrId FROM Person WHERE id=$masterId", ROW_IMPLODE_ALL); + $this->assertEquals('2Smith3', $result); + } + + + + protected function setUp() { + + $this->store = Store::getInstance('form=TestFormName', true); + parent::setUp(); + + $this->store->setVar('form', 'TestFormName', STORE_TYPO3); + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); + + } + +} diff --git a/extension/qfq/tests/phpunit/LinkTest.php b/extension/qfq/tests/phpunit/LinkTest.php index 0443314ca8e8495c4109687c423ef92ee60edb5e..4943875eca303e5148ba09600e60ff8abdfff9fb 100644 --- a/extension/qfq/tests/phpunit/LinkTest.php +++ b/extension/qfq/tests/phpunit/LinkTest.php @@ -11,6 +11,7 @@ namespace qfq; require_once(__DIR__ . '/../../qfq/report/Link.php'); require_once(__DIR__ . '/../../qfq/store/Store.php'); require_once(__DIR__ . '/../../qfq/store/Sip.php'); +require_once(__DIR__ . '/../../qfq/store/Session.php'); class LinkTest extends \PHPUnit_Framework_TestCase { @@ -25,26 +26,49 @@ class LinkTest extends \PHPUnit_Framework_TestCase { */ private $store = null; + /** + * @expectedException \qfq\UserReportException + */ + public function testUnknownTokenException1() { + $link = new Link($this->sip, true); + + $link->renderLink('b:hello world'); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testUnknownTokenException2() { + $link = new Link($this->sip, true); + + $link->renderLink('abc:hello world'); + } + /** * @throws SyntaxReportException */ public function testLinkUrlBasic() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); - $result = $link->renderLink(''); - $this->assertEquals('', $result); - - $result = $link->renderLink('u'); - $this->assertEquals('', $result); + Store::setVar(TYPO3_PAGE_ID, 'firstPage', STORE_TYPO3); - $result = $link->renderLink('u:'); + $result = $link->renderLink(''); $this->assertEquals('', $result); $result = $link->renderLink('u:http://example.com'); $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + $result = $link->renderLink('u:http://example.com?id=100&t=2¶m=hello'); + $this->assertEquals('<a href="http://example.com?id=100&t=2¶m=hello" class="external" >http://example.com?id=100&t=2¶m=hello</a>', $result); + + $result = $link->renderLink('u:example.com'); + $this->assertEquals('<a href="example.com" class="external" >example.com</a>', $result); + $result = $link->renderLink('u:http://example.com|t:Hello world'); $this->assertEquals('<a href="http://example.com" class="external" >Hello world</a>', $result); + + $result = $link->renderLink('u:http://example.com?id=100&t=2¶m=hello|t:Hello world'); + $this->assertEquals('<a href="http://example.com?id=100&t=2¶m=hello" class="external" >Hello world</a>', $result); } /** @@ -52,17 +76,36 @@ class LinkTest extends \PHPUnit_Framework_TestCase { * */ public function testLinkUrlBasicExceptionDouble() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); + + $link->renderLink('u:http://example.com|u:http://new.org'); + } + + /** + * @expectedException \qfq\UserReportException + * + */ + public function testLinkUrlBasicExceptionEmpty1() { + $link = new Link($this->sip, true); - $result = $link->renderLink('u:http://example.com|u:http://new.org'); + $link->renderLink('u'); } + /** + * @expectedException \qfq\UserReportException + * + */ + public function testLinkUrlBasicExceptionEmpty2() { + $link = new Link($this->sip, true); + + $link->renderLink('u:'); + } /** * @throws SyntaxReportException */ public function testLinkPageBasic() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); Store::setVar(TYPO3_PAGE_ID, 'firstPage', STORE_TYPO3); $result = $link->renderLink('p'); @@ -77,6 +120,9 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $result = $link->renderLink('p:id=secondPage'); $this->assertEquals('<a href="?id=secondPage" class="internal" >?id=secondPage</a>', $result); + $result = $link->renderLink('p:id=secondPage&id=100&t=2¶m=hello'); + $this->assertEquals('<a href="?id=secondPage&id=100&t=2¶m=hello" class="internal" >?id=secondPage&id=100&t=2¶m=hello</a>', $result); + $result = $link->renderLink('p:secondPage|t:Hello world'); $this->assertEquals('<a href="?id=secondPage" class="internal" >Hello world</a>', $result); } @@ -85,13 +131,13 @@ class LinkTest extends \PHPUnit_Framework_TestCase { * @throws SyntaxReportException */ public function testLinkMailBasic() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); $result = $link->renderLink('m:john@doe.com'); - $this->assertEquals('<a href="mailto:john@doe.com" >mailto:john@doe.com</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); $result = $link->renderLink('m:john@doe.com|t:John Doe'); - $this->assertEquals('<a href="mailto:john@doe.com" >John Doe</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >John Doe</a>', $result); } /** @@ -99,9 +145,9 @@ class LinkTest extends \PHPUnit_Framework_TestCase { * */ public function testLinkMailBasicExceptionMissing1() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); - $result = $link->renderLink('m'); + $link->renderLink('m'); } /** @@ -109,17 +155,38 @@ class LinkTest extends \PHPUnit_Framework_TestCase { * */ public function testLinkMailBasicExceptionMissing2() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); - $result = $link->renderLink('m:'); + $link->renderLink('m:'); } + /** + * @throws SyntaxReportException + */ + public function testMailEncryption() { + $link = new Link($this->sip, true); + + //TODO: aktivieren sobald encrypted Mails implemented. +// $result = $link->renderLink('m:john@doe.com|e'); +// $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); +// +// $result = $link->renderLink('m:john@doe.com|e:'); +// $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); + + $result = $link->renderLink('m:john@doe.com|e:0'); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); + +// $result = $link->renderLink('m:john@doe.com|e:1'); +// $this->assertEquals('<a href="mailto:john@doe.com" >mailto:john@doe.com</a>', $result); + } + + /** + * @throws UserReportException + */ public function testRenderModeUrl() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); - // r: default (0) - $result = $link->renderLink('u'); - $this->assertEquals('', $result); + Store::setVar(TYPO3_PAGE_ID, 'firstPage', STORE_TYPO3); $result = $link->renderLink('u:http://example.com'); $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); @@ -130,10 +197,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $result = $link->renderLink('u:http://example.com|t:Example'); $this->assertEquals('<a href="http://example.com" class="external" >Example</a>', $result); - // r: 0 - $result = $link->renderLink('u|r:0'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:0'); $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); @@ -144,9 +207,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('<a href="http://example.com" class="external" >Example</a>', $result); // r: 1 - $result = $link->renderLink('u|r:1'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:1'); $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); @@ -158,9 +218,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('<a href="http://example.com" class="external" >Example</a>', $result); // r: 2 - $result = $link->renderLink('u|r:2'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:2'); $this->assertEquals('', $result); @@ -171,9 +228,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('<a href="http://example.com" class="external" >Example</a>', $result); // r: 3 - $result = $link->renderLink('u|r:3'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:3'); // $this->assertEquals('<span >http://example.com</span>', $result); $this->assertEquals('http://example.com', $result); @@ -187,9 +241,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Example', $result); // r: 4 - $result = $link->renderLink('u|r:4'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:4'); // $this->assertEquals('<span >http://example.com</span>', $result); $this->assertEquals('http://example.com', $result); @@ -203,9 +254,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('http://example.com', $result); // r: 5 - $result = $link->renderLink('u|r:5'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:5'); $this->assertEquals('', $result); @@ -216,8 +264,13 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', $result); } + /** + * @throws CodeException + * @throws UserFormException + * @throws UserReportException + */ public function testRenderModePage() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); Store::setVar(TYPO3_PAGE_ID, 'firstPage', STORE_TYPO3); // r: default (0) @@ -321,53 +374,44 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', $result); } + /** + * @throws UserReportException + */ public function testRenderModeMail() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); // r: default (0) - $result = $link->renderLink('u'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com'); - $this->assertEquals('<a href="mailto:john@doe.com" >mailto:john@doe.com</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); $result = $link->renderLink('t:Example'); $this->assertEquals('', $result); $result = $link->renderLink('m:john@doe.com|t:Example'); - $this->assertEquals('<a href="mailto:john@doe.com" >Example</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >Example</a>', $result); // r: 0 - $result = $link->renderLink('u|r:0'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:0'); - $this->assertEquals('<a href="mailto:john@doe.com" >mailto:john@doe.com</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); $result = $link->renderLink('t:Example|r:0'); $this->assertEquals('', $result); $result = $link->renderLink('m:john@doe.com|t:Example|r:0'); - $this->assertEquals('<a href="mailto:john@doe.com" >Example</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >Example</a>', $result); // r: 1 - $result = $link->renderLink('u|r:1'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:1'); - $this->assertEquals('<a href="mailto:john@doe.com" >mailto:john@doe.com</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >mailto:john@doe.com</a>', $result); $result = $link->renderLink('t:Example|r:1'); // $this->assertEquals('<span >Example</span>', $result); $this->assertEquals('Example', $result); $result = $link->renderLink('m:john@doe.com|t:Example|r:1'); - $this->assertEquals('<a href="mailto:john@doe.com" >Example</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >Example</a>', $result); // r: 2 - $result = $link->renderLink('u|r:2'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:2'); $this->assertEquals('', $result); @@ -375,12 +419,9 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', $result); $result = $link->renderLink('m:john@doe.com|t:Example|r:2'); - $this->assertEquals('<a href="mailto:john@doe.com" >Example</a>', $result); + $this->assertEquals('<a href="mailto:john@doe.com" class="external" >Example</a>', $result); // r: 3 - $result = $link->renderLink('u|r:3'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:3'); // $this->assertEquals('<span >mailto:john@doe.com</span>', $result); $this->assertEquals('mailto:john@doe.com', $result); @@ -394,9 +435,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('Example', $result); // r: 4 - $result = $link->renderLink('u|r:4'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:4'); // $this->assertEquals('<span >mailto:john@doe.com</span>', $result); $this->assertEquals('mailto:john@doe.com', $result); @@ -410,9 +448,6 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('mailto:john@doe.com', $result); // r: 5 - $result = $link->renderLink('u|r:5'); - $this->assertEquals('', $result); - $result = $link->renderLink('m:john@doe.com|r:5'); $this->assertEquals('', $result); @@ -423,102 +458,81 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', $result); } + /** + * @throws UserReportException + */ public function testRenderModeUrlPicture() { - $link = new Link(null, $this->sip, true); + $link = new Link($this->sip, true); // r: default (0) - $result = $link->renderLink('u|P:picture.gif'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" ></a>', $result); $result = $link->renderLink('t:Example|P:picture.gif'); $this->assertEquals('', $result); $result = $link->renderLink('u:http://example.com|t:Example|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); // r: 0 - $result = $link->renderLink('u|r:0|P:picture.gif'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:0|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" ></a>', $result); $result = $link->renderLink('t:Example|r:0|P:picture.gif'); $this->assertEquals('', $result); $result = $link->renderLink('u:http://example.com|t:Example|r:0|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); // r: 1 - $result = $link->renderLink('u|r:1|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > ', $result); - $result = $link->renderLink('u:http://example.com|r:1|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" ></a>', $result); $result = $link->renderLink('t:Example|r:1|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example', $result); +// $this->assertEquals('<span ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); + $this->assertEquals('<img alt="picture.gif" src="picture.gif" title="picture.gif" > Example', $result); $result = $link->renderLink('u:http://example.com|t:Example|r:1|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); // r: 2 - $result = $link->renderLink('u|r:2|P:picture.gif'); - $this->assertEquals('', $result); - //TODO: no link if text is empty - image is linked here: this is not what the user expects. $result = $link->renderLink('u:http://example.com|r:2|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" ></a>', $result); $result = $link->renderLink('t:Example|r:2|P:picture.gif'); $this->assertEquals('', $result); $result = $link->renderLink('u:http://example.com|t:Example|r:2|P:picture.gif'); - $this->assertEquals('<a href="http://example.com" ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); + $this->assertEquals('<a href="http://example.com" ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</a>', $result); // r: 3: - $result = $link->renderLink('u|r:3|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > ', $result); - $result = $link->renderLink('u:http://example.com|r:3|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > ', $result); +// $this->assertEquals('<span ><img alt="picture.gif" src="picture.gif" title="picture.gif" > </span>', $result); + $this->assertEquals('<img alt="picture.gif" src="picture.gif" title="picture.gif" >', $result); $result = $link->renderLink('t:Example|r:3|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example', $result); +// $this->assertEquals('<span ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); + $this->assertEquals('<img alt="picture.gif" src="picture.gif" title="picture.gif" > Example', $result); $result = $link->renderLink('u:http://example.com|t:Example|r:3|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example', $result); +// $this->assertEquals('<span ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); + $this->assertEquals('<img alt="picture.gif" src="picture.gif" title="picture.gif" > Example', $result); // r: 4 - $result = $link->renderLink('u|r:4|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > </span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > ', $result); - $result = $link->renderLink('u:http://example.com|r:4|P:picture.gif'); // $this->assertEquals('<span >http://example.com</span>', $result); $this->assertEquals('http://example.com', $result); $result = $link->renderLink('t:Example|r:4|P:picture.gif'); -// $this->assertEquals('<span ><img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); - $this->assertEquals('<img alt="Grafic: picture.gif" src="picture.gif" title="picture.gif" > Example', $result); +// $this->assertEquals('<span ><img alt="picture.gif" src="picture.gif" title="picture.gif" > Example</span>', $result); + $this->assertEquals('<img alt="picture.gif" src="picture.gif" title="picture.gif" > Example', $result); $result = $link->renderLink('u:http://example.com|t:Example|r:4|P:picture.gif'); // $this->assertEquals('<span >http://example.com</span>', $result); $this->assertEquals('http://example.com', $result); // r: 5 - $result = $link->renderLink('u|r:5|P:picture.gif'); - $this->assertEquals('', $result); - $result = $link->renderLink('u:http://example.com|r:5|P:picture.gif'); $this->assertEquals('', $result); @@ -529,9 +543,515 @@ class LinkTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', $result); } - public function testLinkClass() { + /** + * @throws UserReportException + */ + public function testIcons() { + $link = new Link($this->sip, true); + + $result = $link->renderLink('u:http://example.com|E'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|N'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|D'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Delete" ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|H'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|I'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|S'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span></a>', $result); + + $result = $link->renderLink('u:http://example.com|E|o:specific'); + $this->assertEquals('<a href="http://example.com" class="btn btn-default" title="specific" ><span class="glyphicon glyphicon-pencil" ></span></a>', $result); } + /** + * @throws UserReportException + */ + public function testBullet() { + $link = new Link($this->sip, true); + + $result = $link->renderLink('u:http://example.com|B'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:green'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:blue'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet blue" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-blue.gif" title="blue" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:gray'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet gray" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-gray.gif" title="gray" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:pink'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet pink" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-pink.gif" title="pink" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:red'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet red" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-red.gif" title="red" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B:yellow'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet yellow" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-yellow.gif" title="yellow" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|B|o:specific'); + $this->assertEquals('<a href="http://example.com" title="specific" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="specific" ></a>', $result); + } + + /** + * @throws UserReportException + */ + public function testChecked() { + $link = new Link($this->sip, true); + + $result = $link->renderLink('u:http://example.com|C'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:green'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:blue'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked blue" src="typo3conf/ext/qfq/Resources/Public/icons/checked-blue.gif" title="blue" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:gray'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked gray" src="typo3conf/ext/qfq/Resources/Public/icons/checked-gray.gif" title="gray" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:pink'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked pink" src="typo3conf/ext/qfq/Resources/Public/icons/checked-pink.gif" title="pink" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:red'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked red" src="typo3conf/ext/qfq/Resources/Public/icons/checked-red.gif" title="red" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C:yellow'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked yellow" src="typo3conf/ext/qfq/Resources/Public/icons/checked-yellow.gif" title="yellow" ></a>', $result); + + $result = $link->renderLink('u:http://example.com|C|o:specific'); + $this->assertEquals('<a href="http://example.com" title="specific" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="specific" ></a>', $result); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testPictureException1() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|P:picture.gif|B'); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testPictureException2() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|C|B'); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testPictureException3() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|B:green|B:red'); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testPictureException4() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|C:green|C:red'); + } + + /** + * @throws SyntaxReportException + */ + public function testLinkUrlParam() { + $link = new Link($this->sip, true); + + $result = $link->renderLink('u:http://example.com|U:'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + $result = $link->renderLink('u:http://example.com|U:'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + $result = $link->renderLink('u:http://example.com|U:a=1234'); + $this->assertEquals('<a href="http://example.com?a=1234" class="external" >http://example.com?a=1234</a>', $result); + + $result = $link->renderLink('u:http://example.com|U:a=1234&b=abcd'); + $this->assertEquals('<a href="http://example.com?a=1234&b=abcd" class="external" >http://example.com?a=1234&b=abcd</a>', $result); + + $result = $link->renderLink('u:http://example.com|U:a'); + $this->assertEquals('<a href="http://example.com?a" class="external" >http://example.com?a</a>', $result); + + $result = $link->renderLink('u:http://example.com|U:a='); + $this->assertEquals('<a href="http://example.com?a=" class="external" >http://example.com?a=</a>', $result); + + $result = $link->renderLink('u:http://example.com?A=hello|U:a=world'); + $this->assertEquals('<a href="http://example.com?A=hello&a=world" class="external" >http://example.com?A=hello&a=world</a>', $result); + + $result = $link->renderLink('u:http://example.com?A=hello&B=nice|U:a=world'); + $this->assertEquals('<a href="http://example.com?A=hello&B=nice&a=world" class="external" >http://example.com?A=hello&B=nice&a=world</a>', $result); + + $result = $link->renderLink('p:form|U:a=1234'); + $this->assertEquals('<a href="?id=form&a=1234" class="internal" >?id=form&a=1234</a>', $result); + + $result = $link->renderLink('p:form|U:a=1234&b=abcd'); + $this->assertEquals('<a href="?id=form&a=1234&b=abcd" class="internal" >?id=form&a=1234&b=abcd</a>', $result); + + $result = $link->renderLink('p:form|U:a'); + $this->assertEquals('<a href="?id=form&a" class="internal" >?id=form&a</a>', $result); + + $result = $link->renderLink('p:form|U:a='); + $this->assertEquals('<a href="?id=form&a=" class="internal" >?id=form&a=</a>', $result); + } + + /** + * @throws SyntaxReportException + */ + public function testTooltip() { + $link = new Link($this->sip, true); + + // standard case + $result = $link->renderLink('u:http://example.com|o:hello world'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world" >http://example.com</a>', $result); + + // standard case, swapped parameter + $result = $link->renderLink('o:hello world|u:http://example.com'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world" >http://example.com</a>', $result); + + // no text: this is ok + $result = $link->renderLink('u:http://example.com|o'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + // no text: this is ok + $result = $link->renderLink('u:http://example.com|o:'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + // some text, with double ticks inside + $result = $link->renderLink('u:http://example.com|o:hello world "some more text" end'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world \\"some more text\\" end" >http://example.com</a>', $result); + + // some text, with already escaped double ticks inside + $result = $link->renderLink('u:http://example.com|o:hello world \\"some more text\\" end'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world \\"some more text\\" end" >http://example.com</a>', $result); + + // some text with single ticks + $result = $link->renderLink('u:http://example.com|o:hello world \'some more text\' end'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world \'some more text\' end" >http://example.com</a>', $result); + + // some text with already escaped single ticks + $result = $link->renderLink('u:http://example.com|o:hello world \\\'some more text\\\' end'); + $this->assertEquals('<a href="http://example.com" class="external" title="hello world \\\'some more text\\\' end" >http://example.com</a>', $result); + } + + /** + * @throws SyntaxReportException + */ + public function testAltText() { + $link = new Link($this->sip, true); + + // standard case + $result = $link->renderLink('u:http://example.com|a:hello world|P:image.gif'); + $this->assertEquals('<a href="http://example.com" ><img alt="hello world" src="image.gif" title="image.gif" ></a>', $result); + + // standard: swapped parameter + //TODO: fixme +// $result = $link->renderLink('P:image.gif|a:hello world|u:http://example.com'); +// $this->assertEquals('<a href="http://example.com" ><img alt="hello world" src="image.gif" title="image.gif" ></a>', $result); + + // alt text empty + $result = $link->renderLink('u:http://example.com|a:|P:image.gif'); + $this->assertEquals('<a href="http://example.com" ><img alt="image.gif" src="image.gif" title="image.gif" ></a>', $result); + + } + + /** + * @throws SyntaxReportException + */ + public function testClass() { + $link = new Link($this->sip, true); + + // no class + $result = $link->renderLink('u:http://example.com|c:n'); + $this->assertEquals('<a href="http://example.com" >http://example.com</a>', $result); + + // internal class + $result = $link->renderLink('u:http://example.com|c:i'); + $this->assertEquals('<a href="http://example.com" class="internal" >http://example.com</a>', $result); + + // internal class + $result = $link->renderLink('u:http://example.com|c:e'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + // specific class + $result = $link->renderLink('u:http://example.com|c:myClass'); + $this->assertEquals('<a href="http://example.com" class="myClass" >http://example.com</a>', $result); + + $result = $link->renderLink('u:http://example.com'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + $result = $link->renderLink('u:http://example.com|c'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + $result = $link->renderLink('u:http://example.com|c:'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + $result = $link->renderLink('p:form'); + $this->assertEquals('<a href="?id=form" class="internal" >?id=form</a>', $result); + } + + /** + * @throws SyntaxReportException + */ + public function testTarget() { + $link = new Link($this->sip, true); + + // no target + $result = $link->renderLink('u:http://example.com|g'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + // no target + $result = $link->renderLink('u:http://example.com|g:'); + $this->assertEquals('<a href="http://example.com" class="external" >http://example.com</a>', $result); + + // target _blank + $result = $link->renderLink('u:http://example.com|g:_blank'); + $this->assertEquals('<a href="http://example.com" class="external" target="_blank" >http://example.com</a>', $result); + + // target someName + $result = $link->renderLink('u:http://example.com|g:someName'); + $this->assertEquals('<a href="http://example.com" class="external" target="someName" >http://example.com</a>', $result); + + // target someName, swapped parameter + $result = $link->renderLink('g:someName|u:http://example.com'); + $this->assertEquals('<a href="http://example.com" class="external" target="someName" >http://example.com</a>', $result); + } + + /** + * @throws SyntaxReportException + */ + public function testRight() { + $link = new Link($this->sip, true); + + // Bullet, LEFT (Standard) + $result = $link->renderLink('u:http://example.com|t:Hello World|B'); + $this->assertEquals('<a href="http://example.com" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" > Hello World</a>', $result); + + // Bullet, RIGHT + $result = $link->renderLink('u:http://example.com|t:Hello World|B|R'); + $this->assertEquals('<a href="http://example.com" >Hello World <img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" ></a>', $result); + + // Checked, LEFT (Standard) + $result = $link->renderLink('u:http://example.com|t:Hello World|C'); + $this->assertEquals('<a href="http://example.com" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" > Hello World</a>', $result); + + // Checked, RIGHT + $result = $link->renderLink('u:http://example.com|t:Hello World|C|R'); + $this->assertEquals('<a href="http://example.com" >Hello World <img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" ></a>', $result); + + // Picture, LEFT (Standard) + $result = $link->renderLink('u:http://example.com|t:Hello World|P:image.gif'); + $this->assertEquals('<a href="http://example.com" ><img alt="image.gif" src="image.gif" title="image.gif" > Hello World</a>', $result); + + // Picture, RIGHT + $result = $link->renderLink('u:http://example.com|t:Hello World|P:image.gif|R'); + $this->assertEquals('<a href="http://example.com" >Hello World <img alt="image.gif" src="image.gif" title="image.gif" ></a>', $result); + + // swapped param + $result = $link->renderLink('R|P:image.gif|t:Hello World|u:http://example.com'); + $this->assertEquals('<a href="http://example.com" class="external" >Hello World <img alt="image.gif" src="image.gif" title="image.gif" ></a>', $result); + } + + + /** + * @throws SyntaxReportException + */ + public function testSip() { + $link = new Link($this->sip, true); + + // Sip: URL, s + $result = $link->renderLink('u:?form&r=12&xId=2345&L=1&type=99&gId=55|s'); + $this->assertEquals('<a href="index.php?id=form&L=1&type=99&s=badcaffee1234" class="external" >index.php?id=form&L=1&type=99&s=badcaffee1234</a>', $result); + + // Sip: URL, s:0 + $result = $link->renderLink('u:?form&r=12&xId=2345&L=1&type=99&gId=55|s:0'); + $this->assertEquals('<a href="index.php?form&r=12&xId=2345&L=1&type=99&gId=55" class="external" >index.php?form&r=12&xId=2345&L=1&type=99&gId=55</a>', $result); + + // Sip: URL, s:1 + $result = $link->renderLink('u:?form&r=12&xId=2345&L=1&type=99&gId=55|s:1'); + $this->assertEquals('<a href="index.php?id=form&L=1&type=99&s=badcaffee1234" class="external" >index.php?id=form&L=1&type=99&s=badcaffee1234</a>', $result); + + // Sip: Page, s + $result = $link->renderLink('p:form&r=12&xId=2345&L=1&type=99&gId=55|s'); + $this->assertEquals('<a href="index.php?id=form&L=1&type=99&s=badcaffee1234" class="internal" >index.php?id=form&L=1&type=99&s=badcaffee1234</a>', $result); + + // Sip: Page, s:0 + $result = $link->renderLink('p:form&r=12&xId=2345&L=1&type=99&gId=55|s:0'); + $this->assertEquals('<a href="?id=form&r=12&xId=2345&L=1&type=99&gId=55" class="internal" >?id=form&r=12&xId=2345&L=1&type=99&gId=55</a>', $result); + + // Sip: Page, s:1 + $result = $link->renderLink('p:form&r=12&xId=2345&L=1&type=99&gId=55|s:1'); + $this->assertEquals('<a href="index.php?id=form&L=1&type=99&s=badcaffee1234" class="internal" >index.php?id=form&L=1&type=99&s=badcaffee1234</a>', $result); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testSipException1() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|s:s'); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testSipException2() { + $link = new Link($this->sip, true); + + // r: default (0) + $link->renderLink('u:http://www.example.com|s:2'); + } + + /** + * @throws SyntaxReportException + */ + public function testQuestion() { + $link = new Link($this->sip, true); + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Please confirm', type: 'info', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // Question: all default + $result = $link->renderLink('p:person|c:n|q'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: all default + $result = $link->renderLink('p:person|c:n|q:'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text + $js = str_replace('Please confirm', 'do you really want', $js); + $result = $link->renderLink('p:person|c:n|q:do you really want'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text, level: warning + $js = str_replace('info', 'warning', $js); + $result = $link->renderLink('p:person|c:n|q:do you really want:warning'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text, level: warning, positive button: I do + $js = str_replace('Ok', 'I do', $js); + $result = $link->renderLink('p:person|c:n|q:do you really want:warning:I do'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text, level: warning, positive button: I do, negative button: Shut up + $js = str_replace('Cancel', 'Shut up', $js); + $result = $link->renderLink('p:person|c:n|q:do you really want:warning:I do:Shut up'); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text (with escaped colon)), level: warning, positive button: I do, negative button: Shut up + $js = str_replace('do you really want', 'My Question:some nice value', $js); + $result = $link->renderLink("p:person|c:n|q:My Question\\:some nice value:warning:I do:Shut up"); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Question: individual text (with escaped colon)), level: warning, positive button: I do (with escaped colon), negative button: Shut up + $js = str_replace('I do', 'I do: hurry up', $js); + $result = $link->renderLink("p:person|c:n|q:My Question\\:some nice value:warning:I do\\: hurry up:Shut up"); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Timeout: + $js = str_replace('timeout: 0', 'timeout: 10000', $js); + $result = $link->renderLink("p:person|c:n|q:My Question\\:some nice value:warning:I do\\: hurry up:Shut up:10"); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Modal: 1 + $result = $link->renderLink("p:person|c:n|q:My Question\\:some nice value:warning:I do\\: hurry up:Shut up:10:1"); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + // Modal: 0 + $js = str_replace('modal: true', 'modal: false', $js); + $result = $link->renderLink("p:person|c:n|q:My Question\\:some nice value:warning:I do\\: hurry up:Shut up:10:0"); + $this->assertEquals('<a href="?id=person" ' . $js . ' >?id=person</a>', $result); + + } + + + public function testDelete() { + $link = new Link($this->sip, true); + + // Report Delete action, default: 'Report', no Icon + $result = $link->renderLink('U:form=Person&r=123|x'); + $this->assertEquals('<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234" >typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234</a>', $result); + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&form=Person&r=123', $result); + + // Report Delete action, explicit 'Report', no Icon + $result = $link->renderLink('U:form=PersonA&r=1234|x:r'); + $this->assertEquals('<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234" >typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234</a>', $result); + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&form=PersonA&r=1234', $result); + + // Report Delete action, explicit 'Report', Text + $result = $link->renderLink('U:form=PersonAa&r=2234|x:r|t:Delete Record'); + $this->assertEquals('<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234" >Delete Record</a>', $result); + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&form=PersonAa&r=2234', $result); + + // Report Delete action, with Icon + $result = $link->renderLink('U:form=PersonB&r=1235|x|D'); + $this->assertEquals('<a href="typo3conf/ext/qfq/qfq/api/delete.php?s=badcaffee1234" class="btn btn-default" title="Delete" ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&form=PersonB&r=1235', $result); + + // Report Delete action: explicit php file, default: 'Report', no Icon + $result = $link->renderLink('u:mydelete.php|U:form=PersonC&r=1236|x'); + $this->assertEquals('<a href="mydelete.php?s=badcaffee1234" class="external" >mydelete.php?s=badcaffee1234</a>', $result); + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&form=PersonC&r=1236', $result); + } + + /** + * @expectedException \qfq\UserReportException + */ + public function testDeleteException1() { + $link = new Link($this->sip, true); + + // Missing recordId + $link->renderLink('U:form=Person&r=|x'); + } + + + /** + * + */ protected function setUp() { parent::setUp(); diff --git a/extension/qfq/tests/phpunit/QuickFormQueryTest.php b/extension/qfq/tests/phpunit/QuickFormQueryTest.php index 1914a2b717903388994dc430e9f214d20b7ff2a5..bac8ebae8d73a90470acd5e28d0b635a1fcb2b98 100644 --- a/extension/qfq/tests/phpunit/QuickFormQueryTest.php +++ b/extension/qfq/tests/phpunit/QuickFormQueryTest.php @@ -11,6 +11,7 @@ require_once(__DIR__ . '/../../qfq/Constants.php'); require_once(__DIR__ . '/../../qfq/QuickFormQuery.php'); require_once(__DIR__ . '/../../qfq/store/Store.php'); +require_once(__DIR__ . '/../../qfq/store/Sip.php'); class QuickFormQueryTest extends \PHPUnit_Framework_TestCase { @@ -20,6 +21,77 @@ class QuickFormQueryTest extends \PHPUnit_Framework_TestCase { public function testProcess() { $form = new qfq\QuickFormQuery(['bodytext' => "\n; some comment\n" . TYPO3_FORM . "=testformnameDoNotChange\n"]); // $this->assertEquals("", $form->process()); + } +// +//* FORM_LOAD: +//* Specified in T3 body text with form=<formname> Returned Store:Typo3 +//* Specified in T3 body text with form={{form}} ':FSRD' Returned Store:SIP +//* Specified in T3 body text with form={{form:C:ALNUMX}} Returned Store:Client +//* Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE id={{conferenceId:S0}} }} +// * Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE id={{conferenceId:C0:DIGIT}} }} +// * Specified in SIP +//* +// * FORM_SAVE: +// * Specified in SIP +//* +// * +// * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE +//* @param string $foundInStore +//* @return array|bool|mixed|null|string Formname (Form.name) or FALSE, if no formname found. +// * @throws CodeException +//* @throws UserFormException +//*/ + + public function testGetFormName() { + + // <empty> bodytext + $t3data[T3DATA_BODYTEXT] = "\n \n \n"; + $t3data[T3DATA_UID] = "123"; + $qfq = new qfq\QuickFormQuery($t3data, true); + $result = $qfq->getFormName(FORM_LOAD, $foundInStore); + $this->assertEquals('', $result); + + // form= + $t3data[T3DATA_BODYTEXT] = "\n \n" . TYPO3_FORM . "=\n"; + $t3data[T3DATA_UID] = "123"; + $qfq = new qfq\QuickFormQuery($t3data, true); + $result = $qfq->getFormName(FORM_LOAD, $foundInStore); + $this->assertEquals('', $result); + + // form=<formname> + $t3data[T3DATA_BODYTEXT] = "\n \n" . TYPO3_FORM . "=myForm\n"; + $t3data[T3DATA_UID] = "123"; + $qfq = new qfq\QuickFormQuery($t3data, true); + $result = $qfq->getFormName(FORM_LOAD, $foundInStore); + $this->assertEquals('myForm', $result); + + // form={{SELECT 'nextForm'}} + $t3data[T3DATA_BODYTEXT] = "\n \n" . TYPO3_FORM . "={{SELECT 'nextForm'}}\n"; + $t3data[T3DATA_UID] = "123"; + $qfq = new qfq\QuickFormQuery($t3data, true); + $result = $qfq->getFormName(FORM_LOAD, $foundInStore); + $this->assertEquals('nextForm', $result); + + // form={{form:C:alnumx}} + $t3data[T3DATA_BODYTEXT] = "\n \n" . TYPO3_FORM . "={{form:C:alnumx}}\n"; + $t3data[T3DATA_UID] = "123"; + $_SERVER['form'] = 'formNameViaGet'; + $qfq = new qfq\QuickFormQuery($t3data, true); + $result = $qfq->getFormName(FORM_LOAD, $foundInStore); + $this->assertEquals('formNameViaGet', $result); + + // form={{form}} +// $sip = new qfq\Sip('fakesessionname', true); +// $sip->sipUniqId('badcaffee1234'); +// $t3data[T3DATA_BODYTEXT] = "\n \n" . TYPO3_FORM . "={{form}}\n"; +// $t3data[T3DATA_UID] = "123"; +// $_SERVER[CLIENT_SIP]='badcaffee1234'; +// $dummy = $sip->queryStringToSip("http://example.com/index.php?id=input&r=1&form=person", RETURN_URL); +// $qfq = new qfq\QuickFormQuery($t3data, true); +// $result = $qfq->getFormName(FORM_LOAD, $foundInStore); +// $this->assertEquals('person', $result); + + } } diff --git a/extension/qfq/tests/phpunit/ReportTest.php b/extension/qfq/tests/phpunit/ReportTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4010ed85b3abc68011cfa2939babe37acc19d2ca --- /dev/null +++ b/extension/qfq/tests/phpunit/ReportTest.php @@ -0,0 +1,995 @@ +<?php + +//namespace qfq; + +//use qfq; + +require_once(__DIR__ . '/AbstractDatabaseTest.php'); +require_once(__DIR__ . '/../../qfq/report/Report.php'); +require_once(__DIR__ . '/../../qfq/store/Store.php'); +require_once(__DIR__ . '/../../qfq/Evaluate.php'); +require_once(__DIR__ . '/../../qfq/store/Session.php'); + +/** + * Created by PhpStorm. + * User: crose + * Date: 2/2/16 + * Time: 10:07 PM + */ +class ReportTest extends AbstractDatabaseTest { + + /** + * @var qfq\Evaluate + */ + private $eval = null; + + /** + * @var qfq\Report + */ + private $report = null; + + /** + * + */ + public function testReportSingleQuery() { + + //empty + $result = $this->report->process(""); + $this->assertEquals('', $result); + + // comment + $result = $this->report->process('# 10.sql = SELECT "Hello World"'); + $this->assertEquals('', $result); + + // empty (missing '=') + $result = $this->report->process('10.sql SELECT "Hello World"'); + $this->assertEquals('', $result); + + // sql + $result = $this->report->process('10.sql = SELECT "Hello World"'); + $this->assertEquals('Hello World', $result); + + // sql, 2 rows + $result = $this->report->process('10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2'); + $this->assertEquals('DoeJohnSmithJane', $result); + + // sql, 2 rows, head + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>"); + $this->assertEquals('<table>DoeJohnSmithJane', $result); + + // sql, 2 rows, head, tail + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>"); + $this->assertEquals('<table>DoeJohnSmithJane</table>', $result); + + // sql, 2 rows, head, tail, rbeg + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>"); + $this->assertEquals('<table><tr>DoeJohn<tr>SmithJane</table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = </tr>"); + $this->assertEquals('<table><tr>DoeJohn</tr><tr>SmithJane</tr></table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend, fbeg + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = </tr>\n10.fbeg = <td>"); + $this->assertEquals('<table><tr><td>Doe<td>John</tr><tr><td>Smith<td>Jane</tr></table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend, fbeg, fend + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = </tr>\n10.fbeg = <td>\n10.fend = </td>"); + $this->assertEquals('<table><tr><td>Doe</td><td>John</td></tr><tr><td>Smith</td><td>Jane</td></tr></table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend, fbeg, fend, renr + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = ++\n10.fbeg = <td>\n10.fend = </td>\n10.renr = </tr>"); + $this->assertEquals('<table><tr><td>Doe</td><td>John</td>++</tr><tr><td>Smith</td><td>Jane</td>++</tr></table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend, fbeg, fend, renr, rsep + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = ++\n10.fbeg = <td>\n10.fend = </td>\n10.renr = </tr>\n10.rsep = @"); + $this->assertEquals('<table><tr><td>Doe</td><td>John</td>++</tr>@<tr><td>Smith</td><td>Jane</td>++</tr></table>', $result); + + // sql, 2 rows, head, tail, rbeg, rend, fbeg, fend, renr, rsep, fsep + $result = $this->report->process("10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2\n10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = ++\n10.fbeg = <td>\n10.fend = </td>\n10.renr = </tr>\n10.rsep = @\n10.fsep = $$"); + $this->assertEquals('<table><tr><td>Doe</td>$$<td>John</td>++</tr>@<tr><td>Smith</td>$$<td>Jane</td>++</tr></table>', $result); + + // no 'sql' + $result = $this->report->process("10.tail = </table>\n10.rbeg = <tr>\n10.rend = ++\n10.fbeg = <td>\n10.fend = </td>\n10.renr = </tr>\n10.rsep = @\n10.fsep = $$"); + $this->assertEquals('', $result); + + // head, tail, rbeg, rend, fbeg, fend, renr, rsep, fsep, sql 2 rows + $result = $this->report->process("10.head = <table>\n10.tail = </table>\n10.rbeg = <tr>\n10.rend = ++\n10.fbeg = <td>\n10.fend = </td>\n10.renr = </tr>\n10.rsep = @\n10.fsep = $$\n10.sql = SELECT name, firstname FROM Person ORDER BY id LIMIT 2"); + $this->assertEquals('<table><tr><td>Doe</td>$$<td>John</td>++</tr>@<tr><td>Smith</td>$$<td>Jane</td>++</tr></table>', $result); + } + + /** + * @expectedException \qfq\SyntaxReportException + */ + public function testUnknownTokenException() { + + // empty (missing '=') + $result = $this->report->process('10.sql SELECT "Hello = World"'); + } + + /** + * + */ + public function testReportMultipleQuery() { + + // nested 2 + $result = $this->report->process("10.sql = SELECT id FROM Person ORDER BY id LIMIT 2\n10.10.sql = SELECT name FROM Person ORDER BY id LIMIT 2\n"); + $this->assertEquals('1DoeSmith2DoeSmith', $result); + + // nested 3 + $result = $this->report->process("10.sql = SELECT id FROM Person ORDER BY id LIMIT 2\n10.10.sql = SELECT name FROM Person ORDER BY id LIMIT 2\n\n10.10.10.sql = SELECT firstname FROM Person ORDER BY id LIMIT 2\n"); + $this->assertEquals('1DoeJohnJaneSmithJohnJane2DoeJohnJaneSmithJohnJane', $result); + + // nested 2, seq + $result = $this->report->process("10.sql = SELECT id FROM Person ORDER BY id LIMIT 2\n10.10.sql = SELECT name FROM Person ORDER BY id LIMIT 2\n\n10.20.sql = SELECT firstname FROM Person ORDER BY id LIMIT 2\n"); + $this->assertEquals('1DoeSmithJohnJane2DoeSmithJohnJane', $result); + } + + /** + * + */ + public function testReportLink() { + + // link + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" class="external" >http://www.example.com</a>', $result); + + // link, checked + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|C' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" ></a>', $result); + + // linck, checked, text + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|C|t:Hello World' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" > Hello World</a>', $result); + + // link, checked, text, tooltip + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|C|t:Hello World|o:more information' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" title="more information" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="more information" > Hello World</a>', $result); + } + + /** + * + */ + public function testReportLinkRenderMode() { + + // r:0, url + text + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|t:Hello World|r:0' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" class="external" >Hello World</a>', $result); + + // r:0, url + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|r:0' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" class="external" >http://www.example.com</a>', $result); + + // r:0, text + $result = $this->report->process("10.sql = SELECT 'r:0|t:Hello World' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('', $result); + + // r:1, url + text + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|t:Hello World|r:1' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" class="external" >Hello World</a>', $result); + + // r:1, url + $result = $this->report->process("10.sql = SELECT 'u:http://www.example.com|r:1' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="http://www.example.com" class="external" >http://www.example.com</a>', $result); + + // r:1, text + $result = $this->report->process("10.sql = SELECT 'r:1|t:Hello World' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('Hello World', $result); + + } + + /** + * + */ + public function testReportMailto() { + + // r:0, mailto + $result = $this->report->process("10.sql = SELECT 'm:john.doe@example.com' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="mailto:john.doe@example.com" class="external" >mailto:john.doe@example.com</a>', $result); + + $result = $this->report->process("10.sql = SELECT 'm:john.doe@example.com|e:0' AS _link FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="mailto:john.doe@example.com" class="external" >mailto:john.doe@example.com</a>', $result); + + //TODO: enable encryption check +// $result = $this->report->process("10.sql = SELECT 'm:john.doe@example.com|e:1' AS _link FROM Person ORDER BY id LIMIT 1"); +// $this->assertEquals('<a href="mailto:john.doe@example.com" >mailto:john.doe@example.com</a>', $result); + + } + + /** + * + */ + public function testReportPageTokenSip() { + + // page with sip (default, without explizit definition) + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page with sip (explizit definition via 's') + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|s' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page without sip (explizit definition via 's:0') + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|s:0' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="?id=form&r=123&a=hello&type=5&L=3&final=world" class="internal" >Person</a>', $result); + + // page with sip (explizit definition via 's:1') + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|s:1' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page with sip (explizit definition via 's:1') + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >index.php?id=form&type=5&L=3&s=badcaffee1234</a>', $result); + + } + + /** + * + */ + public function testReportPageTokenPicture() { + + // page & picture + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|P:picture.gif' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" ><img alt="picture.gif" src="picture.gif" title="picture.gif" ></a>', $result); + + // page & Edit + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|E' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span></a>', $result); + + // page & New + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|N' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span></a>', $result); + + // page & Help + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|H' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span></a>', $result); + + // page & Information + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|I' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span></a>', $result); + + // page & Show + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|S' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span></a>', $result); + + // page & Show & Text + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|S|t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span> Person</a>', $result); + } + + /** + * + */ + public function testReportPageTokenBullet() { + + // page & bullet (green) + $result = $this->report->process("10.sql = SELECT 'p:form|B' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" ></a>', $result); + + // page & bullet (green) + $result = $this->report->process("10.sql = SELECT 'p:form|B||t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" > Person</a>', $result); + + $arr = ['blue', 'gray', 'green', 'pink', 'red', 'yellow', 'fake']; + foreach ($arr as $color) { + // page & bullet $color + $result = $this->report->process("10.sql = SELECT 'p:form|B:$color' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("<a href=\"index.php?id=form&s=badcaffee1234\" ><img alt=\"Bullet $color\" src=\"typo3conf/ext/qfq/Resources/Public/icons/bullet-$color.gif\" title=\"$color\" ></a>", $result); + } + } + + /** + * + */ + public function testReportPageTokenCheck() { + + // page & bullet (green) + $result = $this->report->process("10.sql = SELECT 'p:form|C' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" ></a>', $result); + + // page & bullet (green) + $result = $this->report->process("10.sql = SELECT 'p:form|C|t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img alt="Checked green" src="typo3conf/ext/qfq/Resources/Public/icons/checked-green.gif" title="green" > Person</a>', $result); + + $arr = ['blue', 'gray', 'green', 'pink', 'red', 'yellow', 'fake']; + foreach ($arr as $color) { + // page & bullet $color + $result = $this->report->process("10.sql = SELECT 'p:form|C:$color' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("<a href=\"index.php?id=form&s=badcaffee1234\" ><img alt=\"Checked $color\" src=\"typo3conf/ext/qfq/Resources/Public/icons/checked-$color.gif\" title=\"$color\" ></a>", $result); + } + } + + /** + * + */ + public function testReportPageTokenUrlParam() { + + // page & Url Param + $result = $this->report->process("10.sql = SELECT 'p:form|U:r=234|s:0|t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="?id=form&r=234" class="internal" >Person</a>', $result); + + // page & Url Param='' + $result = $this->report->process("10.sql = SELECT 'p:form|U|s:0|t:Person' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="?id=form" class="internal" >Person</a>', $result); + } + + /** + * + */ + public function testReportPageTokenToolTip() { + + // page & Tooltip=Tool Tip + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|o:Tool Tip' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" title="Tool Tip" >Person</a>', $result); + + // page & Tooltip='' + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|o' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + } + + /** + * + */ + public function testReportPageTokenAltText() { + + // page & AltText - no image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|a' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & AltText - no image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|a:' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & AltText - no image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|a:Hello World' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & AltText - image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|B|a' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" > Person</a>', $result); + + // page & AltText - image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|B|a:' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" > Person</a>', $result); + + // page & AltText - image + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|B|a:Hello World' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" ><img alt="Hello World" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" > Person</a>', $result); + } + + /** + * + */ + public function testReportPageTokenClass() { + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c:' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c:n' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" >Person</a>', $result); + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c:i' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c:e' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="external" >Person</a>', $result); + + // page & class (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|c:myclass' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="myclass" >Person</a>', $result); + + } + + /** + * + */ + public function testReportPageTokenTarget() { + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|g' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|g:' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|g:_blank' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" target="_blank" >Person</a>', $result); + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|g:_nextframe' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" target="_nextframe" >Person</a>', $result); + + } + + /** + * + */ + public function testReportPageTokenQuestion() { + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Please confirm', type: 'info', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _Page: pagealias, param, Text, Tooltip, Question + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question + $js = str_replace('Please confirm', 'My Question', $js); + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q:My Question' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question:Level + $js = str_replace('info', 'error', $js); + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q:My Question:error' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question:Level, Button Ok + $js = str_replace('Ok', 'YES', $js); + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q:My Question:error:YES' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question:Level, Button Cancel + $js = str_replace('Cancel', 'NO', $js); + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q:My Question:error:YES:NO' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question:Level (with escaped colon) +// $js = str_replace('Cancel', 'NO', $js); + $js = str_replace('My Question', 'Other Question:some nice value', $js); + $result = $this->report->process("10.sql = SELECT 'p:form&r=123&a=hello&type=5&L=3&final=world|t:Person|o:This is a tooltip|q:Other Question\\\:some nice value:error:YES:NO' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + } + + /** + * + */ + public function testReportPageTokenEncryption() { + // TODO: implement + } + + + /** + * + */ + public function testReportPageTokenRight() { + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|R' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >Person</a>', $result); + + // page & target (empty) + $result = $this->report->process("10.sql = SELECT 'p:form|t:Person|R|B' AS _page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" >Person <img alt="Bullet green" src="typo3conf/ext/qfq/Resources/Public/icons/bullet-green.gif" title="green" ></a>', $result); + + } + + /** + * + */ + public function testReportPageFix() { + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Please confirm', type: 'info', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _Page: only pagealias + $result = $this->report->process("10.sql = SELECT 'form' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" >index.php?id=form&s=badcaffee1234</a>', $result); + + // _Page: pagealias, param + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >index.php?id=form&type=5&L=3&s=badcaffee1234</a>', $result); + + // _Page: pagealias, param, Text + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question + $js = str_replace('Please confirm', 'My Question', $js); + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="internal" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="myclass" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="myclass" target="mytarget" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 0: URL + Text + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget|0' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="myclass" target="mytarget" title="This is a tooltip" ' . $js . ' >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 0: URL + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world||This is a tooltip|My Question|myclass|mytarget|0' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="myclass" target="mytarget" title="This is a tooltip" ' . $js . ' >index.php?id=form&type=5&L=3&s=badcaffee1234</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 2: URL + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world||This is a tooltip|My Question|myclass|mytarget|2' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 3: URL + Text + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget|3' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('Person', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 4: URL + Text + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget|4' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('index.php?id=form&type=5&L=3&s=badcaffee1234', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 4: URL + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget|4' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('index.php?id=form&type=5&L=3&s=badcaffee1234', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 4: URL + Text + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip|My Question|myclass|mytarget|5' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode 4: URL + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world||This is a tooltip|My Question|myclass|mytarget|5' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode, Sip ON (1) + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip||myclass|mytarget|0|1' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=5&L=3&s=badcaffee1234" class="myclass" target="mytarget" title="This is a tooltip" >Person</a>', $result); + + // _Page: pagealias, param, Text, Tooltip, Question, Class, Target, Rendermode, Sip OFF (0) + $result = $this->report->process("10.sql = SELECT 'form&r=123&a=hello&type=5&L=3&final=world|Person|This is a tooltip||myclass|mytarget|0|0' AS _Page FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="?id=form&r=123&a=hello&type=5&L=3&final=world" class="myclass" target="mytarget" title="This is a tooltip" >Person</a>', $result); + } + + /** + * + */ + public function testReportPageC() { + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Please confirm!', type: 'info', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _Page: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form' AS _pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >index.php?id=form&s=badcaffee1234</a>', $result); + + // _Page: other than defaults for the alert. + $js = str_replace('Please confirm!', 'Do you like to open', $js); + $js = str_replace("type: 'info'", "type: 'success'", $js); + $js = str_replace('Ok', 'yes', $js); + $js = str_replace('Cancel', 'no', $js); + $js = str_replace('timeout: 0', 'timeout: 10000', $js); + $js = str_replace('modal: true', 'modal: false', $js); + $result = $this->report->process("10.sql = SELECT 'p:form|q:Do you like to open:success:yes:no:10:0' AS _pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >index.php?id=form&s=badcaffee1234</a>', $result); + + + $result = $this->report->process("10.sql = SELECT 'p:form|q:Do you like to open:success:yes:no:10:0|t:click me' AS _pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >click me</a>', $result); + + } + + /** + * + */ + public function testReportPageFixC() { + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Please confirm!', type: 'info', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _Page: incl. alert + $result = $this->report->process("10.sql = SELECT 'form' AS _Pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >index.php?id=form&s=badcaffee1234</a>', $result); + + // _Page: other than defaults for the alert. + $js = str_replace('Please confirm!', 'Do you like to open', $js); + $js = str_replace("type: 'info'", "type: 'success'", $js); + $js = str_replace('Ok', 'yes', $js); + $js = str_replace('Cancel', 'no', $js); + $js = str_replace('timeout: 0', 'timeout: 10000', $js); + $js = str_replace('modal: true', 'modal: false', $js); + $result = $this->report->process("10.sql = SELECT 'form|||Do you like to open:success:yes:no:10:0' AS _Pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >index.php?id=form&s=badcaffee1234</a>', $result); + + + $result = $this->report->process("10.sql = SELECT 'form|click me||Do you like to open:success:yes:no:10:0' AS _Pagec FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&s=badcaffee1234" class="internal" ' . $js . ' >click me</a>', $result); + + } + + /** + * + */ + public function testReportPageD() { + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Do you really want to delete the record?', type: 'warning', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'U:table=Person&r=123' AS _paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + // Check das via '_paged' SIP_MODE_ANSWER and SIP_TARGET_URL has been set. + $result = \qfq\Session::get('badcaffee1234'); + $this->assertEquals('_modeAnswer=html&_targetUrl=localhost&r=123&table=Person', $result); + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'U:form=Person&r=123' AS _paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + // _paged: other than defaults for the alert. + $js = str_replace('Do you really want to delete the record?', 'Move to trash?', $js); + $js = str_replace("type: 'info'", "type: 'success'", $js); + $js = str_replace('Ok', 'yes', $js); + $js = str_replace('Cancel', 'no', $js); + $js = str_replace('timeout: 0', 'timeout: 10000', $js); + $js = str_replace('modal: true', 'modal: false', $js); + $js = str_replace("type: 'warning'", "type: 'success'", $js); + $result = $this->report->process("10.sql = SELECT 'U:table=Person&r=123|q:Move to trash?:success:yes:no:10:0' AS _paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + + $result = $this->report->process("10.sql = SELECT 'U:table=Person&r=123|q:Move to trash?:success:yes:no:10:0|t:click me' AS _paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixD() { + + $js = <<<EOF +id="12345" onClick="var alert = new QfqNS.Alert({ message: 'Do you really want to delete the record?', type: 'warning', modal: true, timeout: 0, buttons: [ + { label: 'Ok', eventName: 'ok' }, + { label: 'Cancel',eventName: 'cancel'} +] } ); +alert.on('alert.ok', function() { + window.location = $('#12345').attr('href'); +}); + +alert.show(); +return false;" +EOF; + + // _Paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'table=Person&r=123' AS _Paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + // _Paged: other than defaults for the alert. + $js = str_replace('Do you really want to delete the record?', 'Move to trash?', $js); + $js = str_replace("type: 'info'", "type: 'success'", $js); + $js = str_replace('Ok', 'yes', $js); + $js = str_replace('Cancel', 'no', $js); + $js = str_replace('timeout: 0', 'timeout: 10000', $js); + $js = str_replace('modal: true', 'modal: false', $js); + $js = str_replace("type: 'warning'", "type: 'success'", $js); + $result = $this->report->process("10.sql = SELECT 'table=Person&r=123|||Move to trash?:success:yes:no:10:0' AS _Paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span></a>', $result); + + + $result = $this->report->process("10.sql = SELECT 'table=Person&r=123|click me||Move to trash?:success:yes:no:10:0' AS _Paged FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="' . API_DIR . '/' . API_DELETE_PHP . '?s=badcaffee1234" class="btn btn-default" title="Delete" ' . $js . ' ><span class="glyphicon glyphicon-trash" ></span> click me</a>', $result); + } + + /** + * Missing r, missing form or table + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException1() { + $this->report->process("10.sql = SELECT '' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * Missing r, given table + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException2() { + $this->report->process("10.sql = SELECT 'table=Person' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * Missing r, given form + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException3() { + $this->report->process("10.sql = SELECT 'form=Person' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * missing form, missing table + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException4() { + $this->report->process("10.sql = SELECT 'r=123' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * missng r, given table + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException5() { + $this->report->process("10.sql = SELECT 'table=Person&r' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * missing table, given r + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException6() { + $this->report->process("10.sql = SELECT 'table&r=123' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * missing form, given r + * @expectedException \qfq\UserReportException + */ + public function testMissingPagedParameterException7() { + $this->report->process("10.sql = SELECT 'form&r=123' AS _Paged FROM Person ORDER BY id LIMIT 1"); + } + + /** + * + */ + public function testReportPageE() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4' AS _pagee FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4|t:click me' AS _pagee FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixE() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4' AS _Pagee FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4|click me' AS _Pagee FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Edit" ><span class="glyphicon glyphicon-pencil" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageH() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4' AS _pageh FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4|t:click me' AS _pageh FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixH() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4' AS _Pageh FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4|click me' AS _Pageh FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Help" ><span class="glyphicon glyphicon glyphicon-question-sign" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageI() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4' AS _pagei FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4|t:click me' AS _pagei FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixI() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4' AS _Pagei FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4|click me' AS _Pagei FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Information" ><span class="glyphicon glyphicon glyphicon-info-sign" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageN() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4' AS _pagen FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4|t:click me' AS _pagen FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixN() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4' AS _Pagen FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4|click me' AS _Pagen FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="New" ><span class="glyphicon glyphicon-plus" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageS() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4' AS _pages FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'p:form&a=1&r=3&type=4|t:click me' AS _pages FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportPageFixS() { + + // _paged: incl. alert + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4' AS _Pages FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span></a>', $result); + + $result = $this->report->process("10.sql = SELECT 'form&a=1&r=3&type=4|click me' AS _Pages FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals('<a href="index.php?id=form&type=4&s=badcaffee1234" class="btn btn-default" title="Details" ><span class="glyphicon glyphicon glyphicon-search" ></span> click me</a>', $result); + } + + /** + * + */ + public function testReportBullet() { + + $arr = ['blue', 'gray', 'green', 'pink', 'red', 'yellow', 'fake']; + foreach ($arr as $color) { + // bullet $color + $result = $this->report->process("10.sql = SELECT '$color' AS _bullet FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("<img alt=\"Bullet $color\" src=\"typo3conf/ext/qfq/Resources/Public/icons/bullet-$color.gif\" title=\"$color\" >", $result); + } + } + + /** + * + */ + public function testReportCheck() { + + $arr = ['blue', 'gray', 'green', 'pink', 'red', 'yellow', 'fake']; + foreach ($arr as $color) { + // check $color + $result = $this->report->process("10.sql = SELECT '$color' AS _check FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("<img alt=\"Checked $color\" src=\"typo3conf/ext/qfq/Resources/Public/icons/checked-$color.gif\" title=\"$color\" >", $result); + } + } + + /** + * + */ + public function testReportSurpress() { + + $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1"); + $this->assertEquals("normaltext", $result); + + $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.hidden}}'"); + $this->assertEquals("normaltexthidden", $result); + + $result = $this->report->process("10.sql = SELECT 'normal', 'hidden' AS _hidden, 'text' FROM Person ORDER BY id LIMIT 1\n10.10.sql = SELECT '{{10.unknown}}'"); + $this->assertEquals("normaltext{{10.unknown}}", $result); + } + + + /** + * + */ + public function testReportPageTokenMail() { + // TODO: implement + } + + /** + * + */ + public function testReportPageTokenRender() { + // TODO: implement + } + + + /** + * @throws Exception + */ + protected function setUp() { + + parent::setUp(); + + $GLOBALS["TSFE"] = new FakeTSFEReport(); + $this->eval = new qfq\Evaluate($this->store, $this->db); + $this->report = new qfq\Report(array(), $this->eval, true); + + $this->executeSQLFile(__DIR__ . '/fixtures/Generic.sql', true); + } +} + +class FakeTSFEReport { + public $id = 1; + public $type = 1; + public $sys_language_uid = 1; +} diff --git a/extension/qfq/tests/phpunit/SanitizeTest.php b/extension/qfq/tests/phpunit/SanitizeTest.php index 1ce2343bd97683349dbadd6cba0e7f1159278a20..24f02a2a076f386fd8a1dad3c15f5e9033e91b45 100644 --- a/extension/qfq/tests/phpunit/SanitizeTest.php +++ b/extension/qfq/tests/phpunit/SanitizeTest.php @@ -14,6 +14,10 @@ require_once(__DIR__ . '/../../qfq/exceptions/CodeException.php'); class SanitizeTest extends \PHPUnit_Framework_TestCase { + /** + * @throws CodeException + * @throws UserFormException + */ public function testSanitize() { # Violates SANITIZE class: SANITIZE string is always an empty string. @@ -26,6 +30,7 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize('', SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals('', Sanitize::sanitize('', SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals('', Sanitize::sanitize('', SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals('', Sanitize::sanitize('', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); # Check '1' $this->assertEquals('1', Sanitize::sanitize('1', SANITIZE_ALLOW_ALNUMX), "SANITIZE_ALNUMX fails"); @@ -34,6 +39,7 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize('1', SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals('1', Sanitize::sanitize('1', SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals('1', Sanitize::sanitize('1', SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals('1', Sanitize::sanitize('1', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); # Check '-3' $this->assertEquals('-3', Sanitize::sanitize('-3', SANITIZE_ALLOW_ALNUMX), "SANITIZE_ALNUMX fails"); @@ -42,6 +48,7 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize('-3', SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals('-3', Sanitize::sanitize('-3', SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals('-3', Sanitize::sanitize('-3', SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals('-3', Sanitize::sanitize('-3', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); # Check 'a' $this->assertEquals('a', Sanitize::sanitize('a', SANITIZE_ALLOW_ALNUMX), "SANITIZE_ALNUMX fails"); @@ -50,6 +57,7 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize('a', SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals('a', Sanitize::sanitize('a', SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals('a', Sanitize::sanitize('a', SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals('a', Sanitize::sanitize('a', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); # Check 'a@-_.,;Z09' @@ -60,6 +68,7 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize($val, SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); # Check 'a+Z09' $val = 'a+Z09'; @@ -69,8 +78,13 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('', Sanitize::sanitize($val, SANITIZE_ALLOW_EMAIL), "SANITIZE_EMAIL fails"); $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_PATTERN, '.*'), "SANITIZE_PATTERN fails"); $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_ALL), "SANITIZE_ALL fails"); + $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLBUT fails"); } + /** + * @throws CodeException + * @throws UserFormException + */ public function testSanitizeMinMax() { # Check min|max @@ -84,6 +98,10 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_MIN_MAX, '-100|200'), "SANITIZE_MIN_MAX fails"); } + /** + * @throws CodeException + * @throws UserFormException + */ public function testSanitizeMinMaxDate() { # Check min|max @@ -112,6 +130,10 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { } + /** + * @throws CodeException + * @throws UserFormException + */ public function testSanitizeEmail() { # Check @@ -152,6 +174,10 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('jo%hn@doe.com', Sanitize::sanitize($val, SANITIZE_ALLOW_EMAIL), "SANITIZE_ALLOW_EMAIL fails"); } + /** + * @throws CodeException + * @throws UserFormException + */ public function testSanitizePattern() { # Check @@ -164,6 +190,29 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($val, Sanitize::sanitize($val, SANITIZE_ALLOW_PATTERN, '(John)*'), "SANITIZE_ALLOW_PATTERN fails"); } + //[ ] { } % & \ # + /** + */ + public function testSanitizeExceptionAllBut() { + $bad = "[]{}%&\\#"; + $good = 'abCD01`~!@$^*()_+=-|":;.,<>/?\''; + + // Single + $this->assertEquals('', Sanitize::sanitize('[', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLOW_ALLBUT fails"); + $this->assertEquals('a', Sanitize::sanitize('a', SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLOW_ALLBUT fails"); + + + for ($i = 0; $i < strlen($bad); $i++) { + $str = '-' . substr($bad, $i, 1) . '-'; + $this->assertEquals('', Sanitize::sanitize($str, SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLOW_ALLBUT fails"); + } + + for ($i = 0; $i < strlen($good); $i++) { + $str = '-' . substr($good, $i, 1) . '-'; + $this->assertEquals($str, Sanitize::sanitize($str, SANITIZE_ALLOW_ALLBUT), "SANITIZE_ALLOW_ALLBUT fails"); + } + } + /** * @expectedException \qfq\CodeException */ @@ -205,5 +254,4 @@ class SanitizeTest extends \PHPUnit_Framework_TestCase { public function testSanitizeExceptionCheckFailed() { Sanitize::sanitize('string', SANITIZE_ALLOW_DIGIT, '', SANATIZE_EXCEPTION); } - } diff --git a/extension/qfq/tests/phpunit/SessionTest.php b/extension/qfq/tests/phpunit/SessionTest.php index 89daa9500ae778bbf08ca27699444db9e1f92b82..d2c74a3938cf1771defe03f2ecd414b15aaf29a7 100644 --- a/extension/qfq/tests/phpunit/SessionTest.php +++ b/extension/qfq/tests/phpunit/SessionTest.php @@ -62,7 +62,7 @@ class SessionTest extends \PHPUnit_Framework_TestCase { // write/read data1 Session::set('var1', 'data1'); - Session::clear(); + Session::clearAll(); $val = Session::get('var1'); $this->assertEquals(false, $val); diff --git a/extension/qfq/tests/phpunit/SipTest.php b/extension/qfq/tests/phpunit/SipTest.php index 04d559a7dff16014814e57778306cef67d4fb900..a12d8e1ca0d614245c91bcb7a3e7a9c8f1e08dc3 100644 --- a/extension/qfq/tests/phpunit/SipTest.php +++ b/extension/qfq/tests/phpunit/SipTest.php @@ -51,24 +51,27 @@ class SipTest extends \PHPUnit_Framework_TestCase { $result = $sip->queryStringToSip("id=input&r=1&L=2&form=person&type=99", RETURN_SIP); $this->assertEquals('badcaffee1234', $result); - $sip->sipUniqId('badcaffee0000'); $result = $sip->queryStringToSip("id=input&r=10&L=2&form=person&type=99", RETURN_SIP); - $this->assertEquals('badcaffee0000', $result); + $this->assertEquals('badcaffee1234', $result); } + /** + * @throws CodeException + * @throws UserFormException + */ public function testGetVarsFromSip() { $sip = new Sip('fakesessionname', true); $sip->sipUniqId('badcaffee1234'); $sip2 = $sip->queryStringToSip("http://example.com/index.php?a=1&b=2&c=3", RETURN_SIP); $arr = $sip->getVarsFromSip($sip2); - $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], $arr); + $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'r' => 0], $arr); $this->assertEquals('badcaffee1234', $sip2); $sip2 = $sip->queryStringToSip("http://example.com/index.php?e=1&f=2&g=3", RETURN_SIP); $arr = $sip->getVarsFromSip($sip2); - $this->assertEquals(['e' => 1, 'f' => 2, 'g' => 3], $arr); + $this->assertEquals(['e' => 1, 'f' => 2, 'g' => 3, 'r' => 0], $arr); $this->assertEquals('badcaffee1234', $sip2); $sip->sipUniqId('badcaffee0000'); @@ -77,15 +80,18 @@ class SipTest extends \PHPUnit_Framework_TestCase { $sip2 = $sip->queryStringToSip("http://example.com/index.php?aa=hello&bb=world", RETURN_SIP); $arr = $sip->getVarsFromSip($sip2); - $this->assertEquals(['aa' => 'hello', 'bb' => 'world'], $arr); - $this->assertEquals('badcaffee0000', $sip2); + $this->assertEquals(['aa' => 'hello', 'bb' => 'world', 'r' => 0], $arr); + $this->assertEquals('badcaffee1234', $sip2); $sip2 = $sip->queryStringToSip("aaa=Don&bbb=John", RETURN_SIP); $arr = $sip->getVarsFromSip($sip2); - $this->assertEquals(['aaa' => 'Don', 'bbb' => 'John'], $arr); + $this->assertEquals(['aaa' => 'Don', 'bbb' => 'John', 'r' => 0], $arr); } + /** + * + */ public function testFakeUniqId() { $sip = new Sip('fakesessionname', true); $this->assertEquals('badcaffee1234', $sip->sipUniqId('badcaffee1234')); @@ -94,6 +100,9 @@ class SipTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('badcaffee5678', $sip->sipUniqId('badcaffee5678')); } + /** + * @throws CodeException + */ public function testGetSipFromUrlParam() { $sip = new Sip('fakesessionname', true); @@ -106,13 +115,17 @@ class SipTest extends \PHPUnit_Framework_TestCase { $s = $sip->getSipFromQueryString('UnknwonParameter=1234'); $this->assertFalse($s); - $sip->sipUniqId('badcaffee1111'); - $url = $sip->queryStringToSip("a=10&b=20&c=30", RETURN_SIP); - $s = $sip->getSipFromQueryString('a=10&b=20&c=30'); - $this->assertEquals('badcaffee1111', $s); + // TODO : Test wieder reinnehmen: +// $sip->sipUniqId('badcaffee1111'); +// $url = $sip->queryStringToSip("a=10&b=20&c=30", RETURN_SIP); +// $s = $sip->getSipFromQueryString('a=10&b=20&c=30'); +// $this->assertEquals('badcaffee1111', $s); } + /** + * + */ public function testSipUniqId() { $sip = new Sip('fakesessionname', true); $sip->sipUniqId('badcaffee1234'); @@ -121,6 +134,9 @@ class SipTest extends \PHPUnit_Framework_TestCase { $this->assertEquals('badcaffee1234', $s); } + /** + * @throws CodeException + */ public function testGetQueryStringFromSip() { $sip = new Sip('fakesessionname', true); $sip->sipUniqId('badcaffee1234'); diff --git a/extension/qfq/tests/phpunit/StoreTest.php b/extension/qfq/tests/phpunit/StoreTest.php index 072fe85ec04dcd7998abff18903a4548dc6c7b8c..279283535ba216f928bbb1d3796e0939c5a60800 100644 --- a/extension/qfq/tests/phpunit/StoreTest.php +++ b/extension/qfq/tests/phpunit/StoreTest.php @@ -66,13 +66,16 @@ class StoreTest extends \PHPUnit_Framework_TestCase { public function testSetVarStoreSystem() { + // set new Sessionname + $this->store->setVar(SYSTEM_SQL_LOG_MODE, "all", STORE_SYSTEM); + // Sessionname: default value - $this->assertEquals('qfq', $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM), "System: SESSIONNAME"); + $this->assertEquals('all', $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM), "System: SQL_LOG"); // set new Sessionname - $this->store->setVar(SYSTEM_SESSION_NAME, "anothersessionname", STORE_SYSTEM); + $this->store->setVar(SYSTEM_SQL_LOG_MODE, "modify", STORE_SYSTEM); - $this->assertEquals('anothersessionname', $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM), "System: SESSIONNAME"); + $this->assertEquals('modify', $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM), "System: SQL_LOG"); } @@ -117,7 +120,9 @@ class StoreTest extends \PHPUnit_Framework_TestCase { public function testStorePriority() { //default prio FSRD + $this->store->unsetStore(STORE_RECORD); $this->store->fillStoreTableDefaultColumnType('Person'); + $this->assertEquals('male', $this->store->getVar('gender'), "Get default definition from table person.gender"); $this->store->setVar('gender', 'female', STORE_RECORD); diff --git a/extension/qfq/tests/phpunit/SupportTest.php b/extension/qfq/tests/phpunit/SupportTest.php index 2b11c43c73d7778b810b0bc81153464cd51d019c..c5bcf446da87aa3d9e810589addf3ba4e68ea658 100644 --- a/extension/qfq/tests/phpunit/SupportTest.php +++ b/extension/qfq/tests/phpunit/SupportTest.php @@ -291,17 +291,20 @@ class SupportTest extends \PHPUnit_Framework_TestCase { } public function testEncryptDoubleCurlyBraces() { +#/+open+/# +#/+close+/# + $arr = [ ['', ''], ['1', '1'], ["1\n2", "1\n2"], ['{', '{'], - ['#&@[[@_#', '{{'], + ['#/+open+/#', '{{'], ['-\{-', '-\{-'], - ['#&@[[@_##&@]]@_#-#&@[[@_##&@]]@_#', '{{}}-{{}}'], - ['#&@[[@_#hello#&@[[@_#world#&@]]@_##&@]]@_#', '{{hello{{world}}}}'], - ["\n\n##&@[[@_#\n#&@]]@_#", "\n\n#{{\n}}"], + ['#/+open+/##/+close+/#-#/+open+/##/+close+/#', '{{}}-{{}}'], + ['#/+open+/#hello#/+open+/#world#/+close+/##/+close+/#', '{{hello{{world}}}}'], + ["\n\n##/+open+/#\n#/+close+/#", "\n\n#{{\n}}"], ]; foreach ($arr as $tuple) { @@ -349,6 +352,58 @@ class SupportTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(['id' => 2], $new); } + + public function testEscapeDoubleTick() { + + // PLAIN + + // empty string + $new = Support::escapeDoubleTick(''); + $this->assertEquals('', $new); + + // nothing to replace + $new = Support::escapeDoubleTick('hello world'); + $this->assertEquals('hello world', $new); + + // last word + $new = Support::escapeDoubleTick('hello "world"'); + $this->assertEquals('hello \\"world\\"', $new); + + // first word + $new = Support::escapeDoubleTick('"hello" world'); + $this->assertEquals('\\"hello\\" world', $new); + + // three double tick + $new = Support::escapeDoubleTick('"""'); + $this->assertEquals('\\"\\"\\"', $new); + + // just " + $new = Support::escapeDoubleTick('"'); + $this->assertEquals('\\"', $new); + + // already ESCAPED + + // just \" + $new = Support::escapeDoubleTick('\\"'); + $this->assertEquals('\\"', $new); + + // already escaped: middle + $new = Support::escapeDoubleTick('hello \\"T world'); + $this->assertEquals('hello \\"T world', $new); + + // already escaped: start + $new = Support::escapeDoubleTick('\\"T hello world'); + $this->assertEquals('\\"T hello world', $new); + + // already escaped: end + $new = Support::escapeDoubleTick('hello world \\"'); + $this->assertEquals('hello world \\"', $new); + + // three double tick + $new = Support::escapeDoubleTick('\\"\\"\\"'); + $this->assertEquals('\\"\\"\\"', $new); + } + protected function setUp() { parent::setUp(); diff --git a/extension/qfq/tests/phpunit/fixtures/Generic.sql b/extension/qfq/tests/phpunit/fixtures/Generic.sql index fb4a96802750aa0595c54535039cea57457d23fe..0a4bf6e5b38dbfcd09079cb24b4ebd6e2a314650 100644 --- a/extension/qfq/tests/phpunit/fixtures/Generic.sql +++ b/extension/qfq/tests/phpunit/fixtures/Generic.sql @@ -1,28 +1,29 @@ 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 '' + firstName VARCHAR(128), + adrId INT(11) NOT NULL DEFAULT 0, + gender ENUM('', 'male', 'female') NOT NULL DEFAULT 'male', + groups SET('', 'a', 'b', 'c') NOT NULL DEFAULT '' ); -INSERT INTO Person (name, firstname, gender, groups) VALUES +INSERT INTO Person (name, firstName, gender, groups) VALUES ('Doe', 'John', 'male', 'c'), ('Smith', 'Jane', 'female', 'a,c'); 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 + id BIGINT AUTO_INCREMENT PRIMARY KEY, + grId INT(11) NOT NULL DEFAULT 0, + personId BIGINT(20) DEFAULT NULL, + value VARCHAR(128), + t1 TEXT, + t2 TEXT ); -INSERT INTO Note (gr_id, value, t1, t2) VALUES +INSERT INTO Note (grId, 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'); @@ -30,11 +31,11 @@ INSERT INTO Note (gr_id, value, t1, t2) VALUES 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 '', + id BIGINT(20) NOT NULL AUTO_INCREMENT, + personId 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/extension/qfq/tests/phpunit/fixtures/TestFormEditor.sql b/extension/qfq/tests/phpunit/fixtures/TestFormEditor.sql index dd8892992eff3cea85fc6145c3cbbb191c6454ad..4ba6abba7e3d15a0e19da2a466a71e8f9496800a 100644 --- a/extension/qfq/tests/phpunit/fixtures/TestFormEditor.sql +++ b/extension/qfq/tests/phpunit/fixtures/TestFormEditor.sql @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS `Form` ( `permitNew` ENUM('sip', 'logged_in', 'logged_out', 'always', 'never') NOT NULL DEFAULT 'sip', `permitEdit` ENUM('sip', 'logged_in', 'logged_out', 'always', 'never') NOT NULL DEFAULT 'sip', `render` ENUM('plain', 'table', 'bootstrap') NOT NULL DEFAULT 'plain', + `requiredParameter` VARCHAR(255) NOT NULL DEFAULT '', `showButton` SET('new', 'delete') NOT NULL DEFAULT 'new,delete', `multiMode` ENUM('none', 'horizontal', 'vertical') NOT NULL DEFAULT 'none', `multiSql` TEXT NOT NULL, @@ -53,51 +54,52 @@ CREATE TABLE IF NOT EXISTS `Form` ( DROP TABLE IF EXISTS `FormElement`; CREATE TABLE IF NOT EXISTS `FormElement` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `formId` INT(11) NOT NULL, - `feIdContainer` INT(11) NOT NULL DEFAULT '0', - `dynamicUpdate` ENUM('yes', 'no') NOT NULL DEFAULT 'no', + `id` INT(11) NOT NULL AUTO_INCREMENT, + `formId` INT(11) NOT NULL, + `feIdContainer` INT(11) NOT NULL DEFAULT '0', + `dynamicUpdate` ENUM('yes', 'no') NOT NULL DEFAULT 'no', - `enabled` ENUM('yes', 'no') NOT NULL DEFAULT 'yes', + `enabled` ENUM('yes', 'no') NOT NULL DEFAULT 'yes', - `name` VARCHAR(255) NOT NULL DEFAULT '', - `label` VARCHAR(255) NOT NULL DEFAULT '', + `name` VARCHAR(255) NOT NULL DEFAULT '', + `label` VARCHAR(255) NOT NULL DEFAULT '', - `mode` ENUM('show', 'readonly', 'required', 'lock', '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', + `mode` ENUM('show', 'readonly', 'required', 'lock', 'disabled') NOT NULL DEFAULT 'show', + `modeSql` TEXT NOT NULL, + `class` ENUM('native', 'action', 'container') NOT NULL DEFAULT 'native', + `type` ENUM('checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', 'gridJQW', 'text', 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', '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') NOT NULL DEFAULT 'text', - `subrecordOption` SET('edit', 'delete', 'new') NOT NULL DEFAULT '', - `checkType` ENUM('alnumx', 'digit', 'email', 'min|max', 'min|max date', 'pattern', 'all') NOT NULL DEFAULT 'alnumx', + 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', + 'afterSave', 'afterInsert', 'afterUpdate', 'afterDelete', + 'sendmail') NOT NULL DEFAULT 'text', + `subrecordOption` SET('edit', 'delete', 'new') NOT NULL DEFAULT '', + `checkType` ENUM('alnumx', 'digit', 'email', 'min|max', 'min|max date', 'pattern', 'all') NOT NULL DEFAULT 'alnumx', - `checkPattern` VARCHAR(255) NOT NULL DEFAULT '', + `checkPattern` VARCHAR(255) NOT NULL DEFAULT '', - `onChange` VARCHAR(255) NOT NULL DEFAULT '', + `onChange` VARCHAR(255) NOT NULL DEFAULT '', - `ord` INT(11) NOT NULL DEFAULT '0', - `tabindex` INT(11) NOT NULL DEFAULT '0', + `ord` INT(11) NOT NULL DEFAULT '0', + `tabindex` INT(11) NOT NULL DEFAULT '0', - `size` VARCHAR(255) NOT NULL DEFAULT '', - `maxLength` VARCHAR(255) NOT NULL DEFAULT '', - `note` TEXT NOT NULL, - `tooltip` VARCHAR(255) NOT NULL DEFAULT '', - `placeholder` VARCHAR(255) NOT NULL DEFAULT '', + `size` VARCHAR(255) NOT NULL DEFAULT '', + `maxLength` VARCHAR(255) NOT NULL DEFAULT '', + `note` TEXT NOT NULL, + `tooltip` VARCHAR(255) NOT NULL DEFAULT '', + `placeholder` VARCHAR(255) NOT NULL DEFAULT '', - `value` TEXT NOT NULL, - `sql1` TEXT NOT NULL, - `sql2` TEXT NOT NULL, - `parameter` TEXT NOT NULL, + `value` TEXT NOT NULL, + `sql1` TEXT NOT NULL, + `sql2` TEXT NOT NULL, + `parameter` TEXT NOT NULL, `clientJs` TEXT, - `feGroup` VARCHAR(255) NOT NULL DEFAULT '', - `debug` ENUM('yes', 'no') NOT NULL DEFAULT 'no', - `deleted` ENUM('yes', 'no') NOT NULL DEFAULT 'no', - `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + `feGroup` VARCHAR(255) NOT NULL DEFAULT '', + `debug` ENUM('yes', 'no') NOT NULL DEFAULT 'no', + `deleted` ENUM('yes', 'no') NOT NULL DEFAULT 'no', + `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), KEY `formId` (`formId`), @@ -126,42 +128,42 @@ INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, r 'Form', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=3'); # FormEditor: FormElements -INSERT INTO FormElement (formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer) +INSERT INTO FormElement (formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, modeSql) VALUES - (1, 'basic', 'Basic', 'show', 'pill', 'container', 10, 0, 0, '', '', '', '', '', '', 0), - (1, 'permission', 'Permission', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0), - (1, 'various', 'Various', 'show', 'pill', 'container', 30, 0, 0, '', '', '', '', '', '', 0), - (1, 'formelement', 'Formelement', 'show', 'pill', 'container', 40, 0, 0, '', '', '', '', '', '', 0), - - (1, 'id', 'id', 'readonly', 'text', 'native', 100, 10, 11, '', '', '', '', '', '', 1), - (1, 'name', 'Name', 'show', 'text', 'native', 120, 40, 255, '', '', '', '', '', 'autofocus=on', 1), - (1, 'title', 'Title', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 1), - (1, 'noteInternal', 'Note', 'show', 'text', 'native', 140, '40,3', 0, '', '', '', '', '', '', 1), - (1, 'tableName', 'Table', 'required', 'select', 'native', 150, 0, 0, '', '', '', '{{!SHOW tables}}', '', 'emptyItemAtStart', 1), - - (1, 'permitNew', 'Permit New', 'show', 'radio', 'native', 160, 0, 0, '', '', '', '', '', '', 2), - (1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'native', 170, 0, 0, '', '', '', '', '', '', 2), - (1, 'permitUrlParameter', 'Permit Url Parameter', 'show', 'text', 'native', 180, 40, 255, '', '', '', '', '', '', 2), - (1, 'render', 'Render', 'show', 'radio', 'native', 190, 0, 0, '', '', '', '', '', '', 2), - - (1, 'multi', 'Multi', 'show', 'fieldset', 'native', 210, 0, 0, '', '', '', '', '', '', 3), - (1, 'multiMode', 'Multi Mode', 'show', 'radio', 'native', 220, 0, 0, '', '', '', '', '', '', 3), - (1, 'multiSql', 'Multi SQL', 'show', 'text', 'native', 230, '40,3', 0, '', '', '', '', '', '', 3), - (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'native', 240, 40, 255, '', '', '', '', '', '', 3), - (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'native', 250, 40, 255, '', '', '', '', '', '', 3), - (1, 'forwardMode', 'Forward', 'show', 'radio', 'native', 260, 0, 0, '', '', '', '', '', '', 3), - (1, 'forwardPage', 'Forward Page', 'show', 'text', 'native', 270, 40, 255, '', '', '', '', '', '', 3), - (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'native', 280, 40, 250, '', '', '', '', '', '', 3), - (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'native', 290, 40, 250, '', '', '', '', '', '', 3), - (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'native', 300, 40, 250, '', '', '', '', '', '', 3), - - (1, 'deleted', 'Deleted', 'show', 'checkbox', 'native', 400, 0, 0, '', '', '', '', '', '', 3), - (1, 'modified', 'Modified', 'readonly', 'text', 'native', 410, 40, 20, '', '', '', '', '', '', 3), - (1, 'created', 'Created', 'readonly', 'text', 'native', 420, 40, 20, '', '', '', '', '', '', 3), + (1, 'basic', 'Basic', 'show', 'pill', 'container', 10, 0, 0, '', '', '', '', '', '', 0, ''), + (1, 'permission', 'Permission', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, ''), + (1, 'various', 'Various', 'show', 'pill', 'container', 30, 0, 0, '', '', '', '', '', '', 0, ''), + (1, 'formelement', 'Formelement', 'show', 'pill', 'container', 40, 0, 0, '', '', '', '', '', '', 0, ''), + + (1, 'id', 'id', 'readonly', 'text', 'native', 100, 10, 11, '', '', '', '', '', '', 1, ''), + (1, 'name', 'Name', 'show', 'text', 'native', 120, 40, 255, '', '', '', '', '', 'autofocus=on', 1, ''), + (1, 'title', 'Title', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 1, ''), + (1, 'noteInternal', 'Note', 'show', 'text', 'native', 140, '40,3', 0, '', '', '', '', '', '', 1, ''), + (1, 'tableName', 'Table', 'required', 'select', 'native', 150, 0, 0, '', '', '', '{{!SHOW tables}}', '', 'emptyItemAtStart', 1, ''), + + (1, 'permitNew', 'Permit New', 'show', 'radio', 'native', 160, 0, 0, '', '', '', '', '', '', 2, ''), + (1, 'permitEdit', 'Permit Edit', 'show', 'radio', 'native', 170, 0, 0, '', '', '', '', '', '', 2, ''), + (1, 'permitUrlParameter', 'Permit Url Parameter', 'show', 'text', 'native', 180, 40, 255, '', '', '', '', '', '', 2, ''), + (1, 'render', 'Render', 'show', 'radio', 'native', 190, 0, 0, '', '', '', '', '', '', 2, ''), + + (1, 'multi', 'Multi', 'show', 'fieldset', 'native', 210, 0, 0, '', '', '', '', '', '', 3, ''), + (1, 'multiMode', 'Multi Mode', 'show', 'radio', 'native', 220, 0, 0, '', '', '', '', '', '', 3, ''), + (1, 'multiSql', 'Multi SQL', 'show', 'text', 'native', 230, '40,3', 0, '', '', '', '', '', '', 3, ''), + (1, 'multiDetailForm', 'Multi Detail Form', 'show', 'text', 'native', 240, 40, 255, '', '', '', '', '', '', 3, ''), + (1, 'multiDetailFormParameter', 'Multi Detail Form Parameter', 'show', 'text', 'native', 250, 40, 255, '', '', '', '', '', '', 3, ''), + (1, 'forwardMode', 'Forward', 'show', 'radio', 'native', 260, 0, 0, '', '', '', '', '', '', 3, ''), + (1, 'forwardPage', 'Forward Page', 'show', 'text', 'native', 270, 40, 255, '', '', '', '', '', '', 3, ''), + (1, 'bsLabelColumns', 'BS Label Columns', 'show', 'text', 'native', 280, 40, 250, '', '', '', '', '', '', 3, ''), + (1, 'bsInputColumns', 'BS Input Columns', 'show', 'text', 'native', 290, 40, 250, '', '', '', '', '', '', 3, ''), + (1, 'bsNoteColumns', 'BS Note Columns', 'show', 'text', 'native', 300, 40, 250, '', '', '', '', '', '', 3, ''), + + (1, 'deleted', 'Deleted', 'show', 'checkbox', 'native', 400, 0, 0, '', '', '', '', '', '', 3, ''), + (1, 'modified', 'Modified', 'readonly', 'text', 'native', 410, 40, 20, '', '', '', '', '', '', 3, ''), + (1, 'created', 'Created', 'readonly', 'text', 'native', 420, 40, 20, '', '', '', '', '', '', 3, ''), (1, '', 'FormElements', 'show', 'subrecord', 'native', 500, 0, 0, '', '', '', '{{!SELECT * FROM FormElement WHERE formId={{id:R0}}}}', - '', 'form=formElement\npage=form.php', 4); + '', 'form=formElement\npage=form.php', 4, ''); # @@ -172,45 +174,45 @@ INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, r 'FormElement', 'always', 'always', 'bootstrap', '', 'maxVisiblePill=3'); # FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, debug) +INSERT INTO FormElement (id, formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, debug, modeSql) VALUES - (100, 2, 'basic', 'Basic', 'show', 'pill', 'container', 10, 0, 0, '', '', '', '', '', '', 0, 'no'), - (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no'), - (102, 2, 'layout', 'Layout', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no'), - (103, 2, 'value', 'Value', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no'), - (104, 2, 'info', 'Info', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no'), + (100, 2, 'basic', 'Basic', 'show', 'pill', 'container', 10, 0, 0, '', '', '', '', '', '', 0, 'no', ''), + (101, 2, 'check_order', 'Check & Order', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no', ''), + (102, 2, 'layout', 'Layout', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no', ''), + (103, 2, 'value', 'Value', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no', ''), + (104, 2, 'info', 'Info', 'show', 'pill', 'container', 20, 0, 0, '', '', '', '', '', '', 0, 'no', ''), - (110, 2, 'id', 'id', 'readonly', 'text', 'native', 100, 10, 11, '', '', '', '', '', '', 100, 'no'), - (111, 2, 'formId', 'formId', 'readonly', 'text', 'native', 120, 40, 255, '', '', '', '', '', '', 100, 'no'), + (110, 2, 'id', 'id', 'readonly', 'text', 'native', 100, 10, 11, '', '', '', '', '', '', 100, 'no', ''), + (111, 2, 'formId', 'formId', 'readonly', 'text', 'native', 120, 40, 255, '', '', '', '', '', '', 100, 'no', ''), (112, 2, 'feIdContainer', 'Container', 'show', 'select', 'native', 150, 0, 0, '', '', '', '{{!SELECT fe.id, CONCAT(fe.class, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{id}} AND fe.class="container" ORDER BY fe.ord }}', - '', 'emptyItemAtStart', 100, 'no'), - (113, 2, 'enabled', 'Enabled', 'show', 'checkbox', 'native', 120, 0, 0, '', '', '', '', '', '', 100, 'no'), - (114, 2, 'name', 'Name', 'show', 'text', 'native', 120, 40, 255, '', '', '', '', '', '', 100, 'no'), - (115, 2, 'label', 'Label', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 100, 'no'), - (116, 2, 'mode', 'Mode', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no'), - (117, 2, 'class', 'Class', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no'), - (118, 2, 'type', 'Type', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no'), - (119, 2, 'checkType', 'Check Type', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 101, 'no'), - (120, 2, 'checkPattern', 'Check Pattern', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no'), - (121, 2, 'onChange', 'JS onChange', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no'), - (122, 2, 'ord', 'Order', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no'), - (123, 2, 'tabindex', 'tabindex', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no'), - (124, 2, 'size', 'Size', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (125, 2, 'maxlenght', 'Maxlength', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (126, 2, 'note', 'note', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (127, 2, 'tooltip', 'Tooltip', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (128, 2, 'placeholder', 'Placeholder', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (129, 2, 'value', 'value', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no'), - (130, 2, 'sql1', 'sql1', 'show', 'text', 'native', 130, '40,4', 255, '', '', '', '', '', '', 103, 'no'), - (131, 2, 'parameter', 'Parameter', 'show', 'text', 'native', 130, '40,4', 255, '', '', '', '', '', '', 103, 'no'), - (132, 2, 'clientJs', 'ClientJS', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 103, 'no'), - (133, 2, 'feGroup', 'feGroup', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 104, 'no'), - (134, 2, 'debug', 'Debug', 'show', 'checkbox', 'native', 130, 0, 0, '', '', '', '', '', '', 104, 'no'), - (135, 2, 'deleted', 'Deleted', 'show', 'checkbox', 'native', 400, 0, 0, '', '', '', '', '', '', 104, 'no'), - (136, 2, 'modified', 'Modified', 'readonly', 'text', 'native', 410, 40, 20, '', '', '', '', '', '', 104, 'no'), - (137, 2, 'created', 'Created', 'readonly', 'text', 'native', 420, 40, 20, '', '', '', '', '', '', 104, 'no'); + '', 'emptyItemAtStart', 100, 'no', ''), + (113, 2, 'enabled', 'Enabled', 'show', 'checkbox', 'native', 120, 0, 0, '', '', '', '', '', '', 100, 'no', ''), + (114, 2, 'name', 'Name', 'show', 'text', 'native', 120, 40, 255, '', '', '', '', '', '', 100, 'no', ''), + (115, 2, 'label', 'Label', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 100, 'no', ''), + (116, 2, 'mode', 'Mode', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no', ''), + (117, 2, 'class', 'Class', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no', ''), + (118, 2, 'type', 'Type', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 100, 'no', ''), + (119, 2, 'checkType', 'Check Type', 'show', 'select', 'native', 120, 0, 255, '', '', '', '', '', '', 101, 'no', ''), + (120, 2, 'checkPattern', 'Check Pattern', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no', ''), + (121, 2, 'onChange', 'JS onChange', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no', ''), + (122, 2, 'ord', 'Order', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no', ''), + (123, 2, 'tabindex', 'tabindex', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 101, 'no', ''), + (124, 2, 'size', 'Size', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (125, 2, 'maxlenght', 'Maxlength', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (126, 2, 'note', 'note', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (127, 2, 'tooltip', 'Tooltip', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (128, 2, 'placeholder', 'Placeholder', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (129, 2, 'value', 'value', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 102, 'no', ''), + (130, 2, 'sql1', 'sql1', 'show', 'text', 'native', 130, '40,4', 255, '', '', '', '', '', '', 103, 'no', ''), + (131, 2, 'parameter', 'Parameter', 'show', 'text', 'native', 130, '40,4', 255, '', '', '', '', '', '', 103, 'no', ''), + (132, 2, 'clientJs', 'ClientJS', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 103, 'no', ''), + (133, 2, 'feGroup', 'feGroup', 'show', 'text', 'native', 130, 40, 255, '', '', '', '', '', '', 104, 'no', ''), + (134, 2, 'debug', 'Debug', 'show', 'checkbox', 'native', 130, 0, 0, '', '', '', '', '', '', 104, 'no', ''), + (135, 2, 'deleted', 'Deleted', 'show', 'checkbox', 'native', 400, 0, 0, '', '', '', '', '', '', 104, 'no', ''), + (136, 2, 'modified', 'Modified', 'readonly', 'text', 'native', 410, 40, 20, '', '', '', '', '', '', 104, 'no', ''), + (137, 2, 'created', 'Created', 'readonly', 'text', 'native', 420, 40, 20, '', '', '', '', '', '', 104, 'no', ''); # FormEditor: Small @@ -221,8 +223,8 @@ INSERT INTO Form (name, title, noteInternal, tableName, permitNew, permitEdit, r 'Person', 'always', 'always', 'bootstrap', '', ''); # FormEditor: FormElements -INSERT INTO FormElement (id, formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, debug) +INSERT INTO FormElement (id, formId, name, label, mode, type, class, ord, size, maxLength, note, clientJs, value, sql1, sql2, parameter, feIdContainer, debug, modeSql) VALUES - (200, 3, 'name', 'Name', 'show', 'text', 'native', 10, 50, 255, '', '', '', '', '', '', 0, 'no'), - (201, 3, 'firstName', 'Firstname', 'show', 'text', 'native', 10, 50, 255, '', '', '', '', '', '', 0, 'no'); + (200, 3, 'name', 'Name', 'show', 'text', 'native', 10, 50, 255, '', '', '', '', '', '', 0, 'no', ''), + (201, 3, 'firstName', 'Firstname', 'show', 'text', 'native', 10, 50, 255, '', '', '', '', '', '', 0, 'no', ''); diff --git a/javascript/src/Alert.js b/javascript/src/Alert.js index 57c9077f4978e1877d799e6fc9d2218d76c1ff14..0bdf82b1876948797096db987fc1b7e95c1f093a 100644 --- a/javascript/src/Alert.js +++ b/javascript/src/Alert.js @@ -6,6 +6,11 @@ /* global EventEmitter */ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { @@ -20,46 +25,91 @@ var QfqNS = QfqNS || {}; * The first instance displaying a message will append an `alert container` to the body. The last message being * dismissed will remove the `alert container`. A typical call sequence might look like: * - * var alert = new QfqNS.Alert("Text being displayed", "info", "none"); + * var alert = new QfqNS.Alert({ + * message: "Text being displayed", + * type: "info" + * }); * alert.show(); * - * Messages may have different background colors (severity levels), controlled by the second argument - * `messageType` of the constructor. The possible values are + * Messages may have different background colors (severity levels), controlled by the `type` property. Possible + * values are * + * * `"success"` * * `"info"` * * `"warning"` - * * `"error"` + * * `"error"`, `"danger"` * * The values are translated into Bootstrap `alert-*` classes internally. * - * Messages can feature clickable buttons, or no buttons at all, in which case a click anywhere on the message - * will dismiss it. Buttons are controlled by the third argument to the constructor: + * If no buttons are configured, a click anywhere on the alert will close it. + * + * Buttons are configured by passing an array of objects in the `buttons` property. The properties of the object + * are as follows + * + * { + * label: <button label>, + * focus: true | false, + * eventName: <eventname> + * } + * + * You can connect to the button events by using + * + * var alert = new QfqNS.Alert({ + * message: "Text being displayed", + * type: "info", + * buttons: [ + * { label: "OK", eventName: "ok" } + * ] + * }); + * alert.on('alert.ok', function(...) { ... }); * - * * `"okcancel"` - * * `"yesno"` - * * `"yesnosave"` - * * `"none"` + * Events are named according to `alert.<eventname>`. * - * Callback functions of the `Ok` or `Yes` button are added by calling Alert#addOkButtonHandler(). Callback - * functions of the `Cancel` or `No` button are added by calling Alert#addCancelButtonHandler(). Lastly, - * Alert#addSaveButtonHandler() adds callback functions to the `Save` button. + * If the property `modal` is set to `true`, a kind-of modal alert will be displayed, preventing clicks + * anywhere but the alert. * + * For compatibility reasons, the old constructor signature is still supported but deprecated + * + * var alert = new QfqNS.Alert(message, type, buttons) + * + * @param {object} options option object has following properties + * @param {string} options.message message to be displayed + * @param {string} [options.type] type of message, can be `"info"`, `"warning"`, or `"error"`. Default is `"info"`. + * @param {number} [options.timeout] timeout in milliseconds. If timeout is less than or equal to 0, the alert + * won't timeout and stay open until dismissed by the user. Default `0`. + * @param {boolean} [options.modal] whether or not alert is modal, i.e. prevent clicks anywhere but the dialog. + * Default is `false`. + * @param {object[]} options.buttons what buttons to display on alert. If empty array is provided, no buttons are + * displayed and a click anywhere in the alert will dismiss it. + * @param {string} options.buttons.label label of the button + * @param {string} options.buttons.eventName name of the event when button is clicked. + * @param {boolean} [options.buttons.focus] whether or not button has focus by default. Default is `false`. * - * @param message {string} message to be displayed - * @param messageType {string} type of message, can either be `"info"`, `"warning"`, or `"error"`. - * @param buttons {string} buttons to be displayed, can either be `"okcancel"`, `"yesno"`, `"yesnosave"`, or `"none"`. - * When `"none"` is provided, clicking anywhere on the message will dismiss it. * @constructor */ - n.Alert = function (message, messageType, buttons) { - this.message = message; - this.messageType = messageType || "info"; - this.buttons = buttons || "none"; + n.Alert = function (options) { + // Emulate old behavior of method signature + // function(message, messageType, buttons) + if (typeof options === "string") { + this.message = arguments[0]; + this.messageType = arguments[1] || "info"; + this.buttons = arguments[2] || []; + this.modal = false; + // this.timeout < 1 means forever + this.timeout = 0; + } else { + // new style + this.message = options.message || "MESSAGE"; + this.messageType = options.type || "info"; + this.buttons = options.buttons || []; + this.modal = options.modal || false; + this.timeout = options.timeout || 0; + } this.$alertDiv = null; + this.$modalDiv = null; this.shown = false; - // this.timeout < 1 means forever - this.timeout = 0; + this.fadeInDuration = 400; this.fadeOutDuration = 400; this.timerId = null; @@ -108,13 +158,16 @@ var QfqNS = QfqNS || {}; switch (this.messageType) { case "warning": return "alert-warning"; + case "danger": case "error": return "alert-danger"; + case "info": + return "alert-info"; + case "success": + return "alert-success"; /* jshint -W086 */ default: n.Log.warning("Message type '" + this.messageType + "' unknown. Use default type."); - case "info": - return "alert-success"; /* jshint +W086 */ } }; @@ -123,72 +176,27 @@ var QfqNS = QfqNS || {}; * @private */ n.Alert.prototype.getButtons = function () { - var buttons = null; - switch (this.buttons) { - case 'okcancel': - buttons = $("<div>") - .addClass("alert-buttons") - .append( - $("<a>") - .append("Ok") - .addClass("btn btn-default") - .click(this.okButtonHandler.bind(this)) - ) - .append( - $("<a>") - .append("Cancel") - .addClass("btn btn-default") - .click(this.cancelButtonHandler.bind(this)) - ); - return buttons; - case "yesno": - buttons = $("<div>") - .addClass("alert-buttons") - .append( - $("<a>") - .append("Yes") - .addClass("btn btn-default") - .click(this.okButtonHandler.bind(this)) - ) - .append( - $("<a>") - .append("No") - .addClass("btn btn-default") - .click(this.cancelButtonHandler.bind(this)) - ); - return buttons; - case "yesnosave": - buttons = $("<div>") - .addClass("alert-buttons") - .append( - $("<a>") - .append("Yes") - .addClass("btn btn-default") - .click(this.okButtonHandler.bind(this)) - ) - .append( - $("<a>") - .append("No") - .addClass("btn btn-default") - .click(this.cancelButtonHandler.bind(this)) - ) - .append( - $("<a>") - .append("Save & Close") - .addClass("btn btn-default") - .click(this.saveButtonHandler.bind(this)) - ) - ; - return buttons; - /* jshint -W086 */ - default: - n.Log.warning("Buttons '" + this.buttons + "' unknown. Use default buttons"); - case "none": - break; - /* jshint +W086 */ + var $buttons = null; + var numberOfButtons = this.buttons.length; + var index; + var buttonConfiguration; + + for (index = 0; index < numberOfButtons; index++) { + buttonConfiguration = this.buttons[index]; + + if (!$buttons) { + $buttons = $("<div>").addClass("alert-buttons"); + } + + var focus = buttonConfiguration.focus ? buttonConfiguration.focus : false; + + $buttons.append($("<button>").append(buttonConfiguration.label) + .attr('type', 'button') + .addClass("btn btn-default" + (focus ? " wants-focus" : "")) + .click(buttonConfiguration, this.buttonHandler.bind(this))); } - return buttons; + return $buttons; }; n.Alert.prototype.show = function () { @@ -198,6 +206,12 @@ var QfqNS = QfqNS || {}; } var $alertContainer = this.makeAlertContainerSingleton(); + if (this.modal) { + this.$modalDiv = $("<div>"); + this.$modalDiv.css('width', Math.max(document.documentElement.clientWidth, window.innerWidth || 0)); + this.$modalDiv.css('height', Math.max(document.documentElement.clientHeight, window.innerHeight || 0)); + } + this.$alertDiv = $("<div>") .hide() .addClass("alert") @@ -207,7 +221,7 @@ var QfqNS = QfqNS || {}; var buttons = this.getButtons(); - if (buttons && this.timeout < 1) { + if (buttons) { // Buttons will take care of removing the message this.$alertDiv.append(buttons); } else { @@ -215,8 +229,15 @@ var QfqNS = QfqNS || {}; this.$alertDiv.click(this.removeAlert.bind(this)); } - $alertContainer.append(this.$alertDiv); + if (this.modal) { + this.$modalDiv.append(this.$alertDiv); + $alertContainer.append(this.$modalDiv); + } else { + $alertContainer.append(this.$alertDiv); + } + this.$alertDiv.slideDown(this.fadeInDuration, this.afterFadeIn.bind(this)); + this.$alertDiv.find(".wants-focus").focus(); this.shown = true; @@ -247,8 +268,13 @@ var QfqNS = QfqNS || {}; this.$alertDiv.slideUp(this.fadeOutDuration, function () { that.$alertDiv.remove(); that.$alertDiv = null; + if (that.modal) { + that.$modalDiv.remove(); + that.$modalDiv = null; + } that.shown = false; + // TODO: removeAlert should not have knowledge on how to handle alert container if (that.countAlertsInAlertContainer() === 0) { that.removeAlertContainer(); } @@ -262,31 +288,9 @@ var QfqNS = QfqNS || {}; * * @private */ - n.Alert.prototype.okButtonHandler = function (handler) { - this.removeAlert(); - this.eventEmitter.emitEvent('alert.ok', n.EventEmitter.makePayload(this, null)); - }; - - /** - * - * @param handler - * - * @private - */ - n.Alert.prototype.saveButtonHandler = function (handler) { - this.removeAlert(); - this.eventEmitter.emitEvent('alert.save', n.EventEmitter.makePayload(this, null)); - }; - - /** - * - * @param handler - * - * @private - */ - n.Alert.prototype.cancelButtonHandler = function (handler) { + n.Alert.prototype.buttonHandler = function (event) { this.removeAlert(); - this.eventEmitter.emitEvent('alert.cancel', n.EventEmitter.makePayload(this, null)); + this.eventEmitter.emitEvent('alert.' + event.data.eventName, n.EventEmitter.makePayload(this, null)); }; n.Alert.prototype.isShown = function () { diff --git a/javascript/src/BSTabs.js b/javascript/src/BSTabs.js index 88a6b71b90dc9c7dc13b649549b292b048cd8b2f..8c1b86d644616612dbbf250f16397b71cabb5f3a 100644 --- a/javascript/src/BSTabs.js +++ b/javascript/src/BSTabs.js @@ -8,6 +8,11 @@ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; @@ -20,6 +25,8 @@ var QfqNS = QfqNS || {}; * * @param {string} tabId HTML id of the element having `nav` and `nav-tabs` classes * @constructor + * + * @name QfqNS.BSTabs */ n.BSTabs = function (tabId) { this.tabId = tabId; diff --git a/javascript/src/Element/Checkbox.js b/javascript/src/Element/Checkbox.js index ebd8c23f3bc7989c169a09d26aeb7313c401985c..5853c723a3733c42521260925f2da0f3ed69387f 100644 --- a/javascript/src/Element/Checkbox.js +++ b/javascript/src/Element/Checkbox.js @@ -4,7 +4,17 @@ /* @depend FormGroup.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { @@ -15,24 +25,43 @@ QfqNS.Element = QfqNS.Element || {}; * * @param $element * @constructor + * @name QfqNS.Element.Checkbox */ function Checkbox($element) { n.FormGroup.call(this, $element); - if (!this.isType("checkbox")) { + var type = "checkbox"; + + if (!this.isType(type)) { throw new Error("$element is not of type 'checkbox'"); } + + // We allow one Form Group to have several checkboxes. Therefore, we have to remember which checkbox was + // selected if possible. + if ($element.length === 1 && $element.attr('type') === type) { + this.$singleElement = $element; + } else { + this.$singleElement = null; + } } Checkbox.prototype = Object.create(n.FormGroup.prototype); Checkbox.prototype.constructor = Checkbox; Checkbox.prototype.setValue = function (val) { - this.$element.prop('checked', val); + if (this.$singleElement) { + this.$singleElement.prop('checked', val); + } else { + this.$element.prop('checked', val); + } }; Checkbox.prototype.getValue = function () { - return this.$element.prop('checked'); + if (this.$singleElement) { + return this.$singleElement.prop('checked'); + } else { + return this.$element.prop('checked'); + } }; n.Checkbox = Checkbox; diff --git a/javascript/src/Element/FormGroup.js b/javascript/src/Element/FormGroup.js index 9fce210f00a33cb468c7b97779caa7b8d375dfcb..f8f58e1baf16bac45ef9885b8bdb757fdb514688 100644 --- a/javascript/src/Element/FormGroup.js +++ b/javascript/src/Element/FormGroup.js @@ -2,7 +2,17 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { @@ -18,6 +28,7 @@ QfqNS.Element = QfqNS.Element || {}; * * * @constructor + * @name QfqNS.Element.FormGroup */ n.FormGroup = function ($enclosedElement) { if (!$enclosedElement || $enclosedElement.length === 0) { @@ -30,6 +41,13 @@ QfqNS.Element = QfqNS.Element || {}; this.$helpBlock = this.$formGroup.find(".help-block"); }; + /** + * Test if the Form Group is of the given type + * + * @param {string} type type name + * @returns {boolean} true if the Form Group is of the given type. False otherwise + * @protected + */ n.FormGroup.prototype.isType = function (type) { var lowerCaseType = type.toLowerCase(); var isOfType = true; @@ -140,15 +158,4 @@ QfqNS.Element = QfqNS.Element || {}; this.$element.prop('required', required); }; - - /** - * Read Only click handler. - * - * Since the readonly attribute does not work as expected on certain input types, emulate read only - */ - n.FormGroup.prototype.readOnlyHandler = function () { - return false; - }; - - })(QfqNS.Element); \ No newline at end of file diff --git a/javascript/src/Element/NameSpaceFunctions.js b/javascript/src/Element/NameSpaceFunctions.js index fcbd7085c47901879bf6414bb825e06a7b5a1427..2a4265a5d969025ace55c413fa9abd1eb824cd1a 100644 --- a/javascript/src/Element/NameSpaceFunctions.js +++ b/javascript/src/Element/NameSpaceFunctions.js @@ -4,12 +4,28 @@ /* global $ */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { 'use strict'; + /** + * + * @param name + * @returns {*} + * @function QfqNS.Element.getElement + */ n.getElement = function (name) { var $element = $('[name="' + name + '"]:not([type="hidden"])'); if ($element.length === 0) { diff --git a/javascript/src/Element/Radio.js b/javascript/src/Element/Radio.js index d449bf9281b66c6b34495b3e56bea8b3dd1472f6..5bc1cf50ed6f2cb80263c421f23c754d339c2978 100644 --- a/javascript/src/Element/Radio.js +++ b/javascript/src/Element/Radio.js @@ -2,7 +2,17 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { @@ -13,6 +23,7 @@ QfqNS.Element = QfqNS.Element || {}; * * @param $element * @constructor + * @name QfqNS.Element.Radio */ function Radio($element) { n.FormGroup.call(this, $element); diff --git a/javascript/src/Element/Select.js b/javascript/src/Element/Select.js index e1968cb0041c90f5904d5c73c1219b467b30125d..88218d801352247131c8a65abd987a44b1b63e5c 100644 --- a/javascript/src/Element/Select.js +++ b/javascript/src/Element/Select.js @@ -3,7 +3,17 @@ */ /* global $ */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { @@ -14,6 +24,7 @@ QfqNS.Element = QfqNS.Element || {}; * * @param $element * @constructor + * @name QfqNS.Element.Select */ function Select($element) { n.FormGroup.call(this, $element); diff --git a/javascript/src/Element/Textual.js b/javascript/src/Element/Textual.js index ed972c2340ff1b50f1a5f730b119de1aebfd46bc..475b632807606a597edd7f95f6fa3528f73ed9eb 100644 --- a/javascript/src/Element/Textual.js +++ b/javascript/src/Element/Textual.js @@ -2,7 +2,17 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { @@ -13,6 +23,7 @@ QfqNS.Element = QfqNS.Element || {}; * * @param $element * @constructor + * @name QfqNS.Element.Textual */ function Textual($element) { n.FormGroup.call(this, $element); diff --git a/javascript/src/Element/data.js b/javascript/src/Element/data.js index b86efcd68f5712328dcd05fcec15d94961f32209..1e48ea02da02c3ed66d929e3fdc0fcc5144b05eb 100644 --- a/javascript/src/Element/data.js +++ b/javascript/src/Element/data.js @@ -2,7 +2,17 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq.Element Namespace + * + * @namespace QfqNS.Element + */ QfqNS.Element = QfqNS.Element || {}; (function (n) { diff --git a/javascript/src/FileDelete.js b/javascript/src/FileDelete.js index 40324659593e1a7cd41618aaebe2fd3551470d3c..2a16c0c24ebe41ab38c7b4e490ea25b4056898c0 100644 --- a/javascript/src/FileDelete.js +++ b/javascript/src/FileDelete.js @@ -6,11 +6,24 @@ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @param formSelector + * @param targetUrl + * @constructor + * + * @name QfqNS.FileDelete + */ n.FileDelete = function (formSelector, targetUrl) { this.formSelector = formSelector; this.targetUrl = targetUrl; @@ -27,7 +40,15 @@ var QfqNS = QfqNS || {}; n.FileDelete.prototype.buttonClicked = function (event) { event.preventDefault(); - var alert = new n.Alert("Do you want to delete the file?", "warning", "okcancel"); + var alert = new n.Alert({ + message: "Do you want to delete the file?", + type: "warning", + modal: true, + buttons: [ + {label: "OK", eventName: "ok"}, + {label: "Cancel", eventName: "cancel", focus: true} + ] + }); alert.on('alert.ok', function () { this.performFileDelete(event); }.bind(this)); diff --git a/javascript/src/FileUpload.js b/javascript/src/FileUpload.js index e0327ffbf4aab7e61ad7afe847010042cdc6a4cd..04ac85e0e1e5c1599a9b9d195b2f43c6034322d5 100644 --- a/javascript/src/FileUpload.js +++ b/javascript/src/FileUpload.js @@ -7,12 +7,23 @@ /* @depend QfqEvents.js */ - +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @param formSelector + * @param targetUrl + * @constructor + * @name QfqNS.FileUpload + */ n.FileUpload = function (formSelector, targetUrl) { this.formSelector = formSelector; this.targetUrl = targetUrl; diff --git a/javascript/src/Form.js b/javascript/src/Form.js index 7a7fb399562ba991b15d723de207e50042bd8166..2f840f41b0147e45b0ffa4c4b36913cab4de28eb 100644 --- a/javascript/src/Form.js +++ b/javascript/src/Form.js @@ -6,11 +6,22 @@ /* global EventEmitter */ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @param formId + * @constructor + * @name QfqNS.Form + */ n.Form = function (formId) { this.formId = formId; this.eventEmitter = new EventEmitter(); @@ -26,6 +37,10 @@ var QfqNS = QfqNS || {}; // On <input> elements, we specifically bind this events, in order to update the formChanged property // immediately, not only after loosing focus. Same goes for <textarea> this.$form.find("input, textarea").on("input paste", this.changeHandler.bind(this)); + + this.$form.on('submit', function (event) { + event.preventDefault(); + }); }; n.Form.prototype.on = n.EventEmitter.onMixin; @@ -39,20 +54,24 @@ var QfqNS = QfqNS || {}; n.Form.prototype.changeHandler = function (event) { this.formChanged = true; this.eventEmitter.emitEvent('form.changed', n.EventEmitter.makePayload(this, null)); - // REMOVE: this.userFormChangeHandlers.call(this); }; n.Form.prototype.getFormChanged = function () { return this.formChanged; }; + n.Form.prototype.markChanged = function () { + this.changeHandler(null); + }; + n.Form.prototype.resetFormChanged = function () { this.formChanged = false; this.eventEmitter.emitEvent('form.reset', n.EventEmitter.makePayload(this, null)); - // REMOVE: this.userResetHandlers.call(this); + }; n.Form.prototype.submitTo = function (to) { + this.eventEmitter.emitEvent('form.submit.before', n.EventEmitter.makePayload(this, null)); $.post(to, this.$form.serialize()) .done(this.ajaxSuccessHandler.bind(this)) .fail(this.submitFailureHandler.bind(this)); @@ -97,12 +116,38 @@ var QfqNS = QfqNS || {}; * @returns {*} */ n.Form.prototype.validate = function () { + this.eventEmitter.emitEvent('form.validation.before', n.EventEmitter.makePayload(this, null)); // uncommented because bootstrap-validator sets novalidate="true" on form. //if (this.$form.attr('novalidate')) { // return true; //} - return document.forms[this.formId].checkValidity(); + var result = document.forms[this.formId].checkValidity(); + + this.eventEmitter.emitEvent('form.validation.after', n.EventEmitter.makePayload(this, {validationResult: result})); + + return result; + }; + + /** + * @public + */ + n.Form.prototype.getFirstNonValidElement = function () { + var index; + var elementNumber = document.forms[this.formId].length; + + for (index = 0; index < elementNumber; index++) { + var element = document.forms[this.formId][index]; + if (!element.willValidate) { + continue; + } + + if (!element.checkValidity()) { + return element; + } + } + + return null; }; })(QfqNS); diff --git a/javascript/src/Helper/FunctionList.js b/javascript/src/Helper/FunctionList.js deleted file mode 100644 index f0ccd20cb3907766636b03d0f5fee5123f8cb188..0000000000000000000000000000000000000000 --- a/javascript/src/Helper/FunctionList.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> - */ - -/* global $ */ - -var QfqNS = QfqNS || {}; -QfqNS.Helper = QfqNS.Helper || {}; - -(function (n) { - 'use strict'; - - /** - * @deprecated - * @constructor - */ - n.FunctionList = function () { - this.functions = []; - }; - - n.FunctionList.prototype.addFunction = function (func, thisObj) { - if (typeof func !== "function") { - throw new TypeError("Expected function"); - } - - if (thisObj) { - this.functions.push([func, thisObj]); - } else { - this.functions.push(func); - } - }; - - n.FunctionList.prototype.call = function () { - var _arguments = arguments; - this.functions.forEach(function (func) { - if (typeof func === "function") { - func.apply(undefined, _arguments); - } else { - // func is an array [ Function, thisObj ] - func[0].apply(func[1], _arguments); - } - }); - }; -})(QfqNS.Helper); \ No newline at end of file diff --git a/javascript/src/Helper/NameSpaceFunctions.js b/javascript/src/Helper/NameSpaceFunctions.js index 8aa20845d2b6ffbd2ca3a6644c057c231b9dcc3b..81fb51231443e5e016f6fadaec0cf3d88ffd704c 100644 --- a/javascript/src/Helper/NameSpaceFunctions.js +++ b/javascript/src/Helper/NameSpaceFunctions.js @@ -4,18 +4,43 @@ /* global $ */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; +/** + * Qfq Helper Namespace + * + * @namespace QfqNS.Helper + */ QfqNS.Helper = QfqNS.Helper || {}; (function (n) { 'use strict'; + /** + * + * @param jqHXR + * @param textStatus + * @param errorThrown + * + * @function QfqNS.Helper.showAjaxError + */ n.showAjaxError = function (jqHXR, textStatus, errorThrown) { var alert = new QfqNS.Alert("Error:<br> " + errorThrown, "error"); alert.show(); }; + /** + * + * @param string + * @returns {*} + * + * @function QfqNS.Helper.stringBool + */ n.stringToBool = function (string) { if (typeof string !== "string") { return string; diff --git a/javascript/src/Helper/jqxComboBox.js b/javascript/src/Helper/jqxComboBox.js new file mode 100644 index 0000000000000000000000000000000000000000..fdde8dc11e69d2951a553643a307f544821f9ffb --- /dev/null +++ b/javascript/src/Helper/jqxComboBox.js @@ -0,0 +1,77 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ + + +/* global $ */ + +/** + * Qfq Namespace + * + * @namespace QfqNS + */ +var QfqNS = QfqNS || {}; +/** + * Qfq Helper Namespace + * + * @namespace QfqNS.Helper + */ +QfqNS.Helper = QfqNS.Helper || {}; + +(function (n) { + 'use strict'; + + /** + * + * @function + * @name QfqNS.Helper.jqxComboBox + */ + var jqxComboBox = function () { + var index; + var $containers = $("div.jqw-combobox"); + + $containers.each(function (index, object) { + (function ($container) { + var controlName = $container.data('control-name'); + if (!controlName) { + QfqNS.Log.error("jqwComboBox container does not have a 'data-control-name' attribute."); + return; + } + + var sourceId = controlName + "_source"; + var $sourceScript = $('#' + sourceId); + if ($sourceScript.length !== 1) { + QfqNS.Log.error("Unable to find data for jqwComboBox using id '" + sourceId + "'"); + return; + } + + var source = JSON.parse($sourceScript.text()); + + + $container.jqxComboBox({ + source: source, + displayMember: "text", + valueMember: "value" + }); + + // Our code creates a hidden input element for each jqxwidget as sibling of the widget. We do this, + // because jqxwidget don't create named input elements, and thus the value would not be sent to the + // server using a Plain Old Form submission (even if performed by an ajax request). + var $hiddenInput = $("<input>") + .attr('type', 'hidden') + .attr('name', controlName); + + $container.after($hiddenInput); + + $hiddenInput.val($container.jqxComboBox('val')); + + $container.on('change', function (event) { + $hiddenInput.val(event.args.item.value); + }); + })($(object)); + }); + }; + + n.jqxComboBox = jqxComboBox; + +})(QfqNS.Helper); \ No newline at end of file diff --git a/javascript/src/Helper/jqxDateTimeInput.js b/javascript/src/Helper/jqxDateTimeInput.js new file mode 100644 index 0000000000000000000000000000000000000000..d19c655ab5ff389ce952a38bfebd0b7d77e8b2d6 --- /dev/null +++ b/javascript/src/Helper/jqxDateTimeInput.js @@ -0,0 +1,107 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ + + +/* global $ */ + +/** + * Qfq Namespace + * + * @namespace QfqNS + */ +var QfqNS = QfqNS || {}; +/** + * Qfq Helper Namespace + * + * @namespace QfqNS.Helper + */ +QfqNS.Helper = QfqNS.Helper || {}; + +(function (n) { + 'use strict'; + + /** + * Initializes jqxDateTimeInput widgets. + * + * It configures all `<div>`s having a class `jqw-datetimepicker` as jqxDateTimeInput. + * + * Given the HTML snippet + * + * ... + * <div class="jqw-datetimepicker" data-control-name="datetimepicker" ></div> + * ... + * + * it will create a hidden input sibling element of the `<div>` where the selected date is stored for plain old + * form submission, thus rendering the above snippet effectively to + * + * ... + * <div class="jqw-datetimepicker" data-control-name="datetimepicker" ></div> + * <input type="hidden" name="datetimepicker"> + * ... + * + * The jqxDateTimeInput can be configured using following `data` attributes + * + * * `data-control-name`: Mandatory attribute. Hold the name of the input element. + * * `data-format-string': Optional Format string as required by jqxDateTimeInput. See also + * {@link http://www.jqwidgets.com/jquery-widgets-documentation/documentation/jqxdatetimeinput/jquery-datetimeinput-api.htm}. + * Default: "F". + * * `data-show-time-button`: Boolean value `true` or `false`, indicating whether or not a time picker will be + * displayed. + * + * @function + * @name QfqNS.Helper.jqxDateTimeInput + */ + var jqxDateTimeInput = function () { + var index; + var $containers = $("div.jqw-datetimepicker"); + + $containers.each(function (index, object) { + (function ($container) { + var controlName = $container.data('control-name'); + if (!controlName) { + QfqNS.Log.error("jqwDateTimePicker does not have a 'data-control-name' attribute."); + return; + } + + var formatString = $container.data('format-string'); + if (!formatString) { + formatString = "F"; + } + + var showTimeButton = $container.data('show-time-button'); + if (showTimeButton === undefined) { + showTimeButton = false; + } + + var jqxDateTimeInputConfig = { + formatString: formatString, + showTimeButton: showTimeButton, + theme: "bootstrap" + }; + + $container.jqxDateTimeInput(jqxDateTimeInputConfig); + + // Our code creates a hidden input element for each jqxwidget as sibling of the widget. We do this, + // because jqxwidget don't create named input elements, and thus the value would not be sent to the + // server using a Plain Old Form submission (even if performed by an ajax request). + var $hiddenInput = $("<input>") + .attr('type', 'hidden') + .attr('name', controlName); + + $container.after($hiddenInput); + + $hiddenInput.val($container.jqxDateTimeInput('value').toISOString()); + + $container.on('valueChanged', function (event) { + $hiddenInput.val(event.args.date.toISOString()); + }); + })($(object)); + }); + + }; + + n.jqxDateTimeInput = jqxDateTimeInput; + + +})(QfqNS.Helper); \ No newline at end of file diff --git a/javascript/src/Helper/jqxEditor.js b/javascript/src/Helper/jqxEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..35b7ade535131324ee23a40b6a62af8fc5dbbadc --- /dev/null +++ b/javascript/src/Helper/jqxEditor.js @@ -0,0 +1,43 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ + +/* global $ */ + +/** + * Qfq Namespace + * + * @namespace QfqNS + */ +var QfqNS = QfqNS || {}; +/** + * Qfq Helper Namespace + * + * @namespace QfqNS.Helper + */ +QfqNS.Helper = QfqNS.Helper || {}; + +(function (n) { + 'use strict'; + + /** + + * @function + * @name QfqNS.Helper.jqxEditor + */ + var jqxEditor = function () { + var index; + var $containers = $("textarea.jqw-editor"); + + $containers.each(function (index, object) { + (function ($container) { + $container.jqxEditor(); + })($(object)); + }); + + }; + + n.jqxEditor = jqxEditor; + + +})(QfqNS.Helper); \ No newline at end of file diff --git a/javascript/src/Helper/tinyMCE.js b/javascript/src/Helper/tinyMCE.js new file mode 100644 index 0000000000000000000000000000000000000000..39032a35bd5c1e79c75ae483c7b7e89ba2acb84f --- /dev/null +++ b/javascript/src/Helper/tinyMCE.js @@ -0,0 +1,82 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ +/* global console */ +/* global tinymce */ +/* global $ */ + +/** + * Qfq Namespace + * + * @namespace QfqNS + */ +var QfqNS = QfqNS || {}; +/** + * Qfq Helper Namespace + * + * @namespace QfqNS.Helper + */ +QfqNS.Helper = QfqNS.Helper || {}; + +(function (n) { + 'use strict'; + + /** + + * @function + * @name QfqNS.Helper.jqxEditor + */ + var tinyMce = function () { + if (typeof tinymce === 'undefined') { + return; + } + + $(".qfq-tinymce").each( + function () { + var config = {}; + var $this = $(this); + var tinyMCEId = $this.attr('id'); + if (!tinyMCEId) { + QfqNS.Log.warning("TinyMCE container does not have an id attribute. Ignoring."); + return; + } + + var configData = $this.data('config'); + if (configData) { + if (configData instanceof Object) { + // jQuery takes care of decoding data-config to JavaScript object. + config = configData; + } else { + QfqNS.Log.warning("'data-config' is invalid: " + configData); + } + } + + config.selector = "#" + QfqNS.escapeJqueryIdSelector(tinyMCEId); + config.setup = function (editor) { + editor.on('Change', function (e) { + // Ensure the associated form is notified of changes in editor. + QfqNS.Log.debug('Editor was changed'); + var eventTarget = e.target; + var $parentForm = $(eventTarget.formElement); + $parentForm.trigger("change"); + + }); + }; + + tinymce.init(config); + } + ); + }; + + tinyMce.prepareSave = function () { + if (typeof tinymce === 'undefined') { + return; + } + + tinymce.triggerSave(); + }; + + n.tinyMce = tinyMce; + + +})(QfqNS.Helper); \ No newline at end of file diff --git a/javascript/src/Log.js b/javascript/src/Log.js index 3361f5985c779124cf5001b3f2c858c256c539df..e2eca6313bed2d84731538eb3d502e8e55948a19 100644 --- a/javascript/src/Log.js +++ b/javascript/src/Log.js @@ -4,10 +4,22 @@ /* global console */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + + /** + * + * @type {{level: number, message: Function, debug: Function, warning: Function, error: Function}} + * + * @name QfqNS.Log + */ n.Log = { level: 3, message: function (msg) { diff --git a/javascript/src/PageState.js b/javascript/src/PageState.js index 52f371a38e09fafd4c4c8a955c41ea087f8d43de..7ba934694efff7280d49cdf67ed70c16d4851acd 100644 --- a/javascript/src/PageState.js +++ b/javascript/src/PageState.js @@ -5,12 +5,22 @@ /* @depend QfqEvents.js */ /* global EventEmitter */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @constructor + * @name QfqNS.PageState + */ n.PageState = function () { this.pageState = location.hash.slice(1); this.data = null; diff --git a/javascript/src/PageTitle.js b/javascript/src/PageTitle.js index e5e4567b8d707ad52dc562e7039999838d3800ca..fb36529bcd654a28945c296992aa428c10697cbd 100644 --- a/javascript/src/PageTitle.js +++ b/javascript/src/PageTitle.js @@ -2,11 +2,22 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @type {{set: Function, get: Function, setSubTitle: Function}} + * + * @name QfqNS.PageTitle + */ n.PageTitle = { set: function (title) { document.title = title; diff --git a/javascript/src/QfqEvents.js b/javascript/src/QfqEvents.js index 26be83cc0395ee412c55c7f7b2a4ae27542f7cd3..992023162dedb4b59643dee12011a3c400b081a8 100644 --- a/javascript/src/QfqEvents.js +++ b/javascript/src/QfqEvents.js @@ -5,11 +5,21 @@ /* global EventEmitter */ /* global $ */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @type {{makePayload: Function, onMixin: Function}} + * + */ n.EventEmitter = { makePayload: function (target, data, additionalArgs) { return [$.extend({}, diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js index 4a36138c2f89ec0f1e0ab7d5c065f7958c388c4f..d0f2cdf0fdb9e63f8a215d0af7921ccc66cd99c1 100644 --- a/javascript/src/QfqForm.js +++ b/javascript/src/QfqForm.js @@ -6,6 +6,11 @@ /* global EventEmitter */ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { @@ -21,6 +26,8 @@ var QfqNS = QfqNS || {}; * @param dataRefreshUrl {string} url where to fetch new element values from * @param fileUploadTo {string} url used for file uploads * @constructor + * + * @name QfqNS.QfqForm */ n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo, fileDeleteUrl) { this.formId = formId; @@ -51,6 +58,7 @@ var QfqNS = QfqNS || {}; this.getDeleteButton().click(this.handleDeleteClick.bind(this)); this.setupFormUpdateHandler(); + this.setupEnterKeyHandler(); this.fileUploader = new n.FileUpload('#' + this.formId, this.fileUploadTo); this.fileUploader.on('fileupload.started', this.startUploadHandler); @@ -63,7 +71,7 @@ var QfqNS = QfqNS || {}; this.fileUploader.on('fileupload.ended', this.endUploadHandler); this.fileDeleter = new n.FileDelete("#" + this.formId, this.fileDeleteUrl); - this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler); + this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler.bind(this)); this.fileDeleter.on('filedelete.delete.failed', function (obj) { @@ -73,10 +81,37 @@ var QfqNS = QfqNS || {}; var configurationData = this.readElementConfigurationData(); this.applyElementConfiguration(configurationData); + + // Initialize jqxDateTimeInput elements. + n.Helper.jqxDateTimeInput(); + // Initialize jqxComboBox elements. + n.Helper.jqxComboBox(); + // Deprecated + //n.Helper.jqxEditor(); + n.Helper.tinyMce(); + this.form.on('form.submit.before', n.Helper.tinyMce.prepareSave); + this.form.on('form.validation.before', n.Helper.tinyMce.prepareSave); }; n.QfqForm.prototype.on = n.EventEmitter.onMixin; + /** + * @private + */ + n.QfqForm.prototype.setupEnterKeyHandler = function () { + $("input").keyup(function (event) { + if (event.which === 13) { + if (this.form.formChanged) { + this.lastButtonPress = "save&close"; + n.Log.debug("save&close click"); + this.submit(); + } + event.preventDefault(); + } + }.bind(this)); + }; + + /** * * @private @@ -154,6 +189,8 @@ var QfqNS = QfqNS || {}; $inputFile.removeClass('hidden'); $inputFile.val(""); + + this.form.markChanged(); }; /** @@ -262,9 +299,6 @@ var QfqNS = QfqNS || {}; n.QfqForm.prototype.handleSaveClick = function () { this.lastButtonPress = "save"; n.Log.debug("save click"); - // First, remove all validation states, in case a previous submit has set a validation state, thus we're not - // stockpiling them. - this.clearAllValidationStates(); this.submit(); }; @@ -274,12 +308,23 @@ var QfqNS = QfqNS || {}; n.QfqForm.prototype.handleCloseClick = function () { this.lastButtonPress = "close"; if (this.form.getFormChanged()) { - var alert = new n.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave"); + var alert = new n.Alert({ + message: "You have unsaved changes. Do you want to save first?", + type: "warning", + modal: true, + buttons: [ + {label: "Yes", eventName: "yes"}, + {label: "No", eventName: "no", focus: true}, + {label: "Cancel", eventName: "cancel"} + ] + }); var that = this; - alert.on('alert.save', function () { + alert.on('alert.yes', function () { that.submit(); }); - alert.on('alert.ok', function () { + alert.on('alert.no', function () { + that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null)); + window.history.back(); }); alert.show(); @@ -291,11 +336,26 @@ var QfqNS = QfqNS || {}; n.QfqForm.prototype.submit = function () { if (this.form.validate() !== true) { + var element = this.form.getFirstNonValidElement(); + if (element.hasAttribute('name') && this.bsTabs) { + var tabId = this.bsTabs.getContainingTabIdForFormControl(element.getAttribute('name')); + if (tabId) { + this.bsTabs.activateTab(tabId); + } + } + + // Since we might have switched the tab, re-validate to hightlight errors + this.form.$form.validator('update'); this.form.$form.validator('validate'); + var alert = new n.Alert("Form is incomplete.", "warning"); alert.show(); return; } + + // First, remove all validation states, in case a previous submit has set a validation state, thus we're not + // stockpiling them. + this.clearAllValidationStates(); this.form.submitTo(this.submitTo); }; @@ -307,15 +367,27 @@ var QfqNS = QfqNS || {}; this.lastButtonPress = "new"; if (this.form.getFormChanged()) { - var alert = new n.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave"); - var that = this; - alert.on('alert.save', function () { - that.submit(); + var alert = new n.Alert({ + message: "You have unsaved changes. Do you want to save first?", + type: "warning", + modal: true, + buttons: + [ + {label: "Yes", eventName: "yes", focus: true}, + {label: "No", eventName: "no"}, + {label: "Cancel", eventName: "cancel"} + ] }); - alert.on('alert.ok', function () { + var that = this; + alert.on('alert.no', function () { + that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null)); + var anchorTarget = that.getNewButtonTarget(); window.location = anchorTarget; }); + alert.on('alert.yes', function () { + that.submit(); + }); alert.show(); } else { var anchorTarget = this.getNewButtonTarget(); @@ -330,7 +402,15 @@ var QfqNS = QfqNS || {}; n.QfqForm.prototype.handleDeleteClick = function () { this.lastButtonPress = "delete"; n.Log.debug("delete click"); - var alert = new n.Alert("Do you really want to delete the record?", "warning", "yesno"); + var alert = new n.Alert({ + message: "Do you really want to delete the record?", + type: "warning", + modal: true, + buttons: [ + {label: "Yes", eventName: "ok"}, + {label: "No", eventName: "cancel", focus: true} + ] + }); var that = this; alert.on('alert.ok', function () { $.post(that.deleteUrl) @@ -391,7 +471,7 @@ var QfqNS = QfqNS || {}; if (data.redirect === "url" || data['redirect-url']) { window.location = data['redirect-url']; - return; + } }; @@ -532,6 +612,9 @@ var QfqNS = QfqNS || {}; form.resetFormChanged(); switch (this.lastButtonPress) { + case 'save&close': + window.history.back(); + break; case 'save': if (data.message) { var alert = new n.Alert(data.message); @@ -620,11 +703,19 @@ var QfqNS = QfqNS || {}; $formGroup.removeClass("has-warning"); $formGroup.removeClass("has-error"); $formGroup.removeClass("has-success"); + $formGroup.removeClass("has-danger"); }; n.QfqForm.prototype.clearAllValidationStates = function () { - $('.has-warning,.has-error,.has-success').removeClass("has-warning has-error has-success"); + // Reset any messages/states added by bootstrap-validator. + this.form.$form.validator('reset'); + + // Reset any states added by a call to QfqForm#setValidationState() + $('.has-warning,.has-error,.has-success,.has-danger').removeClass("has-warning has-error has-success" + + " has-danger"); + + // Remove all messages received from server upon form submit. $('[data-qfq=validation-message]').remove(); }; @@ -733,4 +824,11 @@ var QfqNS = QfqNS || {}; return $('#' + this.formId + ' input[name=s]').val(); }; + /** + * @public + */ + n.QfqForm.prototype.isFormChanged = function () { + return this.form.formChanged; + }; + })(QfqNS); \ No newline at end of file diff --git a/javascript/src/QfqPage.js b/javascript/src/QfqPage.js index 66dc8295dbee132a9cb05617be2291938964e996..340fb0f081b30e9a50e6aaed1e14083f08f0e512 100644 --- a/javascript/src/QfqPage.js +++ b/javascript/src/QfqPage.js @@ -6,11 +6,23 @@ /* global console */ /* @depend QfqEvents.js */ +/** + * Qfq Namespace + * + * @namespace QfqNS + */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; + /** + * + * @param settings + * @constructor + * + * @name QfqNS.QfqPage + */ n.QfqPage = function (settings) { this.settings = $.extend( { @@ -25,6 +37,8 @@ var QfqNS = QfqNS || {}; }, settings ); + this.intentionalClose = false; + try { this.bsTabs = new n.BSTabs(this.settings.tabsId); @@ -53,12 +67,31 @@ var QfqNS = QfqNS || {}; this.settings.fileDeleteUrl); this.qfqForm.setBsTabs(this.bsTabs); this.qfqForm.on('qfqform.destroyed', this.destroyFormHandler.bind(this)); + + var that = this; + this.qfqForm.on('qfqform.close-intentional', function () { + that.intentionalClose = true; + }); + + window.addEventListener("beforeunload", this.beforeUnloadHandler.bind(this)); } catch (e) { n.Log.error(e.message); this.qfqForm = null; } }; + /** + * @private + */ + n.QfqPage.prototype.beforeUnloadHandler = function (event) { + var message = "\0/"; + if (this.qfqForm.isFormChanged() && !this.intentionalClose) { + + event.returnValue = message; + return message; + } + }; + /** * @private */ diff --git a/javascript/src/QfqRecordList.js b/javascript/src/QfqRecordList.js index 18ea78a6f8a20ee521b5c269c7488829cf6cbe03..b69d0601dacb473870d7e99e2725410c3dc5c451 100644 --- a/javascript/src/QfqRecordList.js +++ b/javascript/src/QfqRecordList.js @@ -6,9 +6,21 @@ var QfqNS = QfqNS || {}; +/** + * Qfq Namespace + * + * @namespace QfqNS + */ (function (n) { 'use strict'; + /** + * + * @param deleteUrl + * @constructor + * + * @name QfqNS.QfqRecordList + */ n.QfqRecordList = function (deleteUrl) { this.deleteUrl = deleteUrl; this.deleteButtonClass = 'record-delete'; @@ -26,7 +38,7 @@ var QfqNS = QfqNS || {}; }; n.QfqRecordList.prototype.handleDeleteButtonClick = function (event) { - var $eventTarget = $(event.target); + var $eventTarget = $(event.delegateTarget); var $recordElement = this.getRecordElement(event.target); if ($recordElement.length !== 1) { @@ -40,7 +52,15 @@ var QfqNS = QfqNS || {}; } - var alert = new n.Alert("Do you really want to delete the record?", "warning", "yesno"); + var alert = new n.Alert({ + message: "Do you really want to delete the record?", + type: "warning", + modal: true, + buttons: [ + {label: "Yes", eventName: "ok"}, + {label: "No", eventName: "cancel", focus: true} + ] + }); var that = this; alert.on('alert.ok', function () { $.post(that.deleteUrl + "?s=" + sip) diff --git a/javascript/src/Utils.js b/javascript/src/Utils.js new file mode 100644 index 0000000000000000000000000000000000000000..9f253860e313b179adb8507bb1ad88f63f354145 --- /dev/null +++ b/javascript/src/Utils.js @@ -0,0 +1,19 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ +/* global $ */ + +/** + * Qfq Namespace + * + * @namespace QfqNS + */ +var QfqNS = QfqNS || {}; + +(function (n) { + 'use strict'; + + n.escapeJqueryIdSelector = function (idSelector) { + return idSelector.replace(/(:)/, "\\$1"); + }; +})(QfqNS); \ No newline at end of file diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index 5b11996e43ee35f3d60441995e7be58014500508..661644c7c95c7089d642d02f547b0752aec84916 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -61,3 +61,57 @@ i.@{spinner_class} { font-weight: normal; } } + +.qfq-table-50 { + min-width: 50%; + width: auto; +} + +.qfq-table-80 { + min-width: 80%; + width: auto; +} + +.qfq-form-pill { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +.qfq-form-body { + padding-top: 5px; + padding-bottom: 5px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +/* adjust BS padding of input elements: center */ +.form-group { + padding-top: 5px; + padding-bottom: 5px; + margin-bottom: 0; +} + +.qfq-color-white { + background-color: #ffffff; +} + +.qfq-color-grey-1 { + background-color: #dedede; +} + +.qfq-color-grey-2 { + background-color: #ededed; +} + +.qfq-color-blue-1 { + background-color: #d2dded; +} + +.qfq-color-blue-2 { + background-color: #e2eeff; +} + +.qfq-form-right .qfq-label { + text-align: right; + padding-right: 0; +} diff --git a/mockup/alert.html b/mockup/alert.html index 7e640c77a6adebcc612518d9b05bc7875e799718..0fc3b52abc63e59a912276e635d80bdf503bc5d4 100644 --- a/mockup/alert.html +++ b/mockup/alert.html @@ -13,7 +13,24 @@ <button id="showalert2">Show Alert 2</button> <button id="showalert3">Show Alert 3 primed</button> <button id="showmanyalert1">Show Many Alert 1</button> +<button id="showmodalalert1">Show Modal Alert 1</button> +<a id="alertinlink" onclick=" +var alert = new QfqNS.Alert('Text being displayed', 'info', [ + { label: 'OK', eventName: 'ok' }, + { label: 'Cancel', eventName: 'cancel'} +]); +alert.on('alert.ok', function() { + window.location = $('#alertinlink').attr('href'); +}); + +alert.show(); +return false; +" href="http://www.google.ch">Link alert</a> + +<div style="margin-top: 20ex"> + <button onclick="alert('Button clicked')">Modal test button</button> +</div> <script src="../js/jquery.min.js"></script> <script src="../js/bootstrap.min.js"></script> @@ -23,11 +40,12 @@ <script> $(function () { $('#showalert1').click(function () { - var alert = new QfqNS.Alert("This is alert 1", 'error', 'okcancel'); - alert.addOkButtonHandler(function () { + var alert = new QfqNS.Alert("This is alert 1", 'error', [{label: "OK", eventName: "ok", focus: true}, + {label: "Cancel", eventName: "cancel"}]); + alert.on('alert.ok', function () { console.log("OK button alert 1"); }); - alert.addCancelButtonHandler(function () { + alert.on('alert.cancel', function () { console.log("Cancel button alert 1"); }); alert.show(); @@ -35,18 +53,14 @@ $('#showalert2').click(function () { var alert = new QfqNS.Alert("This is alert 2", 'warning'); - alert.addOkButtonHandler(function () { - console.log("OK button alert 2"); - }); - alert.addCancelButtonHandler(function () { - console.log("Cancel button alert 2"); - }); alert.show(); }); $('#showalert3').click(function () { - var alert = new QfqNS.Alert("This is alert 3. Disappears after 3 secs."); - alert.timeout = 3000; + var alert = new QfqNS.Alert({ + message: "This is alert 3. Disappears after 3 secs.", + timeout: 3000 + }); alert.show(); }); @@ -60,6 +74,27 @@ alert = new QfqNS.Alert("This is alert 3", "error"); alert.show(); }); + + $('#showmodalalert1').click(function () { + var alert = new QfqNS.Alert( + { + message: "This is modal alert 1", + type: 'error', + buttons: [ + {label: "OK", eventName: "ok", focus: true}, + {label: "Cancel", eventName: "cancel"} + ], + modal: true + } + ); + alert.on('alert.ok', function () { + console.log("OK button modal alert 1"); + }); + alert.on('alert.cancel', function () { + console.log("Cancel button modal alert 1"); + }); + alert.show(); + }); }); </script> </body> diff --git a/mockup/api/uploadhandler_error.php b/mockup/api/uploadhandler_error.php index 02ef17ea838ab9002c61613e4a872e097b05e367..2b5d0b4cca03c605ff8566a19fcb3a4a0c29c108 100644 --- a/mockup/api/uploadhandler_error.php +++ b/mockup/api/uploadhandler_error.php @@ -7,6 +7,6 @@ header("Content-Type: text/json"); echo json_encode([ 'status' => "error", - 'message' + 'message' => "error uploading file" ]); diff --git a/mockup/browserhistory/page1.html b/mockup/browserhistory/page1.html new file mode 100644 index 0000000000000000000000000000000000000000..577222aa452390d57cbdadca0b3ae60642965ddc --- /dev/null +++ b/mockup/browserhistory/page1.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>PAGE 1</title> +</head> +<body> +<h1>Page 1</h1> + +<button type="button" id="page2button">Page 2</button> + +<script src="../../js/jquery.min.js"></script> +<script> + $(function () { + $("#page2button").click(function (event) { + window.location = "page2.html"; + }); + }); +</script> + +</body> +</html> \ No newline at end of file diff --git a/mockup/browserhistory/page2.html b/mockup/browserhistory/page2.html new file mode 100644 index 0000000000000000000000000000000000000000..9d3de8d35f3005b20ff4a2de0627775abd72639e --- /dev/null +++ b/mockup/browserhistory/page2.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>PAGE 2</title> +</head> +<body> +<h1>Page 2</h1> + +<button type="button" id="page3button">Page 3</button> +<button type="button" id="closebutton">Close</button> + +<script src="../../js/jquery.min.js"></script> +<script> + $(function () { + + $("#page3button").click(function (event) { + window.history.replaceState({pageName: "Page 2"}, null, null); + window.location = "page3.html"; + }); + + $("#closebutton").click(function (event) { + window.history.back(); + }); + + if (history.state) { + console.log("State: " + history.state.pageName); + } else { + console.log("No State"); + } + }); +</script> + +</body> +</html> \ No newline at end of file diff --git a/mockup/browserhistory/page3.html b/mockup/browserhistory/page3.html new file mode 100644 index 0000000000000000000000000000000000000000..382079173af92239c1768e684ce4a05cd8914adb --- /dev/null +++ b/mockup/browserhistory/page3.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>PAGE 3</title> +</head> +<body> +<h1>Page 3</h1> + +<button id="page3button">Page 3</button> +<button id="closebutton">Close</button> + +<script src="../../js/jquery.min.js"></script> +<script> + $(function () { + window.history.state + window.history.replaceState(null, null, '#fragid'); + $("#page3button").click(function (event) { + window.location = "page3.html"; + }); + + $("#closebutton").click(function (event) { + //window.history.back(); + window.location = "page2.html"; + }); + }); +</script> + + +</body> +</html> \ No newline at end of file diff --git a/mockup/chart.html b/mockup/chart.html new file mode 100644 index 0000000000000000000000000000000000000000..6f9f67767245663ca37b6258d608c27465533da5 --- /dev/null +++ b/mockup/chart.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Chart</title> +</head> +<body> + +<div style="height: 1024px; width: 640px;"> + <h1>Runtime Comparison</h1> + <canvas id="barchart" width="1240" height="640"></canvas> + <canvas id="linechart" width="1240" height="640"></canvas> + <canvas id="piechart" width="1240" height="640"></canvas> +</div> + +<script src="../js/jquery.min.js"></script> +<script src="../js/Chart.min.js"></script> + +<script> + $(function () { + var ctx = document.getElementById("barchart"); + var barChart = new Chart(ctx, { + type: 'bar', + data: { + labels: ['C++', 'Java 8', 'JavaScript', 'Fortran'], + datasets: [ + { + label: "3,636 Forests", + data: [ + 0.01, 0.24, 0.04, 0.01 + ], + backgroundColor: "steelblue" + }, + { + label: "517,236 Forests", + data: [0.05, 0.62, 1.49, 1.95, 0.10,], + backgroundColor: "#91C3DC" + }, + { + label: "2,600,836 Forests", + data: [0.35, 1.46, 8.70, 10.54, 0.96,], + backgroundColor: "#87907D" + + }, { + label: "7,254,436 Forests", + data: [1.20, 3.58, 25.59, 29.94, 4.58,], + backgroundColor: "#AAB6A2" + } + + ] + } + }); + + ctx = document.getElementById("linechart"); + var lineChart = new Chart(ctx, { + type: 'line', + data: { + labels: ['C++', 'Java 8', 'JavaScript', 'Fortran'], + datasets: [ + { + label: "3,636 Forests", + data: [ + 0.01, 0.24, 0.04, 0.01 + ], + backgroundColor: "steelblue" + }, + { + label: "517,236 Forests", + data: [0.05, 0.62, 1.49, 1.95, 0.10,], + backgroundColor: "#91C3DC" + }, + { + label: "2,600,836 Forests", + data: [0.35, 1.46, 8.70, 10.54, 0.96,], + backgroundColor: "#87907D" + + }, { + label: "7,254,436 Forests", + data: [1.20, 3.58, 25.59, 29.94, 4.58,], + backgroundColor: "#AAB6A2" + } + + ] + } + }); + + ctx = document.getElementById("piechart"); + var pieChart = new Chart(ctx, { + type: 'pie', + data: { + labels: ['C++', 'Java 8', 'JavaScript', 'Fortran'], + datasets: [ + { + data: [1.20, 3.58, 25.59, 29.94, 4.58,], + backgroundColor: [ + "steelblue", + "#91C3DC", + "#87907D", + "#AAB6A2" + ] + } + ] + } + }); + }); +</script> + +</body> +</html> \ No newline at end of file diff --git a/mockup/elementconfiguration.html b/mockup/elementconfiguration.html index bf357b1d45bfc7e3ee8034e6a3e661ea213369b0..dd517d4a05376484b35c154ef314036add095cf1 100644 --- a/mockup/elementconfiguration.html +++ b/mockup/elementconfiguration.html @@ -172,6 +172,38 @@ </div> </div> + <div class="form-group"> + <div class="col-md-2"> + <b class="control-label"> + Checkbox 3 test + </b> + </div> + + + <div class="col-md-6"> + <div class="checkbox"> + <label> + <input name='checkbox3_1' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_2' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_3' type="checkbox" value="reminder_value"> + </label> + + </div> + </div> + <div class="col-md-4"> + + </div> + </div> </form> </div> diff --git a/mockup/form.html b/mockup/form.html index fa97ecaa93b67f5f1c8b6a80fdb18fde83aac078..116f098d039c8326673d653fc538f935729f3e1f 100644 --- a/mockup/form.html +++ b/mockup/form.html @@ -31,6 +31,16 @@ <input type="checkbox" name="checkboxinput" value="3"> </label> + <fieldset name="userinfo"> + <legend>User information</legend> + <label for="name">Name</label> + <input type="text" name="name" id="name" size="40"> + <label for="address">Address</label> + <input type="text" name="address" id="address" size="40"> + <label for="phone">Phone</label> + <input type="text" name="phone" id="phone" size="40"> + </fieldset> + </form> <script src="../js/jquery.min.js"></script> diff --git a/mockup/personmock.html b/mockup/personmock.html index bd1c8ce44bee839e189a7703c5fb4bc0522733fc..e20f5ab033d8c7411f8a0904d8f6b1578b3d809e 100644 --- a/mockup/personmock.html +++ b/mockup/personmock.html @@ -5,11 +5,13 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <link rel="stylesheet" href="../css/bootstrap.min.css"> <link rel="stylesheet" href="../css/bootstrap-theme.min.css"> <link rel="stylesheet" href="../css/jqx.base.css"> - <link rel="stylesheet" href="../css/jqx.darkblue.css"> + <link rel="stylesheet" href="../css/jqx.bootstrap.css"> <link rel="stylesheet" href="../css/qfq-bs.css"> + <title>Person Form Mockup</title> @@ -99,7 +101,7 @@ class="glyphicon glyphicon-trash"></span></button> </div> <div class="btn-group" role="group"> - <a id="form-new-button" href="http://www.wikipedia.org" class="btn btn-default navbar-btn"><span + <a id="form-new-button" href="personmock.html?s=badcaffe1" class="btn btn-default navbar-btn"><span class="glyphicon glyphicon-plus"></span></a> </div> </div> @@ -170,7 +172,8 @@ <label for="personHandle" class="control-label">Kurzform</label> </div> <div class="col-md-6"> - <input id="personHandle" name="personhandle" type="text" class="form-control"> + <input id="personHandle" name="personhandle" type="text" class="form-control" + data-required="no"> </div> <div class="col-md-4"> @@ -202,12 +205,12 @@ <div class="col-md-6"> <div class="radio"> <label> - <input type="radio" name="gender">male + <input type="radio" name="gender" required>male </label> </div> <div class="radio"> <label> - <input type="radio" name="gender">female + <input type="radio" name="gender" required>female </label> </div> </div> @@ -215,6 +218,8 @@ <p class="help-block"></p> </div> </div> + <!-- This must be outside the form-group. --> + <input name="gender" value="" type="hidden"> <div class="form-group"> @@ -223,7 +228,34 @@ <label for="personGeburtstag" class="control-label">Geburtstag</label> </div> <div class="col-md-6"> - <input id="personGeburtstag" type="text" class="form-control" name="geburtstag"> + <div id="personGeburtstag" class="jqw-datetimepicker" + data-control-name="personGeburtstag" data-format-string="dd.MM.yyyy HH:mm" + data-show-time-button="true"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="anotherDate" class="control-label">Another Date</label> + </div> + <div class="col-md-6"> + <div id="anotherDate" class="jqw-datetimepicker" + data-control-name="anotherDate" data-format-string="dd.MM.yyyy" + data-show-time-button="false"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="combobox1" class="control-label">Combobox 1</label> + </div> + <div class="col-md-6"> + <div id="combobox1" class="jqw-combobox" + data-control-name="combobox1"></div> </div> </div> @@ -307,8 +339,24 @@ <!--subrecord: bilder zur person--> + <div class="form-group"> + + <div class="col-md-2"> + <label for="notes-editor" class="control-label">Notes</label> + </div> + <div class="col-md-6"> + <textarea id="notes-editor" class="form-control qfq-tinymce" + data-control-name="notesField" name="notesField" required></textarea> + + <div class="help-block with-errors"></div> + </div> + + + </div> + </div> + <!--pill: Funktion--> <div role="tabpanel" class="tab-pane" id="funktion"> <!--Persönliche Funktion (subrecord)--> @@ -655,7 +703,7 @@ <div class="col-md-6"> <div class="uploaded-file "><span class="uploaded-file-name">john.doe.png</span> <button class="delete-file" data-sip="filedeletesip" - name="trash-picture:1"><span + name="trash-picture:1" type="button"><span class="glyphicon glyphicon-trash"></span></button> </div> <input id="fileupload" name="picture:1" disabled="disabled" class="hidden" type="file" @@ -792,17 +840,31 @@ </div> </form> + <a href="https://www.google.ch">away</a> </div> +<script type="application/jqw-combobox-source" id="combobox1_source"> + [ + { "value": 1, "text": "item 1" }, + { "value": 2, "text": "item 2" }, + { "value": 3, "text": "item 3" } + ] + +</script> + <script src="../js/jquery.min.js"></script> <script src="../js/bootstrap.min.js"></script> + <script src="../js/validator.min.js"></script> + <script src="../js/jqx-all.js"></script> +<script src="../js/globalize.js"></script> +<script src="../js/tinymce.min.js"></script> <script src="../js/EventEmitter.min.js"></script> <script src="../js/qfq.debug.js"></script> + <script type="text/javascript"> $(function () { - $("#subrecord_adresse").jqxDataTable( { columns: [ @@ -1050,6 +1112,7 @@ }); QfqNS.Log.level = 0; + }); </script> </body> diff --git a/mockup/recordlist.html b/mockup/recordlist.html index a42b6a3850da1212dc75edc9e1a387bb07446359..49749342ea26ab61f515cb2ed4d0fbf365bf89be 100644 --- a/mockup/recordlist.html +++ b/mockup/recordlist.html @@ -25,11 +25,16 @@ <th></th> </tr> + <!-- + + Please note: The text is enclosed in <span/> tags to simulate the real world deployment. Buttons are mostly used + with <span/> tags to display icons on them. Seldom do they only contain text. + --> <tr class="record"> <td>1</td> <td>a</td> <td> - <button data-sip="1" class="record-delete">Delete</button> + <button data-sip="1" class="record-delete"><span>Delete</span></button> </td> </tr> @@ -37,7 +42,7 @@ <td>2</td> <td>b</td> <td> - <button data-sip="2" class="record-delete">Delete</button> + <button data-sip="2" class="record-delete"><span>Delete</span></button> </td> </tr> @@ -45,7 +50,7 @@ <td>3</td> <td>c</td> <td> - <button data-sip="3" class="record-delete">Delete</button> + <button data-sip="3" class="record-delete"><span>Delete</span></button> </td> </tr> @@ -53,7 +58,7 @@ <td>4</td> <td>d</td> <td> - <button data-sip="4" class="record-delete">Delete</button> + <button data-sip="4" class="record-delete"><span>Delete</span></button> </td> </tr> </table> @@ -67,7 +72,7 @@ $(function () { 'use strict'; - var qfqRecordList = new QfqNS.QfqRecordList('api/' + $("#deleteTo").val()) + var qfqRecordList = new QfqNS.QfqRecordList('api/' + $("#deleteTo").val()); $("#deleteTo").on("change", function (evt) { qfqRecordList.deleteUrl = 'api/' + $(evt.target).val(); diff --git a/mockup/richtexteditor.html b/mockup/richtexteditor.html new file mode 100644 index 0000000000000000000000000000000000000000..1c08c978b33a19580cfa96ee4bec97a324994bee --- /dev/null +++ b/mockup/richtexteditor.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + + <link rel="stylesheet" href="../css/bootstrap.min.css"> + <link rel="stylesheet" href="../css/bootstrap-theme.min.css"> + <link rel="stylesheet" href="../css/qfq-bs.css"> + <title>Rich Text Editor Mock</title> + + +</head> +<body> +<div class="container-fluid"> + <div class="row"> + <div class="col-md-2 "> + <div class="btn-toolbar pull-right" role="toolbar"> + <div class="btn-group" role="group"> + <button id="save-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-ok"></span></button> + <button id="close-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-remove"></span></button> + </div> + <div class="btn-group" role="group"> + <button id="delete-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-trash"></span></button> + </div> + <div class="btn-group" role="group"> + <a id="form-new-button" href="personmock.html?s=badcaffe1" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-plus"></span></a> + </div> + </div> + </div> + + </div> + <div class="row hidden-xs"> + <div class="col-md-12"> + <h1>Title with a long text</h1> + </div> + </div> + + + <form id="myForm" class="form-horizontal" action="richtexteditor.html" method="post"> + + <div class="form-group"> + <div class="col-md-2"> + <label for="name" class="control-label">Name</label> + </div> + + <div class="col-md-6"> + <input id="name" name="name" type="text" class="form-control"> + </div> + + </div> + + <div class="form-group"> + <div class="col-md-2"> + <label for="text" class="control-label">Rich Text Editor (default)</label> + </div> + + <div class="col-md-6"> + <textarea id="text" class="qfq-tinymce" name="rte">Input + </textarea> + </div> + </div> + + <div class="form-group"> + <div class="col-md-2"> + <label for="text:2" class="control-label">Rich Text Editor (plugins, autofocus, no statusbar)</label> + </div> + + <div class="col-md-6"> + <textarea id="text:2" class="qfq-tinymce" name="rte" + data-config="{ "plugins": "advlist autolink link image lists charmap print preview", "auto_focus": "text:2", "statusbar": false }">Input + </textarea> + </div> + </div> + + <div class="form-group"> + <div class="col-md-2"> + <label for="text:3" class="control-label">Rich Text Editor (min_height, max_height)</label> + </div> + + <div class="col-md-6"> + <textarea id="text:3" class="qfq-tinymce" name="rte" + data-config="{ "min_height": "200", "max_height": "400" }">Input + </textarea> + </div> + + </div> + + <button type="submit">Do</button> + + </form> +</div> + +<script src="../js/jquery.min.js"></script> +<script src="../js/bootstrap.min.js"></script> +<script src="../js/EventEmitter.min.js"></script> +<script src="../js/qfq.debug.js"></script> +<script src="../js/tinymce.min.js"></script> +<script type="text/javascript"> + $(function () { + var qfqPage = new QfqNS.QfqPage({ + tabsId: 'myTabs', + formId: 'myForm', + submitTo: 'richtexteditor.html', + deleteUrl: 'api/' + $("#deleteUrl").val(), + fileUploadTo: 'api/' + $("#uploadTo").val(), + fileDeleteUrl: 'api/' + $("#fileDeleteUrl").val(), + }); + }); +</script> +</body> +</html> \ No newline at end of file diff --git a/mockup/second.html b/mockup/second.html index c875ecfabfc78647eb37e62e0701c5e8fc6e0c18..0cf48c45de7dfba3023f27769157c96833f6a96e 100644 --- a/mockup/second.html +++ b/mockup/second.html @@ -1161,6 +1161,8 @@ </div> </div> + <button id="save-button" type="button" class="btn btn-default">Submit</button> + </div> <script src="../js/jquery.min.js"></script> diff --git a/tests/jasmine/MIT.LICENSE b/tests/jasmine/acceptance/MIT.LICENSE similarity index 100% rename from tests/jasmine/MIT.LICENSE rename to tests/jasmine/acceptance/MIT.LICENSE diff --git a/tests/jasmine/acceptance/SpecRunner.html b/tests/jasmine/acceptance/SpecRunner.html new file mode 100644 index 0000000000000000000000000000000000000000..c4c35829481ca29709674513c6edda49dae80b95 --- /dev/null +++ b/tests/jasmine/acceptance/SpecRunner.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Jasmine Spec Runner v2.4.1</title> + + <link rel="shortcut icon" type="image/png" href="../lib/jasmine-2.4.1/jasmine_favicon.png"> + <link rel="stylesheet" href="../lib/jasmine-2.4.1/jasmine.css"> + + <script src="../lib/jasmine-2.4.1/jasmine.js"></script> + <script src="../lib/jasmine-2.4.1/jasmine-html.js"></script> + <script src="../lib/jasmine-2.4.1/boot.js"></script> + + <script src="../helper/mock-ajax.js"></script> + + <script src="../../../js/jquery.min.js"></script> + <script src="../../../js/bootstrap.min.js"></script> + <script src="../../../js/EventEmitter.min.js"></script> + + <!-- include source files here... --> + <script src="../../../js/qfq.debug.js"></script> + + <!-- include spec files here... --> + <script src="spec/FileUploadSpec.js"></script> + +</head> + + +<body> + +<div class="col-md-2 "> + <div class="btn-toolbar pull-right" role="toolbar"> + <div class="btn-group" role="group"> + <button id="save-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-ok"></span></button> + <button id="close-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-remove"></span></button> + </div> + <div class="btn-group" role="group"> + <button id="delete-button" type="button" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-trash"></span></button> + </div> + <div class="btn-group" role="group"> + <a id="form-new-button" href="personmock.html?s=badcaffe1" class="btn btn-default navbar-btn"><span + class="glyphicon glyphicon-plus"></span></a> + </div> + </div> +</div> + +<form id="myForm" class="form-horizontal" data-toggle="validator"> + +</form> + +<script type="text/template" id="fileupload_new"> + <input type="hidden" name="s" value="badcaffee1234"> + + <div class="form-group"> + <div class="col-md-2"> + <label for="fileupload" class="control-label">File upload</label> + </div> + <div class="col-md-6"> + <div class="col-md-6"> + <div class="uploaded-file hidden"><span class="uploaded-file-name">john.doe.png</span> + <button class="delete-file" data-sip="filedeletesip" + name="trash-picture:1"><span + class="glyphicon glyphicon-trash"></span></button> + </div> + <input id="fileupload" name="picture:1" class="hidden" type="file" + data-sip="fileuploadsip"> + + <div class="help-block with-errors"></div> + </div> + </div> + </div> +</script> + +</body> +</html> diff --git a/tests/jasmine/acceptance/spec/FileUploadSpec.js b/tests/jasmine/acceptance/spec/FileUploadSpec.js new file mode 100644 index 0000000000000000000000000000000000000000..6423d519e9990c88064e80fd5d957d154b5032ac --- /dev/null +++ b/tests/jasmine/acceptance/spec/FileUploadSpec.js @@ -0,0 +1,45 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ + +/* global describe */ +/* global it */ +/* global expect */ +/* global QfqNS */ +/* global beforeAll */ +/* global beforeEach */ +/* global jasmine */ +/* global $ */ + +describe("File Upload Acceptance", function () { + 'use strict'; + + function prepareForm(id) { + var $form; + var $template = $("#" + id); + + $form = $('#myForm').empty(); + $form.append($template.text()); + + } + + it("handles no previous uploaded file present", function () { + prepareForm("fileupload_new"); + + jasmine.Ajax.install(); + + var bla = $('#fileupload')[0]; + var qfqForm = new QfqNS.QfqForm("myForm", "", "", "", "", ""); + + $('#fileupload').trigger("change"); + + jasmine.Ajax.requests.mostRecent().respondWith({ + "status": 200, + "contentType": "application/json", + "responseText": "{ \"status\": \"success\" }" + }); + + jasmine.Ajax.uninstall(); + }); + +}); \ No newline at end of file diff --git a/tests/jasmine/spec/HelperFunctionListSpec.js b/tests/jasmine/spec/HelperFunctionListSpec.js deleted file mode 100644 index b7c0a76d9872ec5be79e86ce28a09e860b59948c..0000000000000000000000000000000000000000 --- a/tests/jasmine/spec/HelperFunctionListSpec.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> - */ - -/* global describe */ -/* global it */ -/* global expect */ -/* global QfqNS */ -/* global beforeAll */ -/* global beforeEach */ -/* global jasmine */ - -describe("Helper FunctionList", function () { - 'use strict'; - - var functionList; - - beforeEach(function () { - functionList = new QfqNS.Helper.FunctionList(); - }); - - it("should call one function properly", function () { - var spy = jasmine.createSpy('SingleFunction'); - - functionList.addFunction(spy); - functionList.call("1", 2, {}); - - expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith("1", 2, {}); - - }); - - it("should call several functions properly", function () { - var spy1 = jasmine.createSpy('Function1'); - var spy2 = jasmine.createSpy('Function2'); - var spy3 = jasmine.createSpy('Function3'); - - functionList.addFunction(spy1); - functionList.addFunction(spy2); - functionList.addFunction(spy3); - - functionList.call("1", 2, {}); - - expect(spy1).toHaveBeenCalled(); - expect(spy1).toHaveBeenCalledWith("1", 2, {}); - - expect(spy2).toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledWith("1", 2, {}); - - expect(spy3).toHaveBeenCalled(); - expect(spy3).toHaveBeenCalledWith("1", 2, {}); - - }); - - it("should pass proper object as `this'", function () { - var obj = { - val: 0, - f: function (a) { - this.val = a; - } - }; - - var spy1 = jasmine.createSpy('Function1'); - - functionList.addFunction(obj.f, obj); - functionList.addFunction(spy1); - - functionList.call(2); - - expect(spy1).toHaveBeenCalledWith(2); - expect(obj.val).toBe(2); - - }); - - it("should not allow adding anything besides FunctionS", function () { - expect(function () { - functionList.add('bla'); - }).toThrowError(TypeError); - }); - } -); \ No newline at end of file diff --git a/tests/jasmine/unit/MIT.LICENSE b/tests/jasmine/unit/MIT.LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..aff8ed47a1a4b86c9ccec24d2a76b4d99a78a707 --- /dev/null +++ b/tests/jasmine/unit/MIT.LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008-2014 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/jasmine/SpecRunner.html b/tests/jasmine/unit/SpecRunner.html similarity index 71% rename from tests/jasmine/SpecRunner.html rename to tests/jasmine/unit/SpecRunner.html index f3e9e0fd67058aeb5a2191b5d27e31a5447fa8f4..e631e73e6f82cd8b96ed1de20fb01f45df8053ea 100644 --- a/tests/jasmine/SpecRunner.html +++ b/tests/jasmine/unit/SpecRunner.html @@ -4,25 +4,28 @@ <meta charset="utf-8"> <title>Jasmine Spec Runner v2.4.1</title> - <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.4.1/jasmine_favicon.png"> - <link rel="stylesheet" href="lib/jasmine-2.4.1/jasmine.css"> - <link rel="stylesheet" href="../../css/bootstrap.min.css"> - <link rel="stylesheet" href="../../css/bootstrap-theme.min.css"> - - <script src="lib/jasmine-2.4.1/jasmine.js"></script> - <script src="lib/jasmine-2.4.1/jasmine-html.js"></script> - <script src="lib/jasmine-2.4.1/boot.js"></script> - <script src="helper/mock-ajax.js"></script> - - <script src="../../js/jquery.min.js"></script> - <script src="../../js/bootstrap.min.js"></script> - <script src="../../js/EventEmitter.min.js"></script> + <link rel="shortcut icon" type="image/png" href="../lib/jasmine-2.4.1/jasmine_favicon.png"> + <link rel="stylesheet" href="../lib/jasmine-2.4.1/jasmine.css"> + <link rel="stylesheet" href="../../../css/bootstrap.min.css"> + <link rel="stylesheet" href="../../../css/bootstrap-theme.min.css"> + <link rel="stylesheet" href="../../../css/jqx.base.css"> + <link rel="stylesheet" href="../../../css/jqx.bootstrap.css"> + <link rel="stylesheet" href="../../../css/qfq-bs.css"> + + <script src="../lib/jasmine-2.4.1/jasmine.js"></script> + <script src="../lib/jasmine-2.4.1/jasmine-html.js"></script> + <script src="../lib/jasmine-2.4.1/boot.js"></script> + <script src="../helper/mock-ajax.js"></script> + + <script src="../../../js/jquery.min.js"></script> + <script src="../../../js/bootstrap.min.js"></script> + <script src="../../../js/jqx-all.js"></script> + <script src="../../../js/EventEmitter.min.js"></script> <!-- include source files here... --> - <script src="../../js/qfq.debug.js"></script> + <script src="../../../js/qfq.debug.js"></script> <!-- include spec files here... --> - <script src="spec/HelperFunctionListSpec.js"></script> <script src="spec/ElementFormGroupSpec.js"></script> <script src="spec/ElementTextualSpec.js"></script> <script src="spec/ElementRadioSpec.js"></script> @@ -32,12 +35,16 @@ <script src="spec/BSTabsSpec.js"></script> <script src="spec/PageTitleSpec.js"></script> <script src="spec/FormSpec.js"></script> + <script src="spec/HelperJqxDateTimeInputSpec.js"></script> </head> <body> -<section class="container-fluid"> +<section class="container-fluid" id="jasmineTestTemplateTarget"> +</section> + +<script type="jasmine/test-template" id="jasmineTestTemplate"> <header class="page-header"> <h1>Keep Track of Navigation State</h1> </header> @@ -165,7 +172,7 @@ </div> <div class="col-md-6"> - <input id="name" type="text" class="form-control"> + <input id="name" type="text" class="form-control" name="name"> </div> </div> @@ -176,7 +183,7 @@ </div> <div class="col-md-6"> - <input id="firstname" type="text" class="form-control"> + <input id="firstname" type="text" class="form-control" name="firstname"> </div> </div> @@ -185,7 +192,7 @@ <label for="nameShort" class="control-label">Vorname Kurz</label> </div> <div class="col-md-6"> - <input id="nameShort" type="text" name="personHandle" class="form-control"> + <input id="nameShort" type="text" name="nameShort" class="form-control"> </div> <div class="col-md-4"> @@ -198,7 +205,7 @@ <label for="personHandle" class="control-label">Kurzform</label> </div> <div class="col-md-6"> - <input id="personHandle" type="text" class="form-control"> + <input id="personHandle" type="text" class="form-control" name="personHandle"> </div> <div class="col-md-4"> @@ -213,6 +220,7 @@ </div> <!-- a disturber hidden element --> <input type="hidden" name="personTitle"> + <div class="col-md-6"> <select id="personTitle" class="form-control" name="personTitle"> <option>none</option> @@ -227,7 +235,7 @@ <label for="selectTest2" class="control-label">Titel</label> </div> <div class="col-md-6"> - <select id="selectTest2" class="form-control"> + <select id="selectTest2" class="form-control" name="selectTest2"> <option value="1">a</option> <option value="2" selected>b</option> <option value="3">c</option> @@ -244,9 +252,11 @@ <!-- a disturber hidden element --> <input type="hidden" name="gender"> + <div class="col-md-6"> <!-- a disturber hidden element --> <input type="hidden" name="gender"> + <div class="radio"> <label> <input type="radio" name="gender" value="male">male @@ -317,10 +327,79 @@ </div> </div> - </form> + <div class="form-group"> + <div class="col-md-2"> + <b class="control-label"> + Checkbox 3 test + </b> + </div> -</section> + <div class="col-md-6"> + <div class="checkbox"> + <label> + <input name='checkbox3_1' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_2' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_3' type="checkbox" value="reminder_value"> + </label> + + </div> + </div> + <div class="col-md-4"> + + </div> + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="datetimepicker1" class="control-label">datetimepicker1</label> + </div> + <div class="col-md-6"> + <div id="datetimepicker1" class="jqw-datetimepicker" + data-control-name="datetimepicker" data-format-string="dd.MM.yyyy HH:mm" + data-show-time-button="true"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="anotherDate" class="control-label">Another Date</label> + </div> + <div class="col-md-6"> + <div id="anotherDate" class="jqw-datetimepicker" + data-control-name="anotherDate" data-format-string="dd.MM.yyyy" + data-show-time-button="false"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="combobox1" class="control-label">Combobox 1</label> + </div> + <div class="col-md-6"> + <div id="combobox1" class="jqw-combobox" + data-control-name="combobox1"></div> + </div> + + </div> + + </form> +</script> </body> </html> diff --git a/tests/jasmine/SpecRunner.tmpl b/tests/jasmine/unit/SpecRunner.tmpl similarity index 76% rename from tests/jasmine/SpecRunner.tmpl rename to tests/jasmine/unit/SpecRunner.tmpl index 918414eabb40ff5902982a9e775cf415798bd6bc..8fa1cb189f86f327cea0404587d3cd4b0a1a822f 100644 --- a/tests/jasmine/SpecRunner.tmpl +++ b/tests/jasmine/unit/SpecRunner.tmpl @@ -10,8 +10,11 @@ </head> <body> +<section class="container-fluid" id="jasmineTestTemplateTarget"> +</section> + -<section class="container-fluid"> +<script type="jasmine/test-template" id="jasmineTestTemplate"> <header class="page-header"> <h1>Keep Track of Navigation State</h1> </header> @@ -139,7 +142,7 @@ </div> <div class="col-md-6"> - <input id="name" type="text" class="form-control"> + <input id="name" type="text" class="form-control" name="name"> </div> </div> @@ -150,7 +153,7 @@ </div> <div class="col-md-6"> - <input id="firstname" type="text" class="form-control"> + <input id="firstname" type="text" class="form-control" name="firstname"> </div> </div> @@ -159,7 +162,7 @@ <label for="nameShort" class="control-label">Vorname Kurz</label> </div> <div class="col-md-6"> - <input id="nameShort" type="text" name="personHandle" class="form-control"> + <input id="nameShort" type="text" name="nameShort" class="form-control"> </div> <div class="col-md-4"> @@ -172,7 +175,7 @@ <label for="personHandle" class="control-label">Kurzform</label> </div> <div class="col-md-6"> - <input id="personHandle" type="text" class="form-control"> + <input id="personHandle" type="text" class="form-control" name="personHandle"> </div> <div class="col-md-4"> @@ -187,6 +190,7 @@ </div> <!-- a disturber hidden element --> <input type="hidden" name="personTitle"> + <div class="col-md-6"> <select id="personTitle" class="form-control" name="personTitle"> <option>none</option> @@ -201,7 +205,7 @@ <label for="selectTest2" class="control-label">Titel</label> </div> <div class="col-md-6"> - <select id="selectTest2" class="form-control"> + <select id="selectTest2" class="form-control" name="selectTest2"> <option value="1">a</option> <option value="2" selected>b</option> <option value="3">c</option> @@ -218,7 +222,11 @@ <!-- a disturber hidden element --> <input type="hidden" name="gender"> + <div class="col-md-6"> + <!-- a disturber hidden element --> + <input type="hidden" name="gender"> + <div class="radio"> <label> <input type="radio" name="gender" value="male">male @@ -254,6 +262,7 @@ </b> </div> + <div class="col-md-6"> <div class="checkbox"> <label> @@ -274,6 +283,7 @@ </b> </div> + <div class="col-md-6"> <div class="checkbox"> <label> @@ -287,9 +297,79 @@ </div> </div> - </form> + <div class="form-group"> + <div class="col-md-2"> + <b class="control-label"> + Checkbox 3 test + </b> + </div> -</section> + + <div class="col-md-6"> + <div class="checkbox"> + <label> + <input name='checkbox3_1' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_2' type="checkbox" value="reminder_value"> + </label> + + </div> + <div class="checkbox"> + <label> + <input name='checkbox3_3' type="checkbox" value="reminder_value"> + </label> + + </div> + </div> + <div class="col-md-4"> + + </div> + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="datetimepicker1" class="control-label">datetimepicker1</label> + </div> + <div class="col-md-6"> + <div id="datetimepicker1" class="jqw-datetimepicker" + data-control-name="datetimepicker" data-format-string="dd.MM.yyyy HH:mm" + data-show-time-button="true"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="anotherDate" class="control-label">Another Date</label> + </div> + <div class="col-md-6"> + <div id="anotherDate" class="jqw-datetimepicker" + data-control-name="anotherDate" data-format-string="dd.MM.yyyy" + data-show-time-button="false"></div> + </div> + + </div> + + <div class="form-group"> + + <div class="col-md-2"> + <label for="combobox1" class="control-label">Combobox 1</label> + </div> + <div class="col-md-6"> + <div id="combobox1" class="jqw-combobox" + data-control-name="combobox1"></div> + </div> + + </div> + + </form> +</script> <% with (scripts) { %> <% [].concat(polyfills, jasmine, boot, vendor, helpers, src, specs,reporters).forEach(function(script){ %> <script src="<%= script %>"></script> diff --git a/tests/jasmine/spec/BSTabsSpec.js b/tests/jasmine/unit/spec/BSTabsSpec.js similarity index 91% rename from tests/jasmine/spec/BSTabsSpec.js rename to tests/jasmine/unit/spec/BSTabsSpec.js index 1193ebb409d7594892194b78b9ea2e10f8fd3b66..5a28d397d5ec8e5ae7fd9e5004bffe8598adbaeb 100644 --- a/tests/jasmine/spec/BSTabsSpec.js +++ b/tests/jasmine/unit/spec/BSTabsSpec.js @@ -2,11 +2,13 @@ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ +/* global $ */ /* global describe */ /* global it */ /* global expect */ /* global QfqNS */ /* global beforeAll */ +/* global beforeEach */ /* global jasmine */ describe("Bootstrap Tabs", function () { @@ -14,7 +16,10 @@ describe("Bootstrap Tabs", function () { var bsTabs; - beforeAll(function () { + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); bsTabs = new QfqNS.BSTabs("qfqTabs"); }); @@ -63,7 +68,7 @@ describe("Bootstrap Tabs", function () { var showHandlerSpy = jasmine.createSpy('showhandler'); bsTabs.on('bootstrap.tab.shown', showHandlerSpy); - bsTabs.activateTab('tab1'); + bsTabs.activateTab('tab2'); expect(showHandlerSpy).toHaveBeenCalled(); }); diff --git a/tests/jasmine/spec/ElementCheckboxSpec.js b/tests/jasmine/unit/spec/ElementCheckboxSpec.js similarity index 57% rename from tests/jasmine/spec/ElementCheckboxSpec.js rename to tests/jasmine/unit/spec/ElementCheckboxSpec.js index 9bc9241c1978a9b2e57a78cd0927e61927041892..72fa2c16d1a40af2066e7a230cbd6d3d7f2b94c6 100644 --- a/tests/jasmine/spec/ElementCheckboxSpec.js +++ b/tests/jasmine/unit/spec/ElementCheckboxSpec.js @@ -16,17 +16,16 @@ describe("Element Checkbox", function () { var reminderCheckbox, checkbox2; var $reminderCheckbox, $checkbox2; - beforeAll(function () { + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); $reminderCheckbox = $('input[name=reminder]'); $checkbox2 = $('input[name=checkbox2]'); reminderCheckbox = new QfqNS.Element.Checkbox($reminderCheckbox); checkbox2 = new QfqNS.Element.Checkbox($checkbox2); }); - beforeEach(function () { - $('#myForm')[0].reset(); - }); - it('should get the initial checked value', function () { expect(reminderCheckbox.getValue()).toBe(false); expect(checkbox2.getValue()).toBe(true); @@ -47,4 +46,31 @@ describe("Element Checkbox", function () { expect($reminderCheckbox.prop('checked')).toBe(true); expect($checkbox2.prop('checked')).toBe(false); }); + + it('should properly handle several checkboxes in one Form Group', function () { + var $checkbox3_1 = $("[name='checkbox3_1']"); + var $checkbox3_2 = $("[name='checkbox3_2']"); + var $checkbox3_3 = $("[name='checkbox3_3']"); + + var checkbox3_1 = new QfqNS.Element.Checkbox($checkbox3_1); + checkbox3_1.setValue(true); + + expect(checkbox3_1.getValue()).toBe(true); + + expect($checkbox3_1.prop('checked')).toBe(true); + expect($checkbox3_2.prop('checked')).toBe(false); + expect($checkbox3_3.prop('checked')).toBe(false); + + $('#myForm')[0].reset(); + + var checkbox3_2 = new QfqNS.Element.Checkbox($checkbox3_2); + checkbox3_2.setValue(true); + + expect(checkbox3_2.getValue()).toBe(true); + + expect($checkbox3_2.prop('checked')).toBe(true); + expect($checkbox3_1.prop('checked')).toBe(false); + expect($checkbox3_3.prop('checked')).toBe(false); + + }); }); \ No newline at end of file diff --git a/tests/jasmine/spec/ElementFormGroupSpec.js b/tests/jasmine/unit/spec/ElementFormGroupSpec.js similarity index 93% rename from tests/jasmine/spec/ElementFormGroupSpec.js rename to tests/jasmine/unit/spec/ElementFormGroupSpec.js index 32d762307fb6a7123abcfa0b1bd2ff9a23cf09a3..ea6958fce6d563851e98050cea033c3b4f969a85 100644 --- a/tests/jasmine/spec/ElementFormGroupSpec.js +++ b/tests/jasmine/unit/spec/ElementFormGroupSpec.js @@ -16,7 +16,10 @@ describe("Element FormGroup", function () { var personHandle; var personGender; - beforeAll(function () { + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); personGeburtstag = new QfqNS.Element.FormGroup($("input#personGeburtstag")); personHandle = new QfqNS.Element.FormGroup($("input#personHandle")); personGender = new QfqNS.Element.FormGroup($("input[name='gender']")); @@ -97,7 +100,7 @@ describe("Element FormGroup", function () { expect(personGender.hasHelpBlock()).toBe(true); }); - it("should properly disable the item", function () { + xit("should properly disable the item", function () { personGender.setEnabled(false); }); }); \ No newline at end of file diff --git a/tests/jasmine/spec/ElementRadioSpec.js b/tests/jasmine/unit/spec/ElementRadioSpec.js similarity index 89% rename from tests/jasmine/spec/ElementRadioSpec.js rename to tests/jasmine/unit/spec/ElementRadioSpec.js index b7d4a303879f5182945151c8c9c2bd912e801fcd..bfadc6c2109bf0fa29d8922550b3867daceb2e80 100644 --- a/tests/jasmine/spec/ElementRadioSpec.js +++ b/tests/jasmine/unit/spec/ElementRadioSpec.js @@ -6,6 +6,7 @@ /* global expect */ /* global QfqNS */ /* global beforeAll */ +/* global beforeEach */ /* global jasmine */ /* global $ */ @@ -14,7 +15,10 @@ describe("Element Radio", function () { var $radioInput; var radioInput; - beforeAll(function () { + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); $radioInput = $("input[name='gender']"); radioInput = new QfqNS.Element.Radio($radioInput); }); diff --git a/tests/jasmine/spec/ElementSelectSpec.js b/tests/jasmine/unit/spec/ElementSelectSpec.js similarity index 96% rename from tests/jasmine/spec/ElementSelectSpec.js rename to tests/jasmine/unit/spec/ElementSelectSpec.js index 639f51b1e1068065a4c7aa95464b3a8a13915092..a6ee0343a233f123081ea5325efc1daffdfb00f2 100644 --- a/tests/jasmine/spec/ElementSelectSpec.js +++ b/tests/jasmine/unit/spec/ElementSelectSpec.js @@ -6,6 +6,7 @@ /* global expect */ /* global QfqNS */ /* global beforeAll */ +/* global beforeEach */ /* global jasmine */ /* global $ */ @@ -16,6 +17,9 @@ describe("Element Select", function () { var selectTest2; beforeAll(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); personTitleSelect = new QfqNS.Element.Select($('#personTitle')); selectTest2 = new QfqNS.Element.Select($('#selectTest2')); }); diff --git a/tests/jasmine/spec/ElementSpec.js b/tests/jasmine/unit/spec/ElementSpec.js similarity index 88% rename from tests/jasmine/spec/ElementSpec.js rename to tests/jasmine/unit/spec/ElementSpec.js index b4f47d3319005100eceff32bd1d3e0378497ec51..c0363e6f3263e5d9bf1108c67c831a7a24111a07 100644 --- a/tests/jasmine/spec/ElementSpec.js +++ b/tests/jasmine/unit/spec/ElementSpec.js @@ -7,11 +7,18 @@ /* global expect */ /* global QfqNS */ /* global beforeAll */ +/* global beforeEach */ /* global jasmine */ /* global $ */ describe('Element namespace function getElement()', function () { 'use strict'; + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); + }); + it("should get the proper element instance for radio", function () { var element = QfqNS.Element.getElement('gender'); expect(element instanceof QfqNS.Element.Radio).toBe(true); diff --git a/tests/jasmine/spec/ElementTextualSpec.js b/tests/jasmine/unit/spec/ElementTextualSpec.js similarity index 84% rename from tests/jasmine/spec/ElementTextualSpec.js rename to tests/jasmine/unit/spec/ElementTextualSpec.js index ea767c07fe760b27f3edbf857af7ab07255911d2..fa4907a9ca887ff4051c8c1dc13303e5752af744 100644 --- a/tests/jasmine/spec/ElementTextualSpec.js +++ b/tests/jasmine/unit/spec/ElementTextualSpec.js @@ -6,6 +6,7 @@ /* global expect */ /* global QfqNS */ /* global beforeAll */ +/* global beforeEach */ /* global jasmine */ /* global $ */ @@ -14,7 +15,10 @@ describe("Element Text", function () { var $textInput; var textInput; - beforeAll(function () { + beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); $textInput = $("input[name='personHandle']"); textInput = new QfqNS.Element.Textual($textInput); }); diff --git a/tests/jasmine/spec/FormSpec.js b/tests/jasmine/unit/spec/FormSpec.js similarity index 97% rename from tests/jasmine/spec/FormSpec.js rename to tests/jasmine/unit/spec/FormSpec.js index 0ed3c93e0c5b9b8a868cdbea9126e5572a06dc75..19a946d40a2a200335b041a1dbe3769c27348355 100644 --- a/tests/jasmine/spec/FormSpec.js +++ b/tests/jasmine/unit/spec/FormSpec.js @@ -17,6 +17,9 @@ describe("Form", function () { var form; beforeEach(function () { + $("#jasmineTestTemplateTarget") + .empty() + .append($("#jasmineTestTemplate").text()); form = new QfqNS.Form("myForm"); }); diff --git a/tests/jasmine/unit/spec/HelperJqxDateTimeInputSpec.js b/tests/jasmine/unit/spec/HelperJqxDateTimeInputSpec.js new file mode 100644 index 0000000000000000000000000000000000000000..a2fbf217d3845470f0fcf29fe52788db7c6add3e --- /dev/null +++ b/tests/jasmine/unit/spec/HelperJqxDateTimeInputSpec.js @@ -0,0 +1,21 @@ +/** + * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> + */ + +/* global describe */ +/* global it */ +/* global expect */ +/* global QfqNS */ +/* global beforeAll */ +/* global beforeEach */ +/* global jasmine */ +/* global $ */ + +describe("jqxDateTimeInput helper", function () { + 'use strict'; + + it('should properly initialize all date time inputs', function () { + QfqNS.Helper.jqxDateTimeInput(); + + }); +}); diff --git a/tests/jasmine/spec/PageTitleSpec.js b/tests/jasmine/unit/spec/PageTitleSpec.js similarity index 100% rename from tests/jasmine/spec/PageTitleSpec.js rename to tests/jasmine/unit/spec/PageTitleSpec.js diff --git a/uml/javascript/classdiagram.pu b/uml/javascript/classdiagram.pu index 67061f364e389ee49552e446636234bfddaef0f8..c02c65347ef9db25d272dd4afc4508fe1b65e1c1 100644 --- a/uml/javascript/classdiagram.pu +++ b/uml/javascript/classdiagram.pu @@ -1,33 +1,144 @@ @startuml +scale max 2100*2970 skinparam classAttributeIconSize 0 package "QfqNS" { class Alert { --makeAlertContainerSingleton() --countAlertsInAlertContainer() --getAlertClassbasedOnMessageTyp() --getButtons() --afterFadeIn() --removeAlert() --okButtonHandler() --saveButtonHandler() --cancelButtonHandler() -+isShown() -+show() + +isShown() + +show() + -afterFadeIn() + -cancelButtonHandler() + -countAlertsInAlertContainer() + -getAlertClassbasedOnMessageTyp() + -getButtons() + -makeAlertContainerSingleton() + -buttonHandler() } class EventEmitter -class BSTabs -class FileDelete -class FileUpload -class Form -class PageState -class PageTitle <<singleton>> -class QfqForm -class QfqPage -class QfqRecordList -class Log <<singleton>> +class QfqEvents <<mixin>> { + +makePayload() + +onMixin() +} + +class BSTabs { + +activateTab() + +getActiveTab() + +getContainingTabIdForFormControl() + +getCurrentTab() + +getTabAnchors() + +getTabIds() + +getTabName() + -fillTabInformation() + -installTabHandlers() + -tabShowHandler() + -getActiveTabFromDOM() +} + +class FileDelete { + -setupOnClickHandler() + -buttonClicked() + -performFileDelete() + -prepareData() + -ajaxSuccessHandler() + -ajaxErrorHandler() +} + +class FileUpload { + -setupOnChangeHandler() + -performFileUpload() + -prepareData() + -ajaxSuccessHandler() + -ajaxErrorHandler() +} + +class Form { + +getFormChanged() + +resetFormChanged() + +submitTo() + +validate() + -ajaxSuccessHandler() + -changeHandler() + -serialize() + -submitFailureHandler() +} + +class PageState { + -popStateHandler() + +getPageState() + +getPageData() + +setPageState() + +newPageState() +} + +class PageTitle <<static>> { + +set() + +get() + +setSubTitle() +} + +class QfqForm { + +getSip() + +isFormChanged() + +submit() + -ajaxDeleteSuccessDispatcher() + -applyElementConfiguration() + -changeHandler() + -clearAllValidationStates() + -destroyFormAndSetText() + -endUploadHandler() + -fileDeleteSuccessHandler() + -fileUploadSuccessHandler() + -formUpdateHandler() + -getCloseButton() + -getDeleteButton() + -getFormGroupByControlName() + -getNewButton() + -getNewButtonTarget() + -getSaveButton() + -handleCloseClick() + -handleDeleteClick() + -handleDeleteSuccess() + -handleFormUpdate() + -handleLogicDeleteError() + -handleLogicSubmitError() + -handleNewClick() + -handleSaveClick() + -handleSubmitSuccess() + -readElementConfigurationData() + -resetHandler() + -resetValidationState() + -setBsTabs() + -setButtonEnabled() + -setHelpBlockValidationMessage() + -setValidationState() + -setupFormUpdateHandler() + -startUploadHandler() + -submitSuccessDispatcher() +} + +class QfqPage { + -destroyFormHandler() + -tabShowHandler() + -popStateHandler() +} + +class QfqRecordList { + -connectClickHandler() + -handleDeleteButtonClick() + -ajaxDeleteSuccessDispatcher() + -handleDeleteSuccess() + -getRecordElement() + -handleLogicDeleteError() +} + +class Log <<static>> { + +message() + +debug() + +warning() + +error() +} Alert .. FileDelete Alert .. QfqForm @@ -44,27 +155,56 @@ PageTitle .. QfqPage BSTabs "1" <--* "1" QfqForm Form "1" <--* "1" QfqForm -EventEmitter "1" <--* "1" QfqForm +QfqEvents "1" <--* "1" QfqForm FileUpload "1" <--* "1" QfqForm FileDelete "1" <--* "1" QfqForm -EventEmitter "1" <--* "1" PageState +QfqEvents <-- Alert + +QfqEvents "1" <--* "1" PageState Log .. PageState -EventEmitter "1" <--* "1" FileUpload +QfqEvents "1" <--* "1" FileUpload -EventEmitter "1" <--* "1" FileDelete +QfqEvents "1" <--* "1" FileDelete -EventEmitter "1" <--* "1" BSTabs +QfqEvents "1" <--* "1" BSTabs + +EventEmitter <-- QfqEvents } package "QfqNS.Element" { -class Checkbox -class FormGroup -class Radio -class Select -class Textual +class Checkbox { + +setValue() + +getValue() +} + +class FormGroup { + +hasHelpBlock() + +hasLabel() + +isType() + +setEnabled() + +setHidden() + +setReadOnly() + +setRequired() + -$findFormGroup() + -readOnlyHandler() +} +class Radio { + +setValue() + +getValue() +} +class Select { + +getValue() + +setValue() + -clearSelection() + -setSelection() +} +class Textual { + +setValue() + +getValue() +} FormGroup <-- Checkbox