The following features are only tested / supported on linux hosts:

  • General: QFQ is coded to run on Linux hosts, preferable on Debian derivates like Ubuntu.
  • HTML to PDF conversion - command wkhtmltopdf.
  • Concatenation of PDF files - command pdfunite.
  • PDF decrypt (used for merge with pdfunite) - command qpdf.
  • Mime type detection for uploads - command file.


Report & Form

To normalize UTF8 input, php-intl package is needed by

  • normalizer::normalize()

For the download function, the programs pdfunite, qpdf and file are necessary to concatenate PDF files.

Preparation for Ubuntu:

sudo apt install php-intl
sudo apt install poppler-utils libxrender1 file pdf2svg pdfunite qpdf # for file upload, PDF and 'HTML to PDF' (wkhtmltopdf), PDF split
sudo apt install inkscape imagemagick            # to render thumbnails


wkhtmltopdf will be used by QFQ to offer ‘website print’ and ‘HTML to PDF’ conversion. The program is not included in QFQ and has to be manually installed.

  • The Ubuntu package wkhtmltopdf needs a running Xserver - this does not work on a headless webserver.
    • Best is to install the QT version from the named website above.
    • In case of trouble with wkhtmltopdf, also install ‘libxrender1’.
    • The current version 0.12.4 might have trouble with https connections. Version 0.12.5-dev (github master branch) seems more reliable. Please contact the QFQ authors if you need a compiled Ubuntu version of wkhtmltopdf.

In configuration specify:

config.cmdWkhtmltopdf:  /opt/wkhtmltox/bin/wkhtmltopdf

If wkhtml has been compiled with dedicated libraries (not part of LD_LIBRARY_PATH), specify the LD_LIBRARY_PATH together with the path-filename:

config.cmdWkhtmltopdf: LD_LIBRARY_PATH=/opt/wkhtmltox/lib /opt/wkhtmltox/bin/wkhtmltopdf


To access FE_GROUP protected pages or content, it’s necessary to disable the [FE][lockIP] check! wkhtml will access the Typo3 page locally (localhost) and that IP address is different from the client (=user) IP.

Configure via Typo3 Installtool All configuration > $TYPO3_CONF_VARS[‘FE’]:

[FE][lockIP] = 0


[FE][lockIP] = 0 disables an important anti-‘session hijacking’ protection. The security level of the whole installation will be lowered! Again, this is only needed if wkhtml needs access to FE_GROUP protected pages & content. As an alternative to lower the security level, create a separated page subtree which is only accessible (configured via Typoscript) from specific IPs or if a FE-User is logged in.

If there are problems with converting/downloading FE_GROUP protected pages, check configuration showDebugInfo = download to debug.


Converting HTML to PDF gives no error message but RC=-1? Check carefully all includes of CSS, JS, images and so on! Typically some of them fails to load and wkhtml stops running! Verify the correct loading of all elements by calling the site via a regular browser and bypassing any browser cache (Ctrl F5).

Checklist wkhtml problems

  • config.baseUrl is configured and correct. The baseUrl has to be the same protocol as the website (http or https).
  • To track down problems:
    • In configuration set debug.showDebugInfo=auto,download.
    • Do the download.
    • Check QFQ_LOG for any output.
    • Grab the URL given in the QFQ_LOG, open a browser in private mode (no existing browser session) and open the URL.
    • Check the –cookie-jar ‘/tmp/qfq.cookie….’ file for the cookie.
    • Call wkhtml manually on the webserver, with the same options as given in the QFQ_LOG.

HTML to PDF conversion

wkhtmltopdf converts a website (local or remote) to a (multi)-page PDF file. It’s mainly used in download.


Different browser prints the same page in different variations. To prevent this, QFQ implements a small PHP wrapper print.php with uses wkhtmltopdf to convert HTML to PDF.

Provide a print this page-link (replace ‘current pageId’ ):

<a href="typo3conf/ext/qfq/Classes/Api/print.php?id={current pageId}">Print this page</a>

Any parameter specified after print.php will be delivered to wkhtmltopdf as part of the URL.

Typoscript code to implement a print link on every page:

10 = TEXT
10 {
  wrap = <a href="typo3conf/ext/qfq/Classes/Api/print.php?id=|&type=99"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Printview</a>
  data = page:uid

Send Email

QFQ sends mail via sendEmail - a small perl script without a central configuration.

By default, sendEmail uses the local installed MTA, writes a logfile to fileadmin/protected/log/mail.log and handles attachments via commandline options. A basic HTML email support is implemented.

The latest version is v1.56, which has at least one bug. That one is patched in the QFQ internal version v1.56p1 (see QFQ GIT sources in directory ‘patches/sendEmail.patch’).

Nevertheless, on latest system the TLS support is broken - please check sendEmailProblem.

The Typo3 sendmail eco-system is not used at all by QFQ.


Thumbnails will be rendered via ImageMagick ( ‘convert’ and ‘inkscape’ ( ‘inkscape’ is only used for ‘.svg’ files.

The Typo3 graphic eco-system is not used at all by QFQ.

Usage: column-thumbnail.


  • Install the extension via the Extension Manager.
    • If you install the extension by manual download/upload and get an error message “can’t activate extension”: rename the downloaded zip file to or qfq_<version>.zip (e.g. version: 18.12.0).
    • If the Extension Manager stops after importing: check your memory limit in php.ini.
  • Copy/rename the file <site path>/typo3conf/ext/qfq/config.example.qfq.php to <site path>/typo3conf/config.qfq.php. Configure the necessary settings configuration The configuration file is outside of the extension directory, to not loose it during de-install and install again.
  • When the QFQ Extension is called the first time on the Typo3 frontend, the file <ext_dir>/Classes/Sql/formEditor.sql will played and fills the database with the Form editor records. This also happens automatically after each update of QFQ.
  • Configure Typoscript to include Bootstrap, jQuery, QFQ javascript and CSS files.

Setup CSS & JS

page.meta {
    X-UA-Compatible = IE=edge
    X-UA-Compatible.attribute = http-equiv
    viewport=width=device-width, initial-scale=1

page.includeCSS {
    file01 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css
    file02 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css
    file03 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css
    file04 = typo3conf/ext/qfq/Resources/Public/Css/jqx.bootstrap.css
    file05 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css
    file06 = typo3conf/ext/qfq/Resources/Public/Css/tablesorter-bootstrap.css
    file07 = typo3conf/ext/qfq/Resources/Public/Css/font-awesome.min.css

page.includeJS {
    file01 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.min.js
    file02 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js
    file03 = typo3conf/ext/qfq/Resources/Public/JavaScript/validator.min.js
    file04 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js
    file05 = typo3conf/ext/qfq/Resources/Public/JavaScript/globalize.js
    file06 = typo3conf/ext/qfq/Resources/Public/JavaScript/tinymce.min.js
    file07 = typo3conf/ext/qfq/Resources/Public/JavaScript/EventEmitter.min.js
    file08 = typo3conf/ext/qfq/Resources/Public/JavaScript/typeahead.bundle.min.js
    file09 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.min.js
    file10 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.tablesorter.combined.min.js
    file11 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.tablesorter.pager.min.js
    file12 = typo3conf/ext/qfq/Resources/Public/JavaScript/widget-columnSelector.min.js

    # Only needed in case FormElement 'annotate' is used.
    file20 = typo3conf/ext/qfq/Resources/Public/JavaScript/fabric.min.js
    file21 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.fabric.min.js


Setup a report to manage all forms:

  • Create a Typo3 page.

  • Set the ‘URL Alias’ to form (recommended) or the individual defined value in parameter editFormPage (configuration).

  • Insert a content record of type qfq.

  • In the bodytext insert the following code:

    # If there is a form given by SIP: show
    # In case indexQfq != indexData, set dbIndex=indexQfq.
    dbIndex = {{indexQfq:Y}}
    10 {
        # List of Forms: Do not show this list of forms if there is a form given by SIP.
        # Table header.
        sql = SELECT CONCAT('p:{{pageAlias:T}}&form=form|A:data-reference=newForm') as _pagen, '#', 'Name', 'Title', 'Table', '' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'=''
        head = {{'b|p:id={{pageAlias:T}}&form=copyFormFromExt|t:Copy form from ExtForm|A:data-reference=copyForm' AS _link}}
               <table class="table table-hover qfq-table-50 tablesorter tablesorter-filter" id="{{pageAlias:T}}-form">
        tail = </table>
        rbeg = <thead class="qfq-sticky"><tr>
        rend = </tr></thead>
        fbeg = <th>
        fend = </th>
        10 {
            # All forms
            sql = SELECT CONCAT('p:{{pageAlias:T}}&form=form&r=',, '|A:data-reference=editForm', as _pagee
                         ,, QMORE(, 50), f.title AS '_striptags', f.tableName
                         , CONCAT('U:form=form&r=',, '|A:data-reference=deletForm') as _paged
                      FROM Form AS f
                      ORDER BY
            rbeg = <tr>
            rend = </tr>
            fbeg = <td>
            fend = </td>

To keep the overview about all forms, it’s useful to know which form has been used, how often, on which page and when. Find these information included in the form-editor-usage report.

Installation: Check List

  • Protect the directory <T3 installation>/fileadmin/protected in Apache against direct file access.
    • <T3 installation>/fileadmin/protected/ should be used for confidential (uploaded / generated) data.
    • <T3 installation>/fileadmin/protected/log/… is the default place for QFQ log files.
  • Protect the directory <T3 installation>/fileadmin in Apache to not execute PHP Scripts - malicious uploads won’t be executed.
  • Setup a log rotation rule for sqlLog.
  • Check that sqlLogMode is set to modify on productive sites. With none you have no chance to find out who changed which data and all really logs a mass of data.



Keyword Example Description
DB_<n>_USER DB_1_USER=qfqUser Credentials configured in MySQL
DB_<n>_PASSWORD DB_1_PASSWORD=1234567890 Credentials configured in MySQL
DB_<n>_SERVER DB_1_SERVER=localhost Hostname of MySQL Server
DB_<n>_NAME DB_1_NAME=qfq_db Database name
LDAP_1_RDN LDAP_1_PASSWORD LDAP_1_RDN=’ou=Admin,ou=example,dc=com’ LDAP_1_PASSWORD=’mySecurePassword’ Credentials for non-anonymous LDAP access. Only one set supported.

Example: typo3conf/config.qfq.php:


// QFQ configuration
// Save this file as: <site path>/typo3conf/config.qfq.php

return [
    'DB_1_USER' => '<DBUSER>',
    'DB_1_SERVER' => '<DBSERVER>',
    'DB_1_PASSWORD' => '<DBPW>',
    'DB_1_NAME' => '<DB>',

    //DB_2_USER => <DBUSER>
    //DB_2_PASSWORD => <DBPW>
    //DB_2_NAME => <DB>

    // DB_n ...
    // ...

    // LDAP_1_RDN => 'ou=Admin,ou=example,dc=com'
    // LDAP_1_PASSWORD => 'mySecurePassword'

Extension Manager: QFQ Configuration

Keyword Default / Example Description
flagProduction yes yes|no: used to differentiate production and development site.
maxFileSize 10M If empty, take minimum of ‘post_max_size’ and ‘upload_max_filesize’.
baseUrl URL where wkhtmltopdf will fetch the HTML (no parameter, those comes later)
dateFormat yyyy-mm-dd Possible options: yyyy-mm-dd,
thumbnailDirSecure fileadmin/protected/qfqThumbnail Important: secure directory ‘protected’ (recursive) against direct access.
thumbnailDirPublic typo3temp/qfqThumbnail Both thumbnail directories will be created if not existing.
cmdInkscape inkscape If inkscape is not available, specify an empty string.
cmdConvert convert GraphicsMagics ‘convert’ is recommended.
cmdWkhtmltopdf /usr/bin/wkhtmltopdf PathFilename of wkhtmltopdf. Optional variables like LD_LIBRARY_PATH=…
sendEMailOptions -o tls=yes General options. Check:
documentation Link to the online documentation of QFQ. Every QFQ installation also contains a local copy: typo3conf/ext/qfq/Documentation/html/Manual.html
fillStoreSystemBySql1/2/3 SELECT AS … Specific values read from the database to fill the system store during QFQ load. See fillStoreSystemBySql for a usecase.
fillStoreSystemBySqlErrorMsg1/2/3 No current period found Only define an error message, if QFQ should stop running in case of an SQL error or not exact one record.
throwExceptionGeneralError auto
yes: ‘general errors’ in QFQ (PHP) will throw an exception.
auto: becomes ‘yes’, if ‘flagProduction’!=’yes’, else ‘no’.
no: ‘general errors’ in QFQ (PHP) will be silently ignored.
formSubmitLogMode all
all: every form submission will be logged.
none: no logging.
See form-submit-log-page for example QFQ code to display the log.
redirectAllMailTo If set, redirect all QFQ generated mails (Form, Report) to the specified.
sqlLogMode modify
all: every statement will be logged - this might be a lot.
modifyAll: log all statements which might change data, even if 0 rows
modify: log only statements which change data (affected rows > 0).
error: log only DB errors.
none: no SQL log at all.
sqlLogModeAutoCron error Applies only to AutoCron Jobs. For production ‘error’ should be fine.
sqlLog fileadmin/protected/log/sql.log Filename to log SQL commands: relative to <site path> or absolute. If the directory does not exist, create it.
qfqLog fileadmin/protected/log/qfq.log Filename to log general QFQ events:relative to <site path> or absolute. If the directory does not exist, create it.
mailLog fileadmin/protected/log/mail.log Filename to log sendEmail commands: relative to <site path> or absolute. If the directory does not exist, create it.
showDebugInfo auto FE - Possible values: yes|no|auto|download. For ‘auto’: If a BE User is logged in, a debug information will be shown on the FE.
init init=SET names utf8; SET sql_mode = “NO_ENGINE_SUBSTITUTION” Global init for using the database. For ‘sql_mode=”NO_ENGINE_SUBSTITUTION”’ see #7407.
update auto
auto: apply DB Updates only if there is a newer version.
always: apply DB Updates always, especially play formEditor.sql every time QFQ is called - not recommended!
never: never apply DB Updates.
indexData 1 Optional. Default: 1. Retrieve the current setting via {{dbNameData:Y}}.
indexQfq 1 Optional. Default: 1. Retrieve the current setting via {{dbNameQfq:Y}}.
escapeTypeDefault m All variables {{…}} get this escape class by default. See variable-escape.
securityVarsHoneypot email,username,password If empty: no check. All named variables will rendered as INPUT elements.
securityAttackDelay 5 If an attack is detected, sleep ‘x’ seconds and exit PHP process.
securityShowMessage true If an attack is detected, show a message.
securityGetMaxLength 50 GET vars longer than ‘x’ chars triggers an attack-recognized. ExceptionMaxLength.
securityFailedAuthDelay 3 If REST authorization fails, sleep ‘x’ seconds before answering.
recordLockTimeoutSeconds 900 Timeout for record locking. After this time, a record will be replaced.
sessionTimeoutSeconds 1800 Timeout for FE User session. See sessionTimeoutSeconds
enterAsSubmit enterAsSubmit = 1 0: off, 1: Pressing enter in a form means save and close.
editFormPage form T3 Pagealias to edit a form.
formDataPatternError please check pattern error Customizable error message used in validator.js. ‘pattern’ violation.
formDataRequiredError missing value Customizable error message used in validator.js. ‘required’ fields.
formDataMatchError type error Customizable error message used in validator.js. ‘match’ retype mismatch.
formDataError generic error Customizable error message used in validator.js. ‘no specific’ given.
labelAlign left Label align (left/center/right)/ Default: left. Will be inherited to Form.
cssClassQfqContainer container
QFQ with own Bootstrap: ‘container’.
QFQ already nested in Bootstrap of mainpage: <empty>.
cssClassQfqForm qfq-color-base Wrap around QFQ ‘Form’.
cssClassQfqFormPill qfq-color-grey-1 Wrap around title bar for pills: CSS Class, typically a background color.
cssClassQfqFormBody qfq-color-grey-2 Wrap around FormElements: CSS Class, typically a background color.
formBsColumns col-md-12 col-lg-10 The whole form will be wrapped. See bs-custom-field-width
formBsLabelColumns col-md-3 col-lg-3 The column get the width. See bs-custom-field-width
formBsInputColumns col-md-6 col-lg-6
formBsNoteColumns col-md-3 col-lg-3
extraButtonInfoInline <img src=”info.png”> Image for extraButtonInfo (inline).
extraButtonInfoBelow <img src=”info.png”> Image for extraButtonInfo (below).
extraButtonInfoPosition below ‘auto’ (default) or ‘below’. See extraButtonInfo.
extraButtonInfoClass pull-right ‘’ (default) or ‘pull-right’. See extraButtonInfo.
formLanguage[ABCD]Id E.g.: 1 In Typo3 configured pageLanguage id. The number after the ‘L’ parameter.
formLanguage[ABCD]Label E.G.: english Label shown in Form editor, on the ‘basic’ tab.
saveButtonText   Text on the form save button. Typically none.
saveButtonTooltip Save Tooltip on the form save button.
saveButtonClass btn btn-default navbar-btn Bootstrap CSS class for save button on top of the form.
buttonOnChangeClass alert-info btn-info Bootstrap CSS class for save button showing ‘data changed’.
saveButtonGlyphIcon glyphicon-ok Icon for the form save button.
closeButtonText   Text on the form close button. Typically none.
closeButtonTooltip close Tooltip on the form close button.
closeButtonClass btn btn-default navbar-btn Bootstrap CSS class for close button on top of the form.
closeButtonGlyphIcon glyphicon-remove Icon for the form close button.
deleteButtonText   Text on the form delete button. Typically none.
deleteButtonTooltip delete Tooltip on the form delete button.
deleteButtonClass btn btn-default navbar-btn Bootstrap CSS class for delete button on top of the form.
deleteButtonGlyphIcon glyphicon-trash Icon for the form delete button.
newButtonText   Text on the form new button. Typically none.
newButtonTooltip new Tooltip on the form new button.
newButtonClass btn btn-default navbar-btn Bootstrap CSS class for new button on top of the form.
newButtonGlyphIcon glyphicon-plus Icon for the form new button.
showIdInFormTitle 0 (off), 1 (on) Append at the form title the current record id.
cssClassColumnId text-muted A column in a subrecord with the name id|ID|Id gets this class.

After parsing the configuration, the following variables will be set automatically in STORE_SYSTEM:

Keyword Description
dbNameData Name of the ‘data’-database. ‘{{dbNameData:Y}}
dbNameQfq Name of the ‘QFQ’-database. ‘{{dbNameQfq:Y}}
dbNameT3 Name of the ‘T3’-database. ‘{{dbNameT3:Y}}
sitePath Absolute path of the current T3 instance. ‘{{sitePath:Y}}
extPath Absolute path of the QFQ extension. ‘{{extPath:Y}}

Custom variables

Up to 30 custom variables can be defined in configuration.

E.g. to setup a contact address and reuse the information inside your installation do:

  custom2: ADMINISTRATIVE_ADDRESS = John Doe, Hollywood Blvd. 1, L.A.
  custom3: ADMINISTRATIVE_NAME = John Doe

* Somewhere in a `Form` or in `Report`::


It’s also possible to configure such variables directly in config.qfq.php.


A specified SELECT statement in configuration in variable fillStoreSystemBySql1 (or 2, or 3) will be fired. The query should have 0 (nothing happens) or 1 row. All columns will be added to the existing STORE_SYSTEM. Existing variables will be overwritten. Be careful not to overwrite system values.

This option is useful to make generic custom values, saved in the database, accessible to all QFQ Report and Forms. Access such variables via {{<varname>:Y}}.

In case QFQ should stop working if a given query does not select exact one record (e.g. a missing period), define an error message:

fillStoreSystemBySql1: SELECT name FROM Person WHERE name='Doe'
fillStoreSystemBySqlErrorMsg1: Too many or to few "Doe's" in our database


This is

  • a usecase, implemented via fillStoreSystemBySql,
  • a way to access with respect to the current period (the period itself is custom defined).

After a full QFQ installation:

  • a table Period (extend / change it to your needs, fill them with your periods),
  • one sample record in table Period,

Websites, delivering semester data, school year schedules, or any other type or periods, often need an index to the current period.

In configuration:

fillStoreSystemBySql1: SELECT id AS periodId FROM Period WHERE start<=NOW() ORDER BY start DESC LIMIT 1

a variable ‘periodId’ will automatically computed and filled in STORE SYSTEM. Access it via {{periodId:Y0}}. To get the name and current period:

SELECT name, ' / ', start FROM Period WHERE id={{periodId:Y0}}

Typically, it’s necessary to offer a ‘previous’ / ‘next’ link. In this example, the STORE SIP holds the new periodId:

SELECT CONCAT('p:{{pageAlias:T}}&periodId=', {{periodId:SY0}}-1, '|Next') AS _page, ' ', name, ' ',
  CONCAT('p:{{pageAlias:T}}&periodId=', {{periodId:SY0}}+1, '|Next') AS _page FROM Period AS s WHERE{{periodId:SY0}}

Take care for minimum and maximum indexes (do not render the links if out of range).

DB USER privileges

The specified DB User needs privileges to the database of at least: SELECT / INSERT / UPDATE / DELETE / SHOW.

To apply automatically QFQ-‘DB UPDATE’ the following rights are mandatory too: CREATE / ALTER

To get access to the Typo3 installation, ‘dbuser’ should also have access to the Typo3 Database with at least SELECT / INSERT / UPDATE / DELETE.


If it is necessary to use a GET variable which exceeds securityGetMaxLength limit, name the variable with ‘_<num>’ at the end. E.g. my_long_variable_130. Such a variable has an allowed length of 130 chars. Access the variable as usual with the variable name: {{my_long_variable_130:C:…}}.

FE-User: Session timeout seconds

There is no timeout for website users who are not logged in (but typically those users don’t have access to protected content).

For logged in users, the default timeout is the php.ini settings for session.cookie_lifetime and session.gc_maxlifetime (minimum of both). These timeout only affects QFQ related content and can be specified a) globally (QFQ configuration) and b) specific per Form.

The maximum timeout depends on the minimal value of php.ini session.cookie_lifetime and session.gc_maxlifetime. Specifying a higher value produces an error in the front end.

Every access to QFQ related content resets the timeout.

After FE login, the next access to QFQ related content starts the timeout counter.

Local Documentation

A HTML rendered version is available under: <your site>/typo3conf/ext/qfq/Documentation/html/Index.html

If you get a ‘Page forbidden / not found’ there might be some Webserver restrictions. E.g. the Typo3 example of .htaccess in the Typo3 installation folder will forbid access to any extension documentation (which is a good idea on a productive server). For a development server instead, deactivate the forbid rule of ‘documentation’. In .htaccess (snippet from Typo3 7.6 _.htaccess):

production:   RewriteRule (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/ - [F]
development:  RewriteRule (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|docs?)/ - [F]



The following is a technical background information. Not needed to just use QFQ.

The SIPs (=Server Id Pairs) are uniq timestamps, created/registered on the fly for a specific browser session (=user). Every SIP is registered on the server (= stored in a browser session) and contains one or more key/value pairs. The key/value pairs never leave the server. The SIPs will be used:

  • to protect values not to be spoofed by anyone,
  • to protect values not to be altered by anyone,
  • to grant access, e.g.:
    • load and save forms,
    • upload files,
    • download files,
    • PHP AJAX pages.

SIPs becomes invalid, as soon as the current browser session is destroyed. The client (= user) can’t manipulate the content of SIPs - it’s only possible to reuse already registered SIPs by the user, who already owns the session.

Access privileges

The Typo3 FE Groups can be used to implement access privileges. Such groups are assigned to

  • Typo3 FE users,
  • Typo3 pages,
  • and/or Typo3 content records (e.g. QFQ records).

This will be used for general page structure privileges.

A record base privileges controlling (e.g. which user can edit which person record) will be implicit configured, by the way that records are viewable / editable (or not) through SQL in the specific QFQ tt-content statements.

Typo3 QFQ content element

Insert one or more QFQ content elements on a Typo3 page. Specify column and language per content record as wished.

The title of the QFQ content element will not be rendered on the frontend. It’s only visible to the webmaster in the backend for orientation.

QFQ Keywords (Bodytext)

All of the named parameter are optional.

Name Explanation
Static: form = person
By SIP: form = {{form:SE}}
By SQL: form = {{SELECT c.form FROM config AS c WHERE{{a:C}} }}
<record id>. The form will load the record with the specified id.
Static: r = 123
By SQL: r = {{SELECT …}}
If not specified, the SIP parameter ‘r’ is used.
dbIndex E.g. dbIndex = {{indexQfq:Y}} Select a DB index. Only necessary if a different than the standard DB should be used.
debugShowBodyText If=’1’ and configuration:showDebugInfo: yes, shows a tooltip with bodytext
sqlLog Overwrites configuration: SQL_LOG . Only affects Report, not Form.
sqlLogMode Overwrites configuration: SQL_LOG_MODE . Only affects Report, not Form.
<level>.fbeg Start token for every field (=column)
<level>.fend End token for every field (=column)
<level>.fsep Separator token between fields (=columns)
<level>.fskipwrap Comma separated list of column id’s. Skip wrapping of indexed columns.
<level>.shead Static start token for whole <level>, independent if records are selected Shown before head.
<level>.stail Static end token for whole <level>, independent if records are selected. Shown after tail.
<level>.head Dynamic start token for whole <level>. Only if at least one record is select.
<level>.tail Dynamic end token for whole <level>. Only if at least one record is select.
<level>.rbeg Start token for row.
<level>.rbgd Alternating (per row) token.
<level>.rend End token for row. Will be rendered before subsequent levels are processed
<level>.renr End token for row. Will be rendered after subsequent levels are processed
<level>.rsep Seperator token between rows
<level>.sql SQL Query
<level>.twig Twig Template
<level>.althead If <level>.sql has no rows selected (empty), these token will be rendered.
<level>.altsql If <level>.sql has no rows selected (empty) or affected (delete, update, insert) the <altsql> will be fired. Note: Sub queries of <level> are not fired, even if <altsql> selects some rows.
show (default): content of current and sub level are directly shown.
hide: content of current and sub levels are stored and not shown.
hideLevel: content of current and sub levels are stored and only sub levels
are shown.
store: content of current and sub levels are stored and shown.
To retrieve the content: {{<level>.line.content}}. See syntax-of-report

QFQ Database

Recommended setup for Typo3 & QFQ Installation is with two databases. One for the Typo3 installation and one for QFQ. A good practice is to name both databases equal, appending the suffix ‘_t3’ and ‘_db’.

When QFQ is called, it checks for QFQ system tables. If they do not exist or have a lower version than the installed QFQ version, the system tables will be automatically installed or updated.

System tables

Name Use Database
Clipboard Temporary QFQ
Cron Persistent QFQ
Dirty Temporary QFQ | Data
Form Persistent QFQ
FormElement Persistent QFQ
FormSubmitLog Persistent QFQ | Data
MailLog Persistent QFQ | Data
Period Persistent Data
Split Persistent Data

See mail-log-page and form-submit-log-page for some Frontend views for these tables.

  • Check Bug #5459 / support of system tables in different DBs not supported.

Multi Database

Base: T3 & QFQ

QFQ typically interacts with one database, the QFQ database. The database used by Typo3 is typically a separate one. Theoretically it might be the same (never tested), but it’s strongly recommended to use a separated QFQ database to have no problems on Typo3 updates and to have a clean separation between Typo3 and QFQ.

QFQ: System & Data

QFQ itself can be separated in ‘QFQ system’ (see system-tables) and ‘QFQ data’ databases (even more than one are possible). The ‘QFQ system’ stores the forms, record locking, log tables and so on - QFQ data is for the rest.

A Multi Database setup is given, if ‘QFQ system’ is different from ‘QFQ data’.

Data: Data1, Data2, …, Data n

Every database needs to be configured via configuration with it’s own index.

QFQ data might switch between different ‘data’-databases. In configuration one main QFQ data index will be specified in indexQfq. If specific forms or reports should use a different database than that, dbIndex might change indexData temporarily.

dbIndex: A Report (field dbIndex) as well as a Form (field parameter.`dbIndex`) can operate on a specific database.

A Form will:

  • load the form-definition from indexQfq (table Form and FormElement),
  • loads and save data from/in indexData (config.qfq.php) / dbIndex (form.parameter.dbIndex),
  • retrieve extra information via dbIndexExtra - this is useful to offer information from a database and save them in a different one.

The simplest setup, QFQ system & data in the same database, needs no indexQfq / indexData definition in configuration or one or both of them set to ‘1’

To separate QFQ system and data, indexQfq and indexData will have different indexes.

A Multi Database setup might be useful for:

  • several independent Typo3 QFQ installations (each have it’s own form repository) and one central database, or
  • one QFQ installation which should display / load /save records from different databases, or
  • a combination of the above two.


  • Option ‘A’ is the most simple and commonly used.
  • Option ‘B’ separate the T3 and QFQ databases on two database hosts.
  • Option ‘C’ is like ‘B’ but with a shared ‘QFQ data’-database between three ‘Typo3 / QFQ’ instances.
  • Further variants are possible.
  Domain Website Host T3 QFQ system QFQ data
A ‘w’ <dbHost>, <dbname>_t3, <dbnameSingle>_db
B ‘wApp’ <dbHostApp>, <dbnameB1>_t3 <dbHostB1>, <dbnameApp>_db
B ‘wApp’ <dbHostApp>, <dbnameB2>_t3 <dbHostB2>, <dbnameApp>_db
C ‘wAppC’ <dbHostAppC>, <dbnameC1>_t3 <dbHostC>, <dbnameSysC1>_db <dbHostData>_db, <dbNameData>_db
C ‘wAppC’ <dbHostAppC>, <dbnameC2>_t3 <dbHostC>, <dbnameSysC2>_db <dbHostData>_db, <dbNameData>_db
C ‘wAppC3’ <dbHostAppC3>, <dbnameC3>_t3 <dbHostC3>, <dbnameSysC3>_db <dbHostData>_db, <dbNameData>_db

In config-qfq-php mutliple database credentials can be prepared. Mandatory is at least one credential setup like DB_1_USER, DB_1_SERVER, DB_1_PASSWORD, DB_1_NAME. The number ‘1’ indicates the dbIndex. Increment the number to specify further database credential setups.

Typically the credentials for DB_1 also have access to the T3 database.

Different QFQ versions, shared database

When using different QFQ versions and a shared ‘QFQ data’-database, there is some risk of conflicting ‘QFQ system’ tables. Best is to always use the same QFQ version on all instances or use a Multi Database setup.



Setup in configuration

  • qfqLog
    • Filename where to log QFQ debug and error messages.
    • File is relative to the <site path> or absolute (starting with ‘/’).
    • Content: error situations of QFQ and debug, if enabled.

All non SQL related information will be logged to QFQ log file.


Setup in configuration

  • sqlLog
    • Filename where to log SQL queries and statistical data.
    • File is relative to the <site path> or absolute (starting with ‘/’).
    • Content: SQL queries and timestamp, formName/formId, fe_user, success, affected rows, newly created record id’s and accessed from IP.
    • The global setting can be overwritten by defining sqlLog inside of a QFQ tt-content record.
  • sqlLogMode: all|modify|error|none

    • all: logs every SQL statement.
    • modify: logs only statements who might potentially change data.
    • error: logs only queries which generate SQL errors.
    • none: no query logging at all.
    • The global setting can be overwritten by defining sqlLogMode inside of a QFQ tt-content record.
  • showDebugInfo = [yes|no|auto],[download]

    If active, displays additional information in the Frontend (FE). This is typically helpful during development.

    • yes:

      • Form:

        • For every internal link/button, show tooltips with decoded SIP on mouseover.
        • Shows an ‘Edit form’-button (wrench symbol) on a form. The link points to the T3 page with the FormEditor.
      • Report: Will be configured per tt-content record.

        debugShowBodyText = 1

    • no: No debug info.

    • auto: Depending if there is a Typo3 BE session, set internally:

      • showDebugInfo = yes (BE session exist)
      • showDebugInfo = no (no BE session)
    • download:

      • During a download (especially by using wkhtml), temporary files are not deleted automatically. Also the wkhtmltopdf and pdfunite command lines will be logged to QFQ_LOG. Use this only to debug problems on download.


Setup in configuration

  • mailLog
    • File which sendEmail logs sending mail.
    • File is relative to the <site path> or absolute (starting with ‘/’).

Mail Log page (Table MailLog)

For debugging purposes you may like to add a Mail Log page in the frontend. The following QFQ code could be used for that purpose (put it in a QFQ PageContent element):

# Page parameters
1.sql = SELECT @grId := '{{grId:C0:digit}}' AS _grId
2.sql = SELECT @summary := IF('{{summary:CE:alnumx}}' = 'true', 'true', 'false') AS _s

# Filters
10 {
  sql = SELECT "'",, IF( = @grId, "' selected>", "'>"), gr.value, ' (Id: ',, ')'
           FROM gGroup AS gr
           INNER JOIN MailLog AS ml ON ml.grId =
           GROUP BY
  head = <form onchange='this.submit();' class='form-inline'><input type='hidden' name='id' value='{{pageAlias:T0}}'>
           Filter By Group: <select name='grId' class='form-control'><option value=''></option>
  rbeg = <option value=
  rend = </option>
  tail = </select>

20 {
  sql = SELECT IF(@summary = 'true', ' checked', '')
  head = <div class='checkbox'><label><input type='checkbox' name='summary' value='true'
  tail = >Summary</label></div></form>

# Mail Log
50 {
  sql = SELECT id, '</td><td>', grId, '</td><td>', xId, '</td><td>',
            REPLACE(receiver, ',', '<br>'), '</td><td>', REPLACE(sender, ',', '<br>'), '</td><td>',
            DATE_FORMAT(modified, '%d.%m.%Y<br>%H:%i:%s'), '</td><td style="word-break:break-word;">',
            CONCAT('<b>', subject, '</b><br>', IF(@summary = 'true', CONCAT(SUBSTR(body, 1,
                   LEAST(IF(INSTR(body, '\n') = 0, 50, INSTR(body, '\n')), IF(INSTR(body, '<br>') = 0, 50,
                   INSTR(body, '<br>')))-1), ' ...'), CONCAT('<br>', REPLACE(body, '\n', '<br>'))) )
          FROM MailLog WHERE (grId = @grId OR @grId = 0)
          ORDER BY modified DESC
          LIMIT 100
  head = <table class="table table-condensed table-hover"><tr>
  tail = </table>
  rbeg = <tr><td>
  rend = </td></tr>

Redirect all mail to (catch all)

Setup in configuration

    • During the development, it might be helpful to configure a ‘catch all’ email address, which QFQ uses as the final receiver instead of the original intended one.
    • The setting will:
      • Replace the ‘To’ with the configured one.
      • Clear ‘CC’ and ‘Bcc’
      • Write a note and the original configured receiver at the top of the email body.

Show log files realtime

Display QFQ log files in realtime.

The following QFQ code could be used for that purpose (put it in a QFQ PageContent element):

# {{logfile:SU}}

# Show buttons to select log file.
10 {
  sql = SELECT '{{logfile:SU:::sql.log}}' AS '_=logfile'
  head = <p>
  tail = </p>

  20.sql = SELECT CONCAT('p:{{pageAlias:T}}&logfile=sql.log|t:sql.log|b:', IF('{{logfile:R}}'='sql.log','primary','')) AS _page, ' ',
                  CONCAT('p:{{pageAlias:T}}&logfile=qfq.log|t:qfq.log|b:', IF('{{logfile:R}}'='qfq.log','primary','')) AS _page, ' ',
                  CONCAT('p:{{pageAlias:T}}&logfile=mail.log|t:mail.log|b:', IF('{{logfile:R}}'='mail.log','primary','')) AS _page

# Show selected log file.
100 {
  sql = SELECT 'file:fileadmin/protected/log/{{logfile:R}}' AS _monitor
  head = <pre id="monitor-1">Please wait</pre>

Form Submit Log page

For debugging purposes you may like to add a Form Submit Log page in the frontend. The following QFQ code could be used for that purpose (put it in a QFQ PageContent element):

# Filters
20.shead = <form onchange='this.submit()' class='form-inline'><input type='hidden' name='id' value='{{pageAlias:T0}}'>
20 {
  sql = SELECT "'", id, IF(id = '{{formId:SC0}}', "' selected>", "'>"), name
        FROM Form ORDER BY name
  head = <label for='formId'>Form:</label> <select name='formId' id='formId' class='form-control'><option value=0></option>
  tail = </select>
  rbeg = <option value=
  rend = </option>
30 {
  sql = SELECT feUser, IF(feUser = '{{feUser:SCE:alnumx}}', "' selected>", "'>"), feUser
        FROM FormSubmitLog GROUP BY feUser ORDER BY feUser
  head = <label for='feUser'>FE User:</label> <select name='feUser' id='feUser' class='form-control'><option value=''></option>
  tail = </select>
  rbeg = <option value='
  rend = </option>
30.stail = </form>

# Show Log
50 {
  sql = SELECT,
      CONCAT('<b>Form</b>: ',,
        '<br><b>Record Id</b>: ', l.recordId,
        '<br><b>Fe User</b>: ', l.feUser,
        '<br><b>Date</b>: ', l.created,
        '<br><b>Page Id</b>: ', l.pageId,
        '<br><b>Session Id</b>: ', l.sessionId,
        '<br><b>IP Address</b>: ', l.clientIp,
        '<br><b>User Agent</b>: ', l.userAgent,
        '<br><b>SIP Data</b>: <div style="margin-left:20px;">', "<script>var data = JSON.parse('", l.sipData,
          "'); for (var key in data) {
          document.write('<b>' + key + '</b>: ' + data[key] + '<br>'); }</script>", '</div>'),
      CONCAT("<script>var data = JSON.parse('", l.formData,
        "'); for (var key in data) {
          document.write('<b>' + key + '</b>: ' + data[key] + '<br>'); }</script>")
      FROM FormSubmitLog AS l
      LEFT JOIN Form AS f ON = l.formId
      WHERE (l.formId = '{{formId:SC0}}' OR '{{formId:SC0}}' = 0)
        AND (l.feUser = '{{feUser:SCE:alnumx}}' OR '{{feUser:SCE:alnumx}}' = '')
      ORDER BY l.created DESC LIMIT 100
  head = <table class="table table-hover">
         <tr><th>Id</th><th style="min-width:250px;">Environment</th><th>Submitted Data</th>
  tail = </table>
  rbeg = <tr>
  renr = </tr>
  fbeg = <td>
  fend = </td>


Variables in QFQ are surrounded by double curly braces. Four different types of variable substitution functionality is provided. Access to:

Some examples, including nesting:

# Store
{{name:FS:alnumx:s:my default}}

{{SELECT name FROM person WHERE id=1234}}

# Row columns

# Nesting
{{SELECT name FROM person WHERE id={{r}} }}
{{SELECT name FROM person WHERE id={{key1:C:alnumx}} }} # explained below
{{SELECT name FROM person WHERE id={{SELECT id FROM pf LIMIT 1}} }} # it's more efficient to use only one query

# Link Columns
{{p:form=Person&r=1|t:Edit Person|E|s AS link}}

Leading and trailing spaces inside curly braces are removed.

  • {{ SELECT “Hello World” }} becomes {{SELECT “Hello World”}}
  • {{ varname }} becomes {{varname}}


Store variables

Syntax: {{VarName[:<store(s)[:<sanitize class>[:<escape>[:<default value>[:type violate message]]]]]}}

See also:

  • Example:

    {{name:FSE:alnumx:m:John Doe}}
    {{name::::John Doe}}
    {{name:FSE:alnumx:m:John Doe:forbidden characters}}
    {{name:::::forbidden characters}}
  • Zero or more stores might be specified to be searched for the given VarName.

  • If no store is specified, the default for the searched stores are: FSRVD (=FORM > SIP > RECORD > VARS > DEFAULT).

  • If the VarName is not found in one store, the next store is searched, up to the last specified store.

  • If the VarName is not found and a default value is given, the default is returned.

  • If no value is found, nothing is replaced - the string ‘{{<VarName>}}’ remains.

  • If anywhere along the line an empty string is found, this is a value: therefore, the search will stop.

Sanitize class

Values in STORE_CLIENT C (Client=Browser) and STORE_FORM F (Form, HTTP ‘post’) are checked against a sanitize class. Values from other stores are not checked against any sanitize class, even if a sanitize class is specified.

  • Variables get by default the sanitize class defined in the corresponding FormElement. If not defined, the default class is ‘digit’.
  • A default sanitize class can be overwritten by individual definition: {{a:C:alnumx}}
  • If a value violates the specific sanitize class, see variable-type-message-violate for default or customized message. By default the value becomes !!<name of sanitize class>!!. E.g. !!digit!!.

For QFQ variables and FormElements:

Name Form Query Pattern
alnumx Form Query [A-Za-z][0-9]@-_.,;: /() ÀÈÌÒÙàèìòùÁÉÍÓÚÝáéíóúýÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿç
digit Form Query [0-9]
numerical Form Query [0-9.-+]
allbut Form Query All characters allowed, but not [ ] { } % #. The used regexp: ‘^[^[]{}%\#]+$’,
all Form Query no sanitizing

Only in FormElement:

Rules for CheckType Auto (by priority):

  • TypeAheadSQL or TypeAheadLDAP defined: alnumx
  • Table definition * integer type: digit * floating point number: numerical
  • FE Type * ‘password’, ‘note’: all * ‘editor’, ‘text’ and encode = ‘specialchar’: all
  • None of the above: alnumx

Escape/Action class

The following escape & action types are available:

  • The escape/action class is defined by the fourth parameter of the variable. E.g.: {{name:FE:alnumx:m}} (m = mysql).
  • It’s possible to combine multiple escape/action classes, they will be processed in the order given. E.g. {{name:FE:alnumx:Ls}} (L, s).
  • Escaping is typically necessary for all user supplied content, especially if they are processed via SQL or LDAP queries.
  • Be careful when escaping nested variables. Best is to escape only the most outer variable.
  • In configuration a global escapeTypeDefault can be defined. The configured escape/action class applies to all substituted variables, who do not contain a specific escape/action class.
  • Additionally a defaultEscapeType can be defined per Form (separate field in the Form editor). This overwrites the global definition of configuration. By default, every Form.defaultEscapeType = ‘c’ (=config), which means the setting in configuration.
  • To suppress an escape type, define the escape type = ‘-‘ on the specific variable. E.g.: {{name:FE:alnumx:-}}.


To ‘escape’ a character typically means: a character, which have a special meaning/function, should not treated as a special character. E.g. a string is surrounded by single ticks ‘. If such a string should contain the same single tick inside, the inside single tick has to be escaped - if not, the string end’s at the second tick, not the third. This is typically done by a backlash: \

QFQ offers different ways of escaping. Which of them to use, depends on the situation.

Especially variables used in SQL statements might cause trouble when using: NUL (ASCII 0), \n, \r, \, ‘, “, or Control-Z.


  • password - ‘p’: transforms the value of the variable into a Typo3 salted password hash. The hash function is the one used by Typo3 to encrypt and salt a password. This is useful to manipulate FE user passwords via QFQ. See setFeUserPassword
  • stop replace - ‘S’: typically QFQ will replace nested variables as long as there are variables to replace. This options stops this
  • exception - ‘X’: If a variable is not found in any given store, it’s replace by a default value or an error message. In special situation it might be useful to do a full stop on all current actions (no further procession). A custom message can be defined via: variable-type-message-violate.
  • wipe - ‘w’: In special cases it might be useful to get a value via SIP only one time and after retrieving the value it will be deleted in STORE SIP . Further access to the variable will return ‘variable undefined’. At time of writing only the STORE SIP supports the feature ‘wipe’. This is useful to suppress any repeating events by using the browser history. The following example will send a mail only the first when it is called with a given SIP:

    10.sql = SELECT ‘…’ AS _sendmail FROM Person AS p WHERE ‘{{action:S::w}}’=’send’ AND{{pId:S}}


  • Any string can be given to define a default value.
  • If a default value is given, it makes no sense to define more than one store: with a default value given, only the first store is considered.
  • If the default value contains a ‘:’, that one needs to be escaped by \

Type message violate

If a value violates the sanitize class, the following actions are possible:

  • ‘c’ - The violated class will be set as content, surrounded by ‘!!’. E.g. ‘!!digit!!’. This is the default.
  • ‘e’ - Instead of the value an empty string will be set as content.
  • ‘0’ - Instead of the value the string ‘0’ will be set as content.
  • ‘custom text …’ - Instead of the value, the custom text will be set as content. If the text contains a ‘:’, that one needs to be escaped by \ . Check variable-escape qualifier ‘C’ to let QFQ do the colon escaping.

SQL variables

  • The detection of an SQL command is case insensitive.
  • Leading whitespace will be skipped.
  • The following commands are interpreted as SQL commands:
    • SELECT
  • An SQL Statement might contain variables, including additional SQL statements. Inner SQL queries will be executed first.
  • All variables will be substituted one by one from inner to outer.
  • The number of variables inside an input field or an SQL statement is not limited.

Result: string

A result of an SQL statement will be imploded over all: concat all columns of a row, concat all rows - there is no glue string.

Result: row

A few functions needs more than a returned string, instead separate columns are necessary. To indicate an array result, specify those with an ‘!’:

{{!SELECT ...}}

This manual will specify the individual QFQ elements, who needs an array instead of a string. It’s an error to return a string where an array is needed and vice versa.

Database index

To access different databases in a multi-database setup, the database index can be specified after the opening curly braces.

{{[1]SELECT ... }}

For using the indexData and indexQfq (configuration), it’s a good practice to specify the variable name instead of the numeric index.

{{[{{indexData:Y}}]SELECT ...}}

If no dbIndex is given, {{indexData:Y}} is used.


{{SELECT id, name FROM Person}}
{{SELECT id, name, IF({{feUser:T0}}=0,'Yes','No')  FROM Person WHERE id={{r:S}} }}
{{SELECT id, city FROM Address AS adr WHERE adr.accId={{SELECT id FROM Account AS acc WHERE{{feUser:T0}} }} }}
{{!SELECT id, name FROM Person}}
{{[2]SELECT id, name FROM Form}}
{{[{{indexQfq:Y}}]SELECT id, name FROM Form}}

Row column variables

Syntax: {{<level>.<column>}}

Only used in report to access outer columns. See access-column-values and syntax-of-report.

There might be name conflicts between VarName / SQL keywords and <line identifier>. QFQ checks first for ‘<level>’, than for ‘SQL keywords’ and than for ‘VarNames’ in stores.

All types might be nested with each other. There is no limit of nesting variables.

Very specific: Also, it’s possible that the content of a variable is again (including curly braces) a variable - this is sometimes used in text templates, where the template is retrieved from a record and specific locations in the text will be (automatically by QFQ) replaced by values from other sources.

General note: using this type of variables is only the second choice. First choice is {{column:R}} (see access-column-values) - using the STORE_RECORD is more portable cause no renumbering is needed if the level keys change.


All values passed to QFQ will be:

  • Checked against max. length and allowed content, on the client and on the server side. On the server side, the check happens before any further processing. The ‘length’ and ‘allowed’ content is specified per FormElement. ‘digit’ or ‘alnumx’ is the default. Violating the rules will stop the ‘save record’ process (Form) or result in an empty value (Report). If a variable is not replaced, check the default sanitize class.
  • Only elements defined in the Form definition or requested by Report will be processed.
  • UTF8 normalized (normalizer::normalize) to unify different ways of composing characters. It’s more a database interest, to work with unified data.

SQL statements are typically fired as prepared statements with separated variables. Further custom SQL statements will be defined by the webmaster - those do not use prepared statements and might be affected by SQL injection. To prevent SQL injection, every variable is by default escaped with mysqli::real_escape_string.

QFQ notice:

  • Variables passed by the client (=Browser) are untrusted and use the default sanitize class ‘digit’ (if nothing else is specified). If alpha characters are submitted, the content violates digit and becomes therefore !!<name of sanitize class>!! - there is no error message. Best is to always use SIP (value is trustful) or at least digits for GET (=client) parameter (user might change those and therefore those are not trustful).

Get Parameter

QFQ security restriction:

  • GET parameter might contain urlencoded content (%xx). Therefore all GET parameter will be processed by ‘urldecode()’. As a result a text like ‘%nn’ in GET variables will always be decoded. It’s not possible to transfer ‘%nn’ itself.
  • GET values are limited to securityGetMaxLength (extension-manager-qfq-configuration) chars - any violation will stop QFQ. Individual exceptions are defined via ExceptionMaxLength.
  • GET parameter ‘type’ and ‘L’ might affected by (T3, configuration dependent) cache poisoning. If they contain non digit values, only the first character is used (if this is a digit) or completely cleaned (else).

Post Parameter

Per FormElement (HTML input) the default is to htmlspecialchars() the input. This means &<>’” will be encoded as htmlentity and saved as a htmlentity in the database. In case any of these characters (e.g. for HTML tags) are required, the encoding can be disabled per FormElement: encode=none (default is specialchar).

During Form load, htmlentities are decoded again.


All $_SERVER vars are htmlentities encoded (all, not only specialchars!) .


Every QFQ Form contains ‘honeypot’-HTML input elements (HTML: hidden & readonly). Which of them to use is configured in configuration (default: ‘username’, ‘password’ and ‘email’). On every start of QFQ (form, report, save, …), these variables are tested if they are non-empty. In such a case a probably malicious bot has send the request and the request will not be processed.

If any of the default configured variable names are needed (which will never be the case for QFQ), an explicit variable name list have to be configured in configuration.

QFQ security restriction:

  • The honeypot variables can’t be used in GET or POST as regular HTML input elements - any values of them will terminate QFQ.


On any violation, QFQ will sleep securityAttackDelaySeconds (configuration) and than exit the running PHP process. A detected attack leads to a complete white (=empty) page.

If securityShowMessage: true (configuration), at least a message is displayed after the delay.

Client Parameter via SIP

Links with URL parameters, targeting to the local website, are typically SIP encoded. Instead of transferring the parameter as part of the URL, only one unique GET parameter ‘s’ is appended at the link. The parameter ‘s’ is unique (equal to a timestamp) for the user. Assigned variables are stored as a part of the PHP user session on the server. Two users might have the same value of parameter ‘s’, but the content is completely independent.

Variables needed by Typo3 remains on the link and are not ‘sip-encoded’.

Secure direct file access

If the application uploads files, mostly it’s not necessary and often a security issue, to offer a direct download of the uploaded files. Best is to create a directory, e.g. <site path>/fileadmin/protected and deny direct access via webbrowser to it. E.g. for Apache set a rule:

<Directory "/var/www/html/fileadmin/protected">
    Require all denied

If you only have access to .htaccess, create a file <site path>/fileadmin/protected/.htaccess with:

<IfModule mod_authz_core.c>
     Require all denied


All QFQ uploads should save files only in/below such a protected directory.

To offer download of those files, use the reserved column name ‘_download’ (see download) or variants.


To protect the installation against executing of uploaded malicious script code, disable PHP for the final upload directory. E.g. fileadmin (Apache):

<Directory "/var/www/html/fileadmin">
  php_admin_flag engine Off

This is in general a good security improvement for directories with user supplied content.

File upload

By default the mime type of every uploaded file is checked against a white list of allowed mime types. The mime type of a file can be (easily) faked by an attacker. This check is good to handle regular user file upload for specific file types but won’t help to prevent attacks against uploading and executing malicious code.

Instead prohibit the execution of user contributed files by the webserver config (SecureDirectFileAccess).

Typo3 Setup - best practice

  • Activate notification emails for every BE login (if there are only few BE users). In case the backend has been hacked, unusual login’s (time or username) will appear:

    [BE][warning_email_addr] = <your email>
    [BE][warning_mode] = 1


Only variables that are known in a specified store can be substituted.

Name Description Content
B Store: BEFORE - B: Record - the current record loaded in the form before any update. All columns of the current record from the current table. See STORE_BEFORE.
C Store: CLIENT - C: POST variable, if not found: GET variable. Parameter sent from the Client (=Browser). See STORE_CLIENT.
D Default values column : The table.column specified default value.  
E Empty - always an empty string, might be helpful if a variable is empty or undefined and will be used in an SQL statement. Any key
F Store: FORM - F: data not saved in database yet. All native FormElements. Recent values from the Browser. See: STORE_FORM
L Store: LDAP - L: Will be filled on demand during processing of a FormElement. Custom specified list of LDAP attributes. See STORE_LDAP.
M Column type: The table.column specified type.  
P Parent record. E.g.: on multi & copy forms the current record of the outer query. All columns of the MultiSQL Statement from the table for the current row
R Store: RECORD - R: Record - the current record loaded in the form. All columns of the current record from the current table. See STORE_RECORD.
S Store: SIP - S: Client parameter ‘s’ will indicate the current SIP, which will be loaded from the SESSION repo to the SIP-Store. sip, r (recordId), form. See STORE_SIP.
T Store: TYPO3 (Bodytext) - T: a) Bodytext (ttcontent record), b) Typo3 internal variables. See Typo3 tt_content record configuration. See STORE_TYPO3.
U Store: USER - U: per user variables, valid as long as the browser session lives. Set via report: ‘…’ AS ‘_=<var name>’ See: STORE_USER, store_user_examples
V Store: VARS - V: Generic variables. See STORE_VARS.
Y Store: SYSTEM - Y: a) Database, b) helper vars for logging/debugging: SYSTEM_SQL_RAW … SYSTEM_FORM_ELEMENT_COLUMN, c) Any custom fields: CONTACT, HELP, … See STORE_SYSTEM.
0 Zero - always value: 0, might be helpful if a variable is empty or undefined and will be used in an SQL statement. Any key
  • Default <prio>: FSRVD - Form / SIP / Record / Vars / Table definition.
  • Hint: Preferable, parameter should be submitted by SIP, not by Client (=URL).
    • Warning: Data submitted via ‘Client’ can be easily spoofed and altered.
    • Best: Data submitted via SIP never leaves the server, cannot be spoofed or altered by the user.
    • SIPs can _only_ be defined by using Report. Inside of Report use columns ‘Link’ (with attribute ‘s’), ‘page?’ or ‘Page?’.

Store: FORM - F

  • Sanitized: yes
  • Represents the values in the form, typically before saving them.
  • Used for:
    • FormElements which will be rerendered, after a parent FormElement has been changed by the user.
    • FormElement actions, before saving the form.
    • 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 FormElement (for ‘pId’): {{pId:F}}

Store: SIP - S

  • Sanitized: no
  • 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 parameter in one string
<user defined> additional user defined link parameter

Store: RECORD - R

  • Sanitized: no
  • Form: Current record.
  • Report: See access-column-values
  • If r=0, all values are empty.
Name Type | Explanation
<column name> Form Name of a column of the primary table (as defined in the current form). Example: {{pId:R}}
<column name> Report Name of a column of a previous fired SQL query. Example: {{pId:R}}
&<column name> Report (final) Name of a column of a previous fired SQL query, typically used by columns with a special-column-names. Final value. Example: ‘{{link:R}}’ returns ‘p:home&form=Person|s|b:success|t:Edit’. Whereas ‘{{&link:R}}’ returns ‘<span class=”btn btn-success”><a href=”?home&s=badcaffee1234”>Edit</a></span>

Store: BEFORE - B

  • Sanitized: no
  • Current record loaded in Form without any modification.
  • If r=0, all values are empty.

This store is handy to compare new and old values of a form.

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 FormElement: {{pId:B}}

Store: CLIENT - C

  • Sanitized: yes
Name Explanation
s =SIP
r record id. Only if specified as GET parameter - typically stored in SIP (=STORE_SIP)
form Name of form to load. Only if specified as GET parameter - typically stored in SIP (=STORE_SIP)
REMOTE_ADDR Client IP address
‘$_SERVER[*]’ All other variables accessible by $_SERVER[]. Only the often used have a pre-defined sanitize class.
Authorization Value of the HTTP Header ‘Authorization’. This is typically not set. Mostly used for authentication of REST requests

Store: TYPO3 (Bodytext) - T

  • Sanitized: no
Name Explanation Note
Formname defined in ttcontent record bodytext
* Fix. E.g. form = person
* via SIP. E.g. form = {{form:SE}}
see note
pageId Record id of current Typo3 page see note
pageAlias Alias of current Typo3 page. If empty, take pageId. see note
pageTitle Title 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  
beUser Logged in Typo3 BE User  
beUserLoggedIn ‘yes’ | ‘no’ - Status if a BE-User is logged in  
  • note: not available:
    • in Dynamic Update or
    • by FormElement class ‘action’ with type ‘beforeSave’, ‘afterSave’, ‘beforeDelete’, ‘afterDelete’.

Store: VARS - V

  • Sanitized: no
Name Explanation
random Random string with length of 32 alphanum chars (lower & upper case). This is variable is always filled.
slaveId see `slaveId`_
  • FormElement ‘upload’:
Name Explanation
filename Original filename of an uploaded file via an ‘upload’-FormElement. Valid only during processing of the current ‘upload’-formElement.
filenameOnly Like filename, but without path.
filenameBase Like filename, but without an optional extension. E.g. filename=’image.png’ comes to filenameBase=’image’
filenameExt Like filename, but only the optional extension. E.g. filename=’image.png’ comes to filenameExt=’png’
fileDestination Destination (path & filename) for an uploaded file. Defined in an ‘upload’-FormElement.parameter. Valid: same as ‘filename’.
fileSize Size of the uploaded file.
mimeType Mime type of the uploaded file.

The directive fillStoreVar will fill the store VARS with custom values. Existing Store VARS values will be merged together with them. E.g.:

fillStoreVar = {{! SELECT, FROM Person AS p WHERE{{pId:S}} }}
  • After filling the store, the values can be retrieved via {{name:V}} and {{email:V}}.
  • Be careful by specifying general purpose variables like id, r, pageId and so on. This might conflict with existing variables.
  • fillStoreVar can be used in form-parameter and fe-parameter-attributes

Store: LDAP - L

  • Sanitized: yes
  • See also LDAP:
Name Explanation
<custom defined> See ldapAttributes

Store: SYSTEM - Y

  • Sanitized: no

See configuration for a list of all settings.

Store: USER - U

  • Sanitized: no

At start of a new browser session (=user calls the website the first time or was logged out before) the store is empty. As soon as a value is set in the store, it remains as long as the browser session is alive (=until user logs out).

Values can be set via report ‘… AS “_=<var name>”’

See also: store_user_examples


A form can retrieve data from LDAP server(s) to display or to save them. Configuration options for LDAP will be specified in the parameter field of the Form and/or the FormElement. Definitions of the FormElement will overwrite definitions of the Form. One LDAP Server can be configured per FormElement. Multiple FormElements might use individual LDAP Server configurations.

To decide which Parameter should be placed on Form.parameter and which on FormElement.parameter: If LDAP access is …

  • only necessary in one FormElement, most useful setup is to specify all values in that specific FormElement,
  • needed on multiple FormElement*s (of the same *Form, e.g. one input with typeAhead, one note and one action), it’s more efficient to specify the base parameter ldapServer, ldapBaseDn in Form.parameter and the rest on the current FormElement.
Parameter Example Description Form FormElement Used for
ldapServer Hostname. For LDAPS: ldaps:// x x TA, FSL
ldapBaseDn ou=Addressbook,dc=example,dc=com Base DN to start the search x x TA, FSL
ldapAttributes cn, email List of attributes to save in STORE_LDAP x x FSL
ldapSearch ( Regular LDAP search expression x x FSL
ldapTimeLimit 3 (default) Maximum time to wait for an answer of the LDAP Server x x TA, FSL
ldapUseBindCredentials ldapUseBindCredentials=1 Use LDAP_1_* credentials from config-qfq-php for ldap_bind() x x TA, FSL
typeAheadLdap   Enable LDAP as ‘Typeahead’ data source   x TA
typeAheadLdapSearch (|(cn=*?*)(mail=*?*)) Regular LDAP search expression, returns upto typeAheadLimit x x TA
typeAheadLdapSearchPrefetch (mail=?) Regular LDAP search expression, typically return one record x x TA
Split search value in token and OR-combine every search with
the individual tokens
x x TA
typeAheadLdapValuePrintf ‘%s / %s’, cn, mail Custom format to display attributes, as value x x TA
typeAheadLdapIdPrintf ‘%s’, mail Custom format to display attributes, as id x x TA
typeAheadLimit 20 (default) Result will be limited to this number of entries x x TA
typeAheadPedantic typeAheadPedantic=0 Turn off ‘pedantic’ mode - allow any values (see below) x x TA
typeAheadMinLength 2 (default) Minimum number of characters before starting the search x x TA
fillStoreLdap   Activate Fill STORE LDAP with the first retrieved record   x FSL
  • typeAheadLimit: there might be a hard limit on the server side (e.g. 100) - which can’t be extended.
  • ldapUseBindCredentials is only necessary if anonymous access is not possible. RDN and password has to be configured in config-qfq-php.

Typeahead (TA)

Typeahead offers continous searching of a LDAP directoy by using a regular FormElement of type text. The FormElement.parameter*=*typeAheadLdap will trigger LDAP searches on every user keystroke (starting after typeAheadMinLength keystrokes) for the current FormElement - this is different from dynamicUpdate (triggered by leaving focus of an input element). Typeahead delivers a list of elements.

  • FormElement.parameter.typeAheadLdap - activate the mode Typeahead - no value is needed, the existence is suffucient.
  • Form.parameter or FormElement.parameter:
    • ldapServer =
    • ldapBaseDn = ou=Addressbook,dc=example,dc=com
    • typeAheadLdapSearch = (|(cn=*?*)(mail=*?*))
    • typeAheadLdapValuePrintf = ‘%s / %s’, cn, email
    • typeAheadLdapIdPrintf = ‘%s’, email
    • Optional: ldapUseBindCredentials = 1

All fetched LDAP values will be formatted with:

  • typeAheadLdapValuePrintf, shown to the user in a drop-down box and
  • typeAheadLdapIdPrintf, which represents the final data to save.

The id/value translation is compareable to a regular select drop-down box with id/value pairs. Only attributes, defined in typeAheadLdapValuePrintf / typeAheadLdapIdPrintf will be fetched from the LDAP directory. To examine all possible values of an LDAP server, use the commandline tool ldapsearch. E.g.:

ldapsearch -x -h -L -b ou=Addressbook,dc=example,dc=com "("

All occurrences of a ‘?’ in ldapSearch will be replaced by the user data typed in via the text-FormElement. The typed data will be escaped to fulfill LDAP search limitations. Regular Form variables might be used on all parameter and will be evaluated during form load (!) - not at the time when the user types something.


The typeAheadPedantic mode ensures that the typed value (technically this is the value of the id, latest in the moment when loosing the focus) is valid (= exists on the LDAP server or is defined in typeAheadSql). If the user typed something and that is not a valid id, the client (=browser) will delete the input when losing the focus. To identify the exact id, an additional search filter is necessary: typeAheadLdapSearchPrefetch - see next topic.

typeAheadPedantic is active by default when typeAheadLdap or typeAheadSql is defined, but can be turned off with typeAheadPedantic=0.

  • Form.parameter or FormElement.parameter:
    • typeAheadPedantic=0


After ‘form load’ with an existing record, the user expects to see the previous saved data. In case there is an id to value translation, the value does not exist in the database, instead it has to be fetched again dynamically. A precise LDAP or SQL query has to be defined to force this:

  • Form.parameter or FormElement.parameter:
    • typeAheadLdapSearchPrefetch = (mail=?)
    • typeAheadSqlPrefetch = SELECT firstName, ‘ ‘, lastName FROM person WHERE id = ?

This situation also applies in pedantic mode to verify the user input after each change.


Sometimes a LDAP server only provides attributes like ‘sn’ and ‘givenName’, but not ‘displayName’ or another practial combination of multiple attributes - than it is difficult to search for ‘firstname’ and (=human AND) ‘lastname’. E.g. ‘John Doe’, results to search like (|(sn=*John Doe*)(givenName=*John Doe*)) which will be probably always be empty. Instead, the user input has to be split in token and the search string has to repeated for every token.

  • Form.parameter or FormElement.parameter:
    • typeAheadLdapSearchPerToken - no value needed.

This will repeat the search string per token.


User search string: X Y
Ldap search string: (|(a=*?*)(b=*?*))

Result: (& (|(a=*X*)(b=*X*)) (|(a=*Y*)(b=*Y*))

Attention: this option is only useful in specific environments. Only use it, if it is really needed. The query becomes much more cpu / IO intensive.


Before processing a FormElement, an optional configured FSL-action loads one record from a LDAP directory and stores the named attributes in STORE_LDAP. If the LDAP search query selects more than one record, only the first record is processed. The attributes names always becomes lowercase (PHP implentation detail on get_ldap_entries()) in the store. To make accessing STORE_LDAP easily, the keys are implemented case insensitive for this specific store. FLS is triggered during Form-…

  • load,
  • dynamic update,
  • save.

The FLS happens before the FormElement processing starts. Therefore the fetched LDAP data (specified by ldapAttributes), are available via {{<attributename>:L:allbut:s}} during the regular FormElement processing. Take care to specify a sanitize class and optional escaping on further processing of those data.

Also, the STORE LDAP remains filled, during the whole form processing time. E.g. if the values are needed for a person name and email, it’s sufficient to fire one FSL on the first FormElement Action element, and use the same values during further FormElement Action Elements.


LDAP access might slow down the Form processing on load, update or save! The timeout (default: 3 seconds) have to be multiplied by the number of accesses. E.g. a broken LDAP connection and 3 FormElements with FSL results to 9 seconds delay on save. Also be prepared not to receive the expected data.

  • FormElement.parameter.fillStoreLdap - activate the mode Fill S - no value is needed, the existence is sufficient.
  • Form.parameter or FormElement.parameter:
    • ldapServer =
    • ldapBaseDn = ou=Addressbook,dc=example,dc=com
    • typeAheadLdapSearch = (|(cn=*?*)(mail=*?*))
    • ldapAttributes = givenName, sn, telephoneNumber, email
    • ldapSearch = (mail={{email:F0:alnumx:l}})
    • Optional: ldapUseBindCredentials = 1

After filling the store, access the content via {{<attributename>:L:allbut:s}}.



  • Forms will be created by using the Form Editor on the Typo3 frontend (HTML form).

  • The Form editor itself consist of two predefined QFQ forms: form and formElement - these forms are often updated during the installation of new QFQ versions.

  • 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.

  • Forms 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 (only primary record) automatically.
    • 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 action/afterSave records.
    • Multi form: (multi-form) the form acts simultaneously on more than one record. All records use the same FormElements.
      • The FormElements are defined as a regular simple / or advanced form, plus an SQL Query, which selects and iterates over all records. Those records will be loaded at the same time.
    • Delete form: any form can be used as a form to delete-record.
  • Form mode: The parameter ‘r’ (given via URL or via SIP) decide if the form is in mode:

    • New:

      • Missing parameter ‘r’ or ‘r=0’
      • On form load, no record is displayed.
      • Saving the form creates a new primary record.
      • E.g.: or
    • Edit:

      • Parameter ‘r>0’
      • On form load, the specified record (<tablename>.id= <r>) will be loaded and displayed.
      • Saving the form will update the existing record.
      • E.g.:
    • Providing additional parameter:

      Often, it is necessary to store additional, for the user not visible, parameter in a record. See form-magic.

  • Display a form:

    • Create a QFQ tt_content record on a Typo 3 page.
    • Inside the QFQ record: form = <formname>. E.g.:
      • Static: form = Person
      • Dynamic: form = {{form:SE}} (the left form is a keyword for QFQ, the right form is a free chooseable variable name)
    • With the Dynamic option, it’s easily possible to use one Typo3 page and display different forms on that specific page. This is nice to configure few Typo 3 pages. The disadvantage is that the user might loose the navigation.

Record locking

Forms and ‘record delete’-function support basic record locking. A user opens a form: starting with the first change of content, a record lock will be acquired in the background. If the lock is denied (e.g. another user already owns a lock on the record) an alert is shown. By default the record lock mode is ‘exclusive’ and the default timeout is 15 minutes. Both can be configured per form. The general timeout can also be configured in configuration (will be copied to the form during creating the form).

The lock timeout counts from the first change, not from the last modification on the form.

If a timeout expires, the lock becomes invalid. During the next change in a form, the lock is acquired again.

A lock is assigned to a record of a table. Multiple forms, with the same primary table, uses the same lock for a given record.

If a Form acts on further records (e.g. via FE action), those records are not protected by this basic record locking.

If a user tries to delete a record and another user already owns a lock on that record, the delete action is denied.

If there are different locking modes in multiple forms, the most restricting mode applies for the current lock.


An existing lock on a record forbids any write action on that record.


An existing lock on a record informs the user that another user is currently working on that record. Nevertheless, writing is allowed.


No locking at all.

Comment- and space-character

The following applies to the fields Form.parameter and FormElement.parameter:

  • Lines will be trimmed - leading and trailing spaces will be removed.
  • If a leading and/or trailing space is needed, escape it: \ hello world \ > ‘ hello world ‘.
  • Lines starting with a ‘#’ are treated as a comment and will not be parsed. Such lines are treated as ‘empty lines’.
  • The comment sign can be escaped with \ .


Name Description
Name Unique and speaking name of the Form. Form will be identified by this name. Use only ‘[a-zA-Z0-9._+-]’. form-name
Title Title, shown on/above the form. form-title
Note Personal editor notes. form-note
Table Primary table of the form. form-tablename
Primary Key Primary key of the indicated table. Only needed if != ‘id’. form-primary-key
Required Parameter NEW Name of required SIP parameter to create a new record (r=0), separated by comma. ‘#’ as comment delimiter. See form-requiredParameter
Required Parameter EDIT Name of required SIP parameter to edit an existing record (r>0), separated by comma. ‘#’ as comment delimiter. See form-requiredParameter
Permit New ‘sip, logged_in, logged_out, always, never’ (Default: sip): See form-permitNewEdit
Permit Edit ‘sip, logged_in, logged_out, always, never’ (Default: sip): See form-permitNewEdit
Escape Type Default See variable-escape.
Show button ‘new, delete, close, save’ (Default: ‘new,delete,close,save’): Shown named buttons in the upper right corner of the form. See form-showButton
Forward Mode ‘auto | close | no | url | url-skip-history | url-sip | url-sip-skip-history’ (Default: auto): See form-forward.
Forward (Mode) Page
  1. URL / Typo3 page id/alias or b) Forward Mode (via ‘{{…}}’) or combination of a) & b). See form-forward.
labelAlign Label align (default/left/center/right)/ Default: ‘default’ (defined by Config).
Parameter Misc additional parameters. See form-parameter.
BS Label Columns 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 for all FormElements. Exceptions per FormElement can be specified per FormElement. Default: label=col-md-3, input=col-md-6, note=col-md-3. See form-layout.
BS Input Columns
BS Note Columns
multiMode NOT IMPLEMENTED - ‘none, horizontal, vertical’ (Default ‘none’)
multiSql NOT IMPLEMENTED - Optional. SQL Query which selects all records to edit.
multiDetailForm NOT IMPLEMENTED - Optional. Form to open, if a record is selected to edit (double click on record line)
multiDetailFormParameter NOT IMPLEMENTED - Optional. Translated Parameter submitted to detailform (like subrecord parameter)

permitNew & permitEdit

Depending on r, the following access permission will be taken:

  • r=0 - permitNew
  • r>0 - permitEdit
Option Description
sip The parameter ‘form’ and ‘r’ must be supplied via SIP or hard coded in the QFQ tt_content record.
logged_in The form will only be shown if the current User is logged in as a FE User
logged_out The form will only be shown if the current User is not logged in as a FE User
always No access restriction, the form will always be shown
never The form will never be shown
  • sip
    • is always the preferred way. With ‘sip’ it’s not necessary to differ between logged in or not, cause the SIP only exist and is only valid, if it’s created via QFQ/report earlier. This means ‘creating’ the SIP implies ‘access granted’. The grant will be revoked when the QFQ session is destroyed - this happens when a user loggs out or the web browser is closed.
  • logged_in / logged_out: for forms which might be displayed without a SIP, but maybe on a protected or even unprotected page. The option is probably not often used.
  • always: such a form is always allowed to be loaded.
    • permitNew=always: Public accessible forms (e.g. for registration) will allow users to fill and save the form.
    • permitEdit=always: Public accessible forms will allow users to update existing data. This is dangerous, cause the URL parameter (recordId) ‘r’ might be changed by the user (URL manipulating) and therefore the user might see and/or change data from other users. The option is probably not often used.
  • never: such a form is not allowed to be loaded.
    • permitNew=never: Public accessible forms. It’s not possible to create new records.
    • permitEdit=none: Public accessible forms. It’s not possible to update records.

Required Parameter New|Edit

Comma separated list of variable names. On form load, an error message will be shown in case of missing parameters. The parameters must be given by SIP.

The list of required parameter has to be defined for New (r=0, create a new record) and for Edit (r>0, edit existing record).

Optional a comment might be attached after the parameter definition.


New: grId, pId # Always specify a person, grId2
Edit: pId


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 (=browser), hide this ‘new’ button - the necessary parameter are not provided.
  • delete: This either deletes the current record only, or (if defined via action FormElement ‘beforeDelete’ ) 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.

Forward: Save / Close

Forward (=forwardMode)

After the user presses Save, Close, Delete or New, different actions are possible where the browser redirects to.

  • auto (default) - the QFQ browser Javascript logic, decides to stay on the page or to force a redirection to a previous page. The decision depends on:
    • Close goes back (feels like close) to the previous page. Note: if there is no history, QFQ won’t close the tab,
      instead a message is shown.
    • Save stays on the current page.
  • close - goes back (feels like close) to the previous page. Note: if there is no history, QFQ won’t close the tab,
    instead a message is shown.
  • no - no change, the browser remains on the current side. Close does not close the page. It just triggers a save if there are modified data.
  • url - the browser redirects to the URL or T3 page named in Forward URL / Page. Independent if the user presses save or close.
  • url-skip-history - same as url, but the current location won’t saved in the browser history.
  • url-sip - like url, but any given parameter will be SIP encoded. Only useful if url points to current web instance.
  • url-sip-skip-history - like url-sip, but skips the Browser history.

Only with Forward == url | url-skip-history, the definition of Forward URL / Page becomes active.

Forward URL / Page (=forwardPage)

Format: [<url>] or [<mode>|<url>]

  • <url>:
    • website.html?a=123#bottom
    • ?id=<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
    • {{SELECT …}}
    • <mode>|<url>
  • <mode> - Valid keywords are as above: auto|close|no|url|url-skip-history|url-sip|url-sip-skip-history

Specifying the mode in forwardPage overwrites formMode (but only if formMode is url…).

Also regular QFQ statements like {{var}} or {{SELECT …}} are possible in forwardPage. This is useful to dynamically redirect to different targets, depending on user input or any other dependencies.

If a forwardMode ‘url…’ is specified and there is no forwardPage, QFQ falls down to auto mode.

On a form, the user might click ‘save’ or ‘save,close’ or ‘close’ (with modified data this leads to ‘save,close’). The CLIENT submit_reason shows the user action:

  • {{submit_reason:CE:alnumx}} = save or save,close

Example forwardPage

  • {{SELECT IF(‘{{formModeGlobal:S}}’=’requiredOff’, ‘no’, ‘auto’) }}
  • {{SELECT IF(‘{{submit_reason:CE:alnumx}}’=’save’, ‘no’, ‘url’), ‘|’ }}

Type: combined dynamic mode & URL/page

Syntax: forwardPage=<mode>|<page>

  • forwardPage={{SELECT IF(a.url=’’,’no’,’url’), ‘|’, a.url FROM address AS a }}


  • The following parameter are optional and can be configured in the Form.parameter field.
  • Example:
    • maxVisiblePill = 5
    • class = container-fluid
    • classBody = qfq-form-right


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.

  • Optional.
  • Default: Empty


  • 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.


  • 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 elements of type ‘Pill’.


  • 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:
    • adjust the background color of the FormElement area.
    • 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)


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’ can be specified. If the user opens 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’

See also: delete-record.

Form mode global

The Form global mode mode is given by default with {{formModeGlobal:SE:alnumx}}.

Optional it might be defined via Form.parameter

  • readonly: all FormElement of the whole form are temporarily in readonly mode. This is a fast way to use an existing Form just to display the form data, without a possibility for the user to change any data of the form.
  • requiredOff: all FormElement of the whole, with mode=required, will temporarily switch to mode=show. In this mode, the user might save the form without providing all necessary data. Later on, when application logic requires a final submit, this mode is not used any longer (call the form as regular without the ‘formModeGlobal’ parameter) and the form can only be saved with all data given. Than, e.g. an action-FormElement ‘afterSave’ can be used to detect the final submit situation and do some extra stuff, necessary for the final submit.

The following shows the same Form in the regular, readonly and requiredOff mode:

10.sql = SELECT CONCAT('p:{{pageAlias:T}}&form=person&r=',, '|Regular') as _pagee,
                CONCAT('p:{{pageAlias:T}}&form=person&formModeGlobal=readonly&r=',, '|Readonly') as _pagee,
                CONCAT('p:{{pageAlias:T}}&form=person&formModeGlobal=requiredOff&r=',, '|Required off') as _pagee
                FROM Person AS p


  • Each form contains one or more FormElement.
  • The FormElements are divided in three categories:
  • Ordering and grouping: Native FormElements and Container-Elements (both with feIdContainer=0) will be ordered by ‘ord’.
  • Inside of a container, all nested elements will be displayed.
  • Technical, it’s not necessary to configure a FormElement for the primary index column id.
  • Additional options to a FormElement will be configured via the FormElement.parameter field (analog to Form.parameter for Forms ).

Class: Container

  • Pills are containers for ‘fieldset’ and / or ‘native’ FormElements.
  • Fieldsets are containers for ‘native’ FormElements.
  • TemplateGroups are containers for ‘fieldset’ and / or ‘native’ FormElements.

Type: fieldset

  • Native FormElements can be assigned to a fieldset.
  • FormElement settings:
    • name: technical name, used as HTML identifier.
    • label: Shown title of the fieldset.

Type: pill (tab)

  • Pill is synonymous for a tab and looks like a tab.
  • If there is at least one pill defined:
    • every native FormElement needs to be assigned to a pill or to a fieldset.
    • every fieldset needs to be assigned to a pill.
  • Mode:
    • show: all child elements will be shown.
    • required: same as ‘show’. This mode has no other meaning than ‘show’.
    • readonly: technical it’s like HTML/CSS disabled.
      • The pill title is shown, but not clickable.
      • The FormElements on the pill still exist, but are not reachable for the user via UI.
    • hidden:
      • The pill is invisible.
      • The FormElements on the pill still exist, but are not reachable for the user via UI.
    • Note: Independent of the mode, all child elements are always rendered and processed by the client/server.
  • Pills are ‘dynamicUpdate’ aware. title and mode are optional recalculated during ‘dynamicUpdate’.
  • FormElement settings:
    • name: technical name, used as HTML identifier.
    • label: Label shown on the corresponding pill button or inside the drop-down menu.
    • mode:
      • show, required: regular mode. The pill will be shown.
      • readonly: the pill and it’s name is visible, but not clickable.
      • hidden: the pill is not shown at all.
    • modeSql:
    • type: pill
    • feIdContainer: 0 - Pill’s can’t be nested.
    • tooltip: Optional tooltip on hover. Especially helpful if the pill is set to readonly.
    • parameter:
      • maxVisiblePill: <nr> - Number of Pill-Buttons shown. Undefined means unlimited. Excess Pill buttons will be displayed as a drop-down menu.

Type: templateGroup

TemplateGroups will be used to create a series of grouped (by the given templateGroup) FormElements.

FormElements can be assigned to a templateGroup. These templateGroup will be rendered upto n-times. On ‘form load’ only a single (=first) copy of the templateGroup will be shown. Below the last copy of the templateGroup an ‘add’-button is shown. If the user click on it, an additional copy of the templateGroup is displayed. This can be repeated up to templateGroup.maxLength times. Also, the user can ‘remove’ previously created copies by clicking on a remove button near beside every templateGroup. The first copy of a templateGroup can’t be removed.

  • FormElement settings:
    • label: Shown in the FormElement-editor container field.
    • maxLength: Maximum number of copies of the current templateGroup. Default: 5.
    • bsLabelColumn, bsInputColumn, bsNoteColumn: column widths for row with the ‘Add’ button.
    • parameter:
      • tgAddClass: Class of the ‘add’ button. Default: btn btn-default.
      • tgAddText: Text shown on the button. Default: Add.
      • tgRemoveClass: Class of the ‘remove’ button. Default: btn btn-default.
      • tgRemoveText: Text shown on the button. Default: Remove.
      • tgClass: Class wrapped around every copy of the templateGroup. E.g. the class qfq-child-margin-top adds a margin between two copies of the templateGroup. Default: empty

Multiple templateGroups per form are allowed.

The name of the native FormElements, inside the templateGroup, which represents the effective table columns, uses the placeholder %d. E.g. the columns grade1, grade2, grade3 needs a = grade%d. The counting will always start with 1. The placeholder %d can also be used in the FormElement.label

Example of styling the Add/ Delete Button: Icons Template Group

Column: primary record

If the columns <name>%d are real columns on the primary table, saving and delete (=empty string) are done automatically. E.g. if there are up to five elements grade1, …, grade5 and the user inputs only the first three, the remaining will be set to an empty string.

Column: non primary record

Additional logic is required to load, update, insert and/or delete slave records.


On each native FormElement of the templateGroup define an SQL query in the FormElement.value field. The query has to select all values of all possible existing copies of the FormElement - therefore the exclamation mark is necessary. Also define the order. E.g. FormElement.value:

{{!SELECT street FROM Address WHERE personId={{id}} ORDER BY id}}
Insert / Update / Delete

Add an action record, type=’afterSave’, and assign the record to the given templateGroup.

In the parameter field define:

slaveId = {{SELECT id FROM Address WHERE personId={{id}} ORDER BY id LIMIT %D,1}}
sqlHonorFormElements = city%d, street%d
sqlUpdate = {{UPDATE Address SET city='{{city%d:FE:alnumx:s}}', street='{{street%d:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1}}
sqlInsert = {{INSERT INTO Address (`personId`, `city`, `street`) VALUES ({{id}}, '{{city%d:FE:alnumx:s}}' , '{{street%d:FE:alnumx:s}}') }}
sqlDelete = {{DELETE FROM Address WHERE id={{slaveId}} LIMIT 1}}

The slaveId needs attention: the placeholder %d starts always at 1. The LIMIT directive starts at 0 - therefore use %D instead of %d, cause %D is always one below %d - but can only be used on the action element.

Class: Native


FE: Value

By default this field is empty: QFQ will fill it with the corresponding existing column value on form load. For a customized default value define:

{{SELECT IF('{{column:RE}}'='','custom default', '{{column:R}}') }}

For non primary records, this is the place to load an existing value. E.g. we’re on a ‘Person’ detail form and would like to edit, on the same form, a corresponding person email address (which is in a separate table):

{{SELECT FROM Address AS a WHERE a.pId={{id:R0}} ORDER BY LIMIT 1}}

Report syntax can also be used, see report-notation.

FE: ‘Report’ notation

The FE fields ‘value’ and ‘note’ understand the Report syntax. Nested SQL queries as well as links with SIP encoding are possible. To distinguish between ‘Form’ and ‘Report’ syntax, the first line has to be #!report:


10.sql = SELECT ...

20 {
  sql = SELECT ...
  5.sql = SELECT ...

Attributes defined in the parameter field

See also at specific FormElement definitions.

Name Type Note
acceptZeroAsRequired string 0|1 - Accept a ‘0’ as a valid input. Default ‘0’ (=0 is not a valid input)
autofocus string See input-option-autofocus
capture, accept, maxFileSize, fileDestination, fileTrash, fileTrashText, fileReplace, autoOrient, autoOrientCmd, autoOrientMimeType, chmodFile, chmodDir, slaveId, sqlBefore, sqlInsert, sqlUpdate, sqlDelete, sqlAfter, importToTable, importToColumns, importRegion, importMode, importType, importNamedSheetsOnly, importSetReadDataOnly, importListSheetNames, string See input-upload
checkBoxMode checked unchecked label2 itemList emptyHide emptyItemAtStart emptyItemAtEnd buttonClass string See input-checkbox, input-radio, input-select
dateFormat string yyyy-mm-dd /
data-pattern-error string Pattern violation: Text for error message
data-required-error string Required violation: Text for error message
data-match-error string Match violation: Text for error message
data-error string Violation of ‘check-type’: Text for error message
decimalFormat string [precision,scale] Limits and formats input to a decimal number with the specified precision and scale. If no precision and scale are specified, the decimal format is pulled from the table definition.
htmlAfter string HTML Code wrapped after the complete FormElement
htmlBefore string HTML Code wrapped before the complete FormElement
extraButtonLock none [0|1] Show a ‘lock’ on the right side of the input element. See extraButtonLock
extraButtonPassword none [0|1] Show an ‘eye’ on the right side of the input element. See extraButtonPassword
extraButtonInfo string Text. Show an ‘i’ on the right side of the input element. See extraButtonInfo
extraButtonInfoClass string By default empty. Specify any class to be assigned to wrap extraButtonInfo
editor-plugins, editor-toolbar, editor-statusbar, string See input-editor
fileButtonText string Overwrite default ‘Choose File’
fillStoreVar string Fill the STORE_VAR with custom values. See STORE_VARS.
form, page, title, extraDeleteForm, detail, subrecordTableClass, string See subrecord-option
min s/d/n

Minimum and/or maximum allowed values for input field. Can be used for numbers, dates, or strings.

Always use the international format ‘yyyy-mm-dd[ hh:mm[:ss]]

max s/d/n
processReadOnly [n] [0|1] By default FE’s with type=’readonly’ are not processed during ‘save’. This option forces to process them during ‘save’ as well.
retype, retypeLabel, retypeNote, characterCountWrap, hideZero, emptyMeansNull, string See input-text
showSeconds string 0|1 - Shows the seconds on form load. Default: 0
showZero string 0|1 - Empty timestamp: ‘0’(default) - nothing shown, ‘1’ - the string ‘0000-00-00 00:00:00’ is displayed
timeIsOptional string 0|1 - Used for datetime input. 0 (default): Time is required - 1: Entering a time is optional (defaults to 00:00:00 if none entered).
typeAheadLimit, typeAheadMinLength, typeAheadSql, typeAheadSqlPrefetch, string See input-typeahead
wrapRow string If specified, skip default wrapping (<div class=’col-md-?’>). Instead the given string is used.
wrapLabel string
wrapInput string
wrapNote string
trim string By default, whitespace is trimmed. To disable, use ‘trim=none’. You can also specify custom trim characters: ‘trim=\ ‘ only trims spaces.
sqlValidate string See sqlValidate
expectRecords int
messageFail string
dataReference string Optional. See applicationTest
requiredPosition int See requiredPosition .
  • s/d/n: string or date or number.

slaveId, sqlBefore, sqlAfter, …

See slave-id

Native FormElements

  • Like ‘input’, ‘checkbox’, …

The first FormElement with this attribute will get the focus after form load. If there is no such attribute given to any FormElement, the attribute will be automatically assigned to the first editable FormElement.

To disable ‘autofocus’ on a form, set ‘autofocus=0’ on the first editable FormElement.

Note: If there are multiple pills defined on a form, only the first pill will be set with ‘autofocus’.

  • The user has to click on the lock, before it’s possible to change the value. This will protect data against unwanted modification.
  • After Form load, the value is shown, but not editable.
  • Shows a ‘lock’ on the right side of an input element of type text, date, time or datetime.
  • This option is not available for FormElements with mode=readonly.
  • There is no value needed for this parameter.
  • The user has to click on the eye (unhide) to see the value.
  • After Form load, the data is hided by asteriks.
  • Shows an ‘eye’ on the right side of an input element of type text, date, time or datetime.
  • There is no value needed for this parameter.
  • After Form load, the info button/icon is shown but the information message is hidden.
  • The user has to click on the info button/icon to see an additional message.
  • The value of this parameter is the text shown to the user.
  • Shows an info button/icon, depending of extraButtonInfoPosition in configuration
    • auto, depending on FormElement type:
      • on the right side of an input element for type text, date, time or datetime,
      • below the FormElement for all other types.
    • below: below the FormElement for all types.
  • For FormElement with mode below, a span element with the given class in extraButtonInfoClass (FE, F, configuration) will be applied. E.g. this might be pull-right to align the info button/icon on the right side below the input element.

Required Position

By default, input elements with Mode=required will be displayed with a ‘red asterix’ right beside the label. The position of the ‘red asterix’ can be choosen via the parameter field:

requiredPosition = label-left|label-right|input-left|input-right|note-left|note-right

The default is ‘label-right’.

The definition can be set per Form (=affects all FormElements) or per FormElement.

Type: checkbox

Checkboxes can be rendered in mode:

  • single:

    • One column in a table corresponds to one checkbox.
    • The value for statuses checked and unchecked are free to choose.
    • This mode is selected, if a) checkBoxMode = single, or b) checkBoxMode is missing and the number of fields of the column definition is <3.
    • FormElement.parameter:
      • checkBoxMode = single (optional)
      • checked = <value> (optional, the value which represents ‘checked’)
        • If checked is empty or missing: If type = ‘enum’ or ‘set’, get first item of the definition. If type = string, get default.
      • unchecked = <value> (optional, the value which represents ‘unchecked’)
        • If unchecked is empty or missing: If type = ‘enum’ or ‘set’, get second item of checked. If type = ‘string’, get ‘’.
      • label2 = <value> (Text right beside checkbox) (optional)
  • multi:

    • One column in a table represents multiple checkboxes. This is typically useful for the column type set.

    • The value for status checked are free to choose, the value for status unchecked is always the empty string.

    • Each field key (or the corresponding value from the key/value pair) will be rendered right beside the checkbox.

    • FormElement.parameter

      • checkBoxMode = multi
      • itemList - E.g.:
        • itemList=red,blue,orange
        • itemList=1:red,2:blue,3:orange
        • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange`
    • FormElement.sql1 = {{!SELECT id, value FROM someTable}}

    • FormElement.maxlength - vertical or horizontal alignment:

      • Value: ‘’, 0, 1 - The check boxes will be aligned vertical.
      • Value: >1 - The check boxes will be aligned horizontal, with a linebreak every ‘value’ elements.
  • FormElement.parameter:

    • emptyHide: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty entry, but the empty value should not be selectable.

    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.

    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.

    • buttonClass: Instead of the plain HTML checkbox fields, Bootstrap buttons. are rendered as checkbox elements. Use one of the following classes:

      • btn-default (default, grey),
      • btn-primary (blue),
      • btn-success (green),
      • btn-info (light blue),
      • btn-warning (orange),
      • btn-danger (red).

      With a given buttonClass, all buttons (=radios) are rendered horizontal. A value in FormElement.maxlength has no effect.

  • No preselection:

    • If a form is in ‘new’ mode and if there is a default value configured on a table column, such a value is shown by default. There might be situations, where the user should be forced to select a value (e.g. specifying the gender). An unwanted default value can be suppressed by specifying an explicit definition on the FormElement field value:


      For existing records the shown value is as expected the value of the record. For new records, it’s the value 0, which is typically not one of the ENUM / SET values and therefore nothing is selected.

Type: date

Type: datetime

  • Range datetime: ‘1000-01-01 00:00:00’ to ‘9999-12-31 23:59:59’ or ‘0000-00-00 00:00:00’. (
  • Optional:
    • FormElement.parameter:
      • dateFormat = yyyy-mm-dd |
      • 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: extra

  • The element behaves like, and can be used as, a HTML hidden input element - with the difference & advantage, that the element never leaves the server and therefore can’t be manipulated by a user.
  • The following names are reserved and can’t be used to name ‘extra’ FormElements: ‘id’, ‘type’, ‘L’.
  • The element is not transferred to the the browser.
  • The element can be used to define / pre calculate values for a column, which do not already exist as a native FormElement.
  • The element is build / computed on form load and saved alongside with the SIP parameter of the current form.
  • The element is not recalculated during save - it’s stored during ‘Form Load’ inside the current form SIP handle.
  • Access the value without specifying any store (default store priority is sufficient).

Type: text

General input for any text.

  • By default, the maximum length of input data is automatically restricted by the underlying database column.
  • HTML decides between one line input (=Input text) and multiline input (=Textarea).
  • FormElement.size = [<width>[,<min height (lines)>[,<max height (pixel)>]]]
    • The parameter is optional and controls the behaviour of the input / textarea element.
    • The <width> is counted in ‘characters’.
      • But: the visible width of an input element is defined by the Bootstrap column width (and not the width given here). Finally: the value here is meaningless. Nevertheless it has to be given for future compatibility.
    • <min-height>:
      • Counted as ‘lines’.
      • If not set the height is treated as 1.
      • A <min-height> of 1 forces an one line input. Exception: <max-height> > 0 enables auto-grow.
    • <max-height>:
      • Controls the auto-grow-Mode.
      • Counted in ‘pixel’.
      • If not set it becomes the default of 350 pixels.
      • If > 0, the auto-grow mode is activated and the height of the textarea will be dynamically updated up to <max-height>.
      • If = 0, the auto-grow mode is disabled.
  • FormElement.parameter:
    • retype = 1 (optional): Current input element will be rendered twice. The form can only submitted if both elements are equal.
      • retypeLabel = <text> (optional): The label of the second element.
      • retypeNote = <text> (optional): The note of the second element.
    • characterCountWrap = <span class=”qfq-cc-style”>Count: | </span> (optional). Displays a character counter below the input/textarea element.
    • Also check the fe-parameter-attributes data-…-error to customize error messages shown by the validator.
    • hideZero = 0|1 (optional): with hideZero=1 a ‘0’ in the value will be replaced by an empty string.
    • emptyMeansNull = [0|1] (optional): with emptyMeansNull or emptyMeansNull=1 a NULL value will be written if the value is an empty string
    • inputType = number (optional). Typically the HTML tag ‘type’ will be ‘text’, ‘textarea’ or ‘number’ (detected automatically). If necessary, the HTML tag ‘type’ might be forced to a specific given value.
    • step = Step size of the up/down buttons which increase/decrease the number of in the input field. Optional. Default 1. Only useful with inputType=number (defined explicit via inputType or detected automatically).
    • textareaResize = 0|1 (optional). Be default = 1 (=on). A textarea element is resizable by the user.

Type Ahead

Activating typeahead functionality offers an instant lookup of data and displaying them to the user, while the user is typing, a drop-down box offers the results. As datasource the regular SQL connection or a LDAP query can be used. With every keystroke (starting from the typeAheadMinLength characters), the already typed value will be transmitted to the server, the lookup will be performed and the result, upto typeAheadLimit entries, are displayed as a drop-down box.

  • FormElement.parameter:
    • typeAheadLimit = <number>. Max numbers of result records to be shown. Default is 20.
    • typeAheadMinLength = <number>. Minimum length to type before the first lookup starts. Default is 2.

Depending of the typeahead setup, the given FormElement will contain the displayed value or id (if an id/value dict is configured).

Configuration via Form / FormElement

All of the typeAhead* (except typeAheadLdap) and ldap* parameter can be specified either in Form.parameter or in FormElement.parameter.

  • FormElement.parameter:
    • typeAheadSql = SELECT … AS ‘id’, … AS ‘value’ WHERE name LIKE ? OR firstName LIKE ? LIMIT 100
      • If there is only one column in the SELECT statement, that one will be used and there is no dict (key/value pair).
      • If there is no column id or no column value, then the first column becomes id and the second column becomes value.
      • The query will be fired as a ‘prepared statement’.
      • The value, typed by the user, will be replaced on all places where a ? appears.
      • All ? will be automatically surrounded by ‘%’. Therefore wildcard search is implemented: … LIKE ‘%<?>%’ …
    • typeAheadSqlPrefetch = SELECT firstName, ‘ ‘, lastName FROM person WHERE id = ?
      • If the query returns several results, only the first one is returned and displayed.
      • If the query selects multiple columns, the columns are concatenated.

See Typeahead (TA)

Type: editor

  • TinyMCE (, 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:

  • 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 in FormElement.parameter is:

    editor-plugins=code link lists 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
  • To deactivate the surrouding <p> tag, configure in FormElement.parameter:

    editor-forced_root_block = false

    This might have impacts on the editor. See

  • Set ‘extended_valid_elements’ to enable HTML tags and their attributes. Example:

    editor-extended_valid_elements = span[class|style]
  • Set ‘editor-content_css’ to use a custom CSS to style elements inside the editor. Example:

    editor-content_css = fileadmin/custom.css
  • FormElement.size = <min_height>,<max_height>: in pixels, including top and bottom bars. E.g.: 300,600

Type: annotate

Annotate image or text. Typically the image or text has been uploaded during a previous step. The annotation will be saved in column of the current record. The uploaded file itself will not be modified. The annotations can be shown in edit (and might be modified) or in readonly mode.

Two modes are available:

A simple grafic editor to paint on top of the image (best by a tablet with pen or grafic tablet). The uploaded image is shown in the background. All drawings are saved as a JSON fabric.js data string. Supported file types: png, svg. PDF files can be easily divided into per page SVG files during upload - see split-pdf-upload
Per code line, annotation(s) can be added. Different users can add individual annotations. A user can only edit the own annotations. The annotations are saved as QFQ internal JSON string.


Drawing with fabric.js might produce a lot data. Take care the column type/size is big enough (>=64kB).


An image, specified by FormElement.parameter.imageSource={{pathFileName}}, will be displayed in the background. On form load, both, the image and an optional already given grafical annotations, will be displayed. The image is SIP protected and will be loaded on demand.


Attribute Value Description
annotateType grafic grafic|text. Default is grafic. Select mode.
imageSource <path filename> Background image. E.g. fileadmin/images/scan.png
defaultPenColor <rgb hex value> | Pen default color, after loading the fabric element. Default is ‘0000FF’ (blue).


By using the the FormElement annotate, the JS code fabric.min.js and qfq.fabric.min.js has to be included. See setup-css-js.



Attribute Value Description
annotateType text grafic|text. Default is grafic. Select mode.
textSource <path filename> Text file to annotate.
annotateUserName <john doe> Will be shown at annotation line.
annotateUserUid <123> Will be shown at annotation line.
annotateUserAvatar <https://gravatar…> Will be shown at annotation line.
highlight auto off,auto,javascript,qfq,python,matlab

Type: imageCut

Uploaded images can be cut or rotate via QFQ (via fabric.js). The modified image is saved under the given pathFileName.

  • The ‘value’ of the FormElement has to be a valid PathFileName to an image.
  • Valid image file formats are SVG, PNG, JPG, GIF.
  • Invalid or missing filenames results to an empty ‘imageCut’ element.
  • FormElement.parameter:
    • resizeWidth = <empty>|[width in pixel] - the final width of the modified image. If empty (or not given), no change.
    • keepOriginal = <empty>|[string] - By default: ‘.save’. If empty (no string given), don’t keep the original. If an extension is given and if there is not already a <pathFileName><.extension>, than the original file is to copied to it.

Type: note

An FormElement without any ‘input’ functionality -just to show some text. Use the typical fields ‘label’, ‘value’ and ‘note’ to be displayed in the corresponding three standard columns.

Type: password

  • Like a text element, but every character is shown as an asterisk.

Type: radio

  • Radio Buttons will be built from one of three sources:

    1. ‘sql1’: E.g. {{!SELECT type AS label FROM car }} or {{!SELECT type AS label, typeNr AS id FROM car}} or {{!SHOW tables}}.
    • Resultset format ‘named’: column ‘label’ and optional a column ‘id’.
    • Resultset format ‘index’:
      • One column in resultset >> first column represents label
      • Two or more columns in resultset >> first column represents id and second column represents label.
    1. FormElement.parameter:
    • itemList = <attribute> E.g.: itemList=red,blue,orange or itemList=1:red,2:blue,3:orange
    • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange
    1. Definition of the enum or set field (only labels, ids are not possible).
  • FormElement.maxlength = <value>

    • Applies only to ‘plain’ radio elements (not the Bootstrap ‘buttonClass’ from below)
    • vertical or horizontal alignment:
      • <value>: ‘’, 0, 1 - The radios will be aligned vertical.
      • <value>: >1 - The readios will be aligned horizontal, with a linebreak every ‘value’ elements.
  • FormElement.parameter:

    • emptyHide: Existence of this item hides an entry with an empty string. This is useful for e.g. Enums, which have an empty entry, but the empty value should not be selectable.

    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.

    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.

    • buttonClass = <class> - Instead of the plain radio fields, Bootstrap buttons. are rendered as radio elements. Use one of the following classes:

      • btn-default (default, grey),
      • btn-primary (blue),
      • btn-success (green),
      • btn-info (light blue),
      • btn-warning (orange),
      • btn-danger (red).

      With a given buttonClass, all buttons (=radios) are rendered horizontal. A value in FormElement.maxlength has no effect.

  • No preselection:

    • If there is a default configured on a table column, such a value is selected by default. If the user should actively choose an option, the ‘preselection’ can be omitted by specifying an explicit definition on the FormElement field value:


      For existing records the shown value is as expected the value of the record. For new records, it’s the value 0, which is typically not one of the ENUM values and therefore nothing is selected.

Type: select

  • Select lists will be built from one of three sources:
    • FormElement.sql1 = {{!<SQL Query}}
      • E.g. {{!SELECT type AS label FROM car }} or {{!SELECT type AS label, typeNr AS id FROM car}} or {{!SHOW tables}}.
      • Resultset format ‘named’: column ‘label’ and optional a column ‘id’.
      • Resultset format ‘index’:
        • One column in resultset >> first column represents label
        • Two or more columns in resultset >> first column represents id and second column represents label.
    • FormElement.parameter:
      • itemList = <attribute> - E.g.: itemList=red,blue,orange or itemList=1:red,2:blue:3:orange
      • If ‘:’ or ‘,’ are part of key or value, it needs to escaped by \ . E.g.: itemList=1:red\: (with colon),2:blue\, (with comma),3:orange
    • Definition of the enum or set field (only labels, ids are not possible).
  • FormElement.size = <value>
    • <value>: <empty>|0|1: drop-down list.
    • <value>: >1: Select field with size rows height. Multiple selection of items is possible.
  • FormElement.parameter:
    • emptyItemAtStart: Existence of this item inserts an empty entry at the beginning of the selectlist.
    • emptyItemAtEnd: Existence of this item inserts an empty entry at the end of the selectlist.
    • emptyHide: Existence of this item hides the empty entry. This is useful for e.g. Enums, which have an empty entry and the empty value should not be an option to be selected.
    • datalist: Similar to ‘typeAhead’. Enables the user to select a predefined option (sql1, itemList) or supply any free text. Attention: Safari (and some other) browsers do not support this fully -

Type: subrecord

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 an SQL query. The number of records shown is not limited. These FormElement will be rendered inside the form as a HTML table.

  • mode / modeSql = <type/value>

    • show / required: the regular mode to show the subrecords
    • readonly: New / Edit / Delete Buttons are disabled
    • hidden: The FormElement is rendered, but hidden with display=’none’.
  • dynamicUpdate - not supported at the moment.

  • sql1 = {{!SQL Query}}

    • SQL query to select records. E.g.:

      {{!SELECT AS id, CONCAT(addr.street, addr.streetnumber) AS a, AS b, AS c FROM Address AS addr}}
    • 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’.

    • Column name: [title=]<title>[|[maxLength=]<number>][|nostrip][|icon][|link][|url][|mailto][|_rowClass][|_rowTooltip]

      • If the keyword is used, all parameter are position independent.

      • Parameter are separated by ‘|’.

      • [title=]<text>: Title of the column. The keyword ‘title=’ is optional. Columns with a title starting with ‘_’ won’t be rendered.

      • [maxLength=]<number>: Max. number of characters displayed per cell. The keyword ‘maxLength=’ is optional. Default maxLength ‘20’. A value of ‘0’ means no limit. 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 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 rendered in the cell).

      • link: value will be rendered as described under Column: _link

      • url: value will be rendered as a href url.

      • mailto: value will be rendered as a href mailto.

      • _rowClass

      • _rowTooltip

        • Defines the title attribute (=tooltip) of a subrecord table row.
      • Examples:

        {{!SELECT id, note1 AS 'Comment', note2 AS 'Comment|50' , note3 AS 'title=Comment|maxLength=100|nostrip', note4 AS '50|Comment',
        'checked.png' AS 'Status|icon', email AS 'mailto', CONCAT(homepage, '|Homepage') AS 'url',
        CONCAT('d|s|F:', pathFileName) AS 'Download|link',
        ELT(status,'info','warning','danger') AS '_rowClass', help AS '_rowTooltip' ...}}
  • FormElement.parameter

    • form = <form name> - Target form, e.g. form=person

    • page = <T3 page alias or id> - Target page with detail form. If none specified, use the current page.

    • extraDeleteForm: Optional. The per row delete Button will reference the form specified here (for deleting) instead of the default (form).

    • detail = <string> - Mapping of values from

        1. the primary form,
        1. the current row,
        1. any constant or ‘{{…}}’ -

      to the target form (defined via form=…).

      • Syntax:

        <source table column name 1|&constant 1>:<target column name 1>[,<source table column name 2|&constant 2>:<target column name 2>][...]
      • Example: detail=id:personId,rowId:secId,&12:xId,&{{a}}:personId (rowId is a column of the current selected row defined by sql1)

      • 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 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 (r=5). The definition detail=id:personId and form=address maps 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.

    • subrecordTableClass: Optional. Default: ‘table table-hover qfq-subrecord-table qfq-color-grey-2’. If given, the default will be overwritten. Example:

      subrecordTableClass = table table-hover qfq-subrecord-table qfq-table-50
    • Tablesorter in Subrecord:

      subrecordTableClass = table table-hover qfq-subrecord-table tablesorter tablesorter-pager tablesorter-filter

    • subrecordColumnTitleEdit: Optional. Will be rendered as the column title for the new/edit column.

    • subrecordColumnTitleDelete: Optional. Will be rendered as the column title for the delete column.

Subrecord DragAndDrop

Subrecords inherently support drag-and-drop, see also drag_and_drop. The following parameters can be used in the parameter field to customize/activate drag-and-drop:

  • orderInterval: The order interval to be used, default is 10.
  • dndTable: The table that contains the records to be ordered. If not given, the table name of the form specified via form=… is used.
  • orderColumn: The dedicated order column in the specified dndTable (needs to match a column in the table definition). Default is ord.

If dndTable is an actual table with a column orderColumn, QFQ automatically applies drag-and-drop logic to the rendered subrecord. It does so by using the subrecord field sql1. The sql1 query should therefore include both a column id (or _id) and ord (or _ord).


  • If you want to deactivate a drag-and-drop that QFQ automatically renders, set the orderColumn to a non-existing column. E.g., orderColumn = nonExistingColumn. This will deactivate drag-and-drop.
  • In order to evaluate the sql1 query dynamically during a drag-and-drop event, the fill store R (with the current form) is loaded. Currently, SIP parameters and other variables are not supplied to be evaluated during a drag-and-drop event. This may be added later upon request.
  • If the subrecord is rendered with drag-and-drop active, but the order is not affected upon reload, there is most likely a problem with evaluating the sql1 query at runtime.

Type: time

  • Range time: ‘00:00:00’ to ‘23:59:59’ or ‘00:00:00’. (
  • Optional:
  • FormElement.parameter
    • showSeconds = 0|1 - shows the seconds. Independent if the user specifies seconds, they are displayed ‘1’ or not ‘0’.
    • showZero = 0|1 - For an empty timestamp, With ‘0’ nothing is displayed. With ‘1’ the string ‘00:00[:00]’ is displayed.

Type: upload

An upload element is based on a ‘file browse’-button and a ‘trash’-button (=delete). Only one of them is shown at a time. The ‘file browse’-button is displayed, if there is no file uploaded already. The ‘trash’-button is displayed, if there is a file uploaded already.

After clicking on the browse button, the user 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 (see below) the file, the ‘file browse’-button disappears and the filename is shown, followed by a ‘trash’-button. 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 fileDestination. The user have to save the current record, either to finalize the upload and/or to delete a previously uploaded file.

The FormElement behaves like a

  • ‘native FormElement’ (showing controls/text on the form) as well as an
  • ‘action FormElement’ by firing queries and doing some additional actions during form save.

Inside the Form editor it’s shown as a ‘native FormElement’. During saving the current record, it behaves like an action FormElement and will be processed after saving the primary record and before any action FormElements are processed.

  • FormElement.value = <string> - By default, the full path of any already uploaded file is shown. To show something different, e.g. only the filename, define:

    a) {{filenameBase:V}}
    b) {{SELECT SUBSTRING_INDEX( '{{pathFileName:R}}', '/', -1)  }}

See also downloadButton to offer a download of an uploaded file.

  • FormElement.parameter:

    • fileButtonText: Overwrite default ‘Choose File’

    • capture = camera - On a smartphone, after pressing the ‘open file’ button, the camera will be opened and a choosen picture will be uploaded. Automatically set/overwrite accept=image/*.

    • accept = <mime type>,image/*,video/*,audio/*,.doc,.docx,.pdf

      • List of mime types (also known as ‘media types’):
      • If none mime type is specified, ‘application/pdf’ is set. This forces that always (!) one type is specified.
      • To allow any type, specify * or */* or *.*.
      • One or more media types might be specified, separated by ‘,’.
      • Different browser respect the given definitions in different ways. Typically the ‘file choose’ dialog offer:
        • the specified mime type (some browers only show ‘custom’, if more than one mime type is given),
        • the option ‘All files’ (the user is always free to try to upload other file types) - but the server won’t accept them,
        • the ‘file choose’ dialog only offers files of the selected (in the dialog) type.
      • If for a specific file type is no mime type available, the definition of file extension(s) is possible. This is less secure, cause there is no content check on the server after the upload.
    • maxFileSize = <size> - max filesize in bytes (no unit), kilobytes (k/K) or megabytes (m/M) for an uploaded file. If empty or not given, take value from Form, System or System default.

    • fileTrash = [0|1] - Default: ‘1’. This option en-/disables the trash button right beside the file chooser. By default the trash is visible. The trash is only visible if a) there is already a file uploaded or b) a new file has been chosen.

    • fileTrashText = <string> - Default: ‘’. Will be shown right beside the trash glyph-icon.

    • fileDestination = <pathFileName> - Destination where to copy the file. A good practice is to specify a relative fileDestination - such an installation (filesystem and database) are moveable.

      • If the original filename should be part of fileDestination, the variable {{filename}} (STORE_VARS) can be used. Example

        fileDestination={{SELECT 'fileadmin/user/pictures/',, '-{{filename}}' FROM Person AS p WHERE{{id:R0}} }}
        • Several more variants of the filename and also mimetype and filesize are available. See store_vars_form_element_upload.
        • The original filename will be sanitized: only ‘<alnum>’, ‘.’ and ‘_’ characters are allowed. German ‘umlaut’ will be replaced by ‘ae’, ‘ue’, ‘oe’. All non valid characters will be replaced by ‘_’.
      • 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. Check also fileReplace below.

      • All necessary subdirectories in fileDestination are automatically created.

      • Using the current record id in the fileDestination: Using {{r}} is problematic for a ‘new’ primary record: that one is still ‘0’ at the time of saving. Use {{id:R0}} instead.

      • Uploading of malicious code (e.g. PHP files) is hard to detect. The default mime type check can be easily faked by an attacker. Therefore it’s recommended to use a fileDestination-directory, which is secured against script execution (even if the file has been uploaded, the webserver won’t execute it) - see SecureDirectFileAccess.

    • sqlBefore, sqlAfter: available in Upload simple mode and Upload advanced mode.

    • slaveId, sqlInsert, sqlUpdate, sqlDelete, sqlUpdate: available only in Upload advanced mode.

    • fileSize / mimeType

      • In Upload simple mode the information of fileSize and mimeType will be automatically updated on the current record, if table columns fileSize and/or mimeType exist.

        • If there are more than one Upload FormElement in a form, the automatically update for fileSize and/or mimeType are not done automatically.
      • In Upload advanced mode the fileSize and / or mimeType have to be updated with an explicit SQL statement:

        sqlAfter = {{UPDATE Data SET mimeType='{{mimeType:V}}', fileSize={{fileSize:V}} WHERE id={{id:R}} }}
    • fileReplace = always - If fileDestination exist - replace it by the new one.

    • chmodFile = <unix file permission mode> - e.g. 660 for owner and group read and writeable. Only the numeric mode is allowed.

    • chmodDir = <unix file permission mode> - e.g. 770 for owner and group read, writeable and executable. Only the numeric mode is allowed. Will be applied to all new created directories.

    • autoOrient: images might contain EXIF data (e.g. captured via mobile phones) incl. an orientation tag like TopLeft, BottomRight and so on. Web-Browser and other grafic programs often understand and respect those information and rotate such images automatically. If not, the image might be displayed in an unwanted oritentation. With active option ‘autoOrient’, QFQ tries to normalize such images via ‘convert’ (part of ImageMagick). Especially if images are processed by the QFQ internal ‘Fabric’-JS it’s recommended to normalize images first. The normalization process does not solve all orientation problems.

      • autoOrient = [0|1]
      • autoOrientCmd = ‘convert -auto-orient {{fileDestination:V}} {{fileDestination:V}}.new; mv {{fileDestination:V}}.new {{fileDestination:V}}’
      • autoOrientMimeType = image/jpeg,image/png,image/tiff

      If the defaults for autoOrientCmd and autoOrientMimeType are sufficient, it’s not necessary to specify them.

  • downloadButton = t:<string> - If given, shows a button to download the previous uploaded file - instead of the string given in fe.value. The button is only shown if fe.value points to a readable file on the server.
    • If downloadButton is empty, just shows the regular download glyph.
    • To just show the filename: downloadButton = t:{{filenameOnly:V}}
    • Additional attributes might be given like downloadButton = t:Download|o:check file. Please check download.
      • The following attributes are hard coded (can’t be changed): s|M:file|d|F
  • fileSplit, fileDestinationSplit, tableNameSplit: see split-pdf-upload
  • Excel Import: QFQ offers functionality to directly import excel data into the database. This functionality can optionally be combined with saving the file by using the above parameters like fileDestination. The data is imported without formatting. Please note that this means Excel dates will be imported as a number (e.g. 43214), which is the serial value date in Excel. To convert such a number to a MariaDb date, use: DATE_ADD(‘1899-12-30’, INTERVAL serialValue DAY).
    • importToTable = <[db.]tablename> - Required. Providing this parameter activates the import. If the table doesn’t exist, it will be created.
    • importToColumns = <col1>,<col2>,… - If none provided, the Excel column names A, B, … are used. Note: These have to match the table’s column names if the table already exists.
    • importRegion = [tab],[startColumn],[startRow],[endColumn],[endRow]|… - All parts are optional (default: entire 1st sheet). Tab can either be given as an index (1-based) or a name. start/endColumn can be given either numerically (1, 2, …) or by column name (A, B, …). Note that you can specify several regions to import.
    • importMode = append (default) | replace - The data is either appended or replace in the specified table.
    • importType = auto (default) | xls | xlsx | ods | csv - Define what kind of data should be expected by the Spreadsheet Reader.
    • importNamedSheetsOnly = <comma separated list of sheet names>. Use this option if specific sheets cause problems during import and should be skipped, by naming only those sheets, who will be read. This will also reduce the memory usage.
    • importSetReadDataOnly = 0|1. Read only cell data, not the cell formatting. Warning: cell types other than numerical will be misinterpreted.
    • importListSheetNames = 0|1. For debug use only. Will open a dialog and report all found worksheet names.

Immediately after the upload finished (before the user press save), the file will be checked on the server for it’s content or file extension (see ‘accept’).

The maximum size is defined by the minimum of upload_max_filesize, post_max_size and memory_limit (PHP script) in the php.ini.

In case of broken uploads, please also check max_input_time in php.ini.

Deleting a record and the referenced file

If the user deletes a record (e.g. pressing the delete button on a form) which contains reference(s) to files, such files are deleted too. Slave records, which might be also deleted through a ‘delete’-form, are not checked for file references and therefore such files are not deleted on the filesystem.

Only column(name)s which contains pathFileName as part of their name, are checked for file references.

If there are other records, which references the same file, such files are not deleted. It’s a very basic check: just the current column of the current table is compared. In general it’s not a good idea to have multiple references to a single file. Therefore this check is just a fallback.

Upload simple mode

Requires: ‘upload’ = ‘column name’ of an column in the primary table.

After moving the file to fileDestination, the current record/column will be updated to fileDestination. The database definition of the named column has to be a string variant (varchar, text but not numeric or else). On form load, the column value will be displayed as the whole value (pathFileName)

Deleting an uploaded file in the form (by clicking on the trash near beside) will delete the file on the filesystem as well. The column will be updated to an empty string.

This happens automatically without any further definiton in the ‘upload’-FormElement.

Multiple ‘upload’-FormElements per form are possible. Each of it needs an own table column.

Upload advanced mode

Requires: ‘upload’ is unknown as a column in the primary table.

This mode will serve further database structure scenarios.

A typical name for such an ‘upload’-FormElement, to show that the name does not exist in the primary table, might start with ‘my’, e.g. ‘myUpload1’.

  • FormElement.value = <string> - The path/filename, shown during ‘form load’ to indicate a previous uploaded file, has to be queried with this field. E.g.:

    {{SELECT pathFileNamePicture FROM Note WHERE id={{slaveId}} }}
  • FormElement.parameter:

    • fileDestination = <pathFileName> - determine the path/filename. E.g.:

    • slaveId = <id> - Defines the target record where to retrieve and store the path/filename of the uploaded file. Check also Parameter: slaveId. E.g.:

      slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}}
    • sqlBefore = {{<query>}} - fired during a form save, before the following queries are fired.

    • sqlInsert = {{<query>}} - fired if slaveId=0 and an upload exist (user has choosen a file):

      sqlInsert={{INSERT INTO Note (pId, type, pathFileName) VALUE ({{id:R0}}, 'image', '{{fileDestination}}') }}
    • sqlUpdate = {{<query>}} - fired if slaveId>0 and an upload exist (user has choosen a file). E.g.:

      sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
    • sqlDelete = {{<query>}} - fired if slaveId>0 and no upload exist (user has not choosen a file). E.g.:

      sqlDelete={{DELETE FROM Note WHERE id={{slaveId:V}}  LIMIT 1}}
    • sqlAfter = {{<query>}} - fired after all previous queries have been fired. Might update the new created id to a primary record. E.g.:

      sqlAfter={{UPDATE Person SET noteIdPicture = {{slaveId}} WHERE id={{id:R0}} LIMIT 1 }}

Split PDF Upload

Additional to the upload, it’s possible to split the uploaded file (only PDF files) into several SVG or JPEG files, one file per PDF page. The split is done via a) or b) Image Magick convert.

Currently, QFQ can only split PDF files.

If the source file is not of type PDF, activating fileSplit has no impact: no split and NO complain about invalid file type.

  • FormElement.parameter:

    • fileSplit = <type> - Activate the splitting process. Possible values: svg or jpeg. No default.

    • fileSplitOptions = <command line options>.

      • [svg] - no default
      • [jpeg] - default: -density 150 -quality 90
    • fileDestinationSplit = <pathFileName (pattern)> - Target directory and filename pattern for the created & split’ed files. Default <fileDestination>.split/split.<nr>.<fileSplit>. If explicit given, respect that SVG needs a printf style for <nr>, whereas JPEG is numbered automatically. E.g.

      [svg] fileDestinationSplit = fileadmin/protected/{{id:R}}.{{filenameBase:V}}.%02d.svg
      [jpeg] fileDestinationSplit = fileadmin/protected/{{id:R}}.{{filenameBase:V}}.jpg
    • tableNameSplit = <tablename> - Default: name of table of current form. This name will be saved in table Split

The splitting happens immediately after the user pressed save.

To easily access the split files via QFQ, per file one record is created in table ‘Split’.

Table ‘Split’:

Column Description
id Uniq auto increment index
tableName Name of the table, where the reference to the original file (multipage PDF file) is saved.
xId Primary id of the reference record.
pathFileName Path/filename reference to one of the created files

One usecase why to split an upload: annotate individual pages by using the FormElement.type=`annotate`.

Class: Action

Type: before… | after…

These type of ‘action’ FormElements will be used to implement data validation or creating/updating additional records.


  • beforeLoad (e.g. good to check access permission)
  • afterLoad
  • beforeSave (e.g. to prohibit creating of duplicate records)
  • afterSave (e.g. to to create & update additional records)
  • beforeInsert
  • afterInsert
  • beforeUpdate
  • afterUpdate
  • beforeDelete (e.g. to delete slave records)
  • afterDelete
  • paste (configure copy/paste forms)

Parameter: sqlValidate

Perform checks by firing an SQL query and expecting a predefined number of selected records.

  • 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.


  • requiredList = <[s]> - 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 = {{<query>}} - validation query. E.g.: sqlValidate={{SELECT id FROM Person AS p WHERE LIKE {{name:F:all}} AND p.firstname LIKE {{firstname:F:all}} }}
  • expectRecords = <value>- 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 = <string> - Message to show. E.g.: messageFail = There is already a person called {{firstname:F:all}} {{name:F:all}}

Parameter: slaveId


  • slaveId = <id>:
    • 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 column name, set slaveId = 0.
    • Explicit definition: slaveId = 123 or slaveId = {{SELECT id …}}


  • {{slaveId:V}} can be used in any query of the current FormElement.
  • 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() becomes the {{slaveId:V}}.
  • fillStoreVar is fired first, than slaveId.
  • If slaveId is known in fillStoreVar, set: slaveId={{someId:V}}.

Parameter: sqlBefore / sqlInsert / sqlUpdate / sqlDelete / sqlAfter

  • Save values of a form to different record(s), optionally on different table(s).
  • Typically useful on ‘afterSave’ - be careful when using it earlier, e.g. beforeLoad.


  • requiredList = <[s]> - List of native-FormElement: only if all of those elements are filled, the current action-FormElement will be processed.
  • sqlBefore = {{<query>}} - always fired (before any sqlInsert, sqlUpdate, ..)
  • sqlInsert = {{<query>}} - fired if slaveId == 0 or slaveId == ‘’.
  • sqlUpdate = {{<query>}} - fired if slaveId > 0.
  • sqlDelete = {{<query>}} - fired if slaveId > 0, after sqlInsert or sqlUpdate. Be careful not to delete filled records! Always add a check, if values given, not to delete the record! sqlHonorFormElements helps to skip such checks.
  • sqlAfter = {{<query>}} - always fired (after sqlInsert, sqlUpdate or sqlDelete).
  • sqlHonorFormElements = <[s]> list of FormElement names (this parameter is optional).
    • If one of the named FormElements is not empty:
      • fire sqlInsert if slaveId == 0,
      • fire sqlUpdate if slaveId > 0
    • If all of the named FormElements are empty:
      • fire sqlDelete if slaveId > 0


Situation 1: (1:1)

  • Name the action element ‘xId’: than {{slaveId}} will be automatically set to the value of ‘master.xId’

    • {{slaveId}} == 0 ? ‘sqlInsert’ will be fired.
    • {{slaveId}} != 0 ? ‘sqlUpdate’ will be fired.
  • In case of firing ‘sqlInsert’, the ‘’ of the new created record are copied to master.xId (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 …}}

  • Two FormElements myStreet and myCity:

    • Without sqlHonorFormElements. Parameter:

      sqlInsert = {{INSERT INTO address (`street`, `city`) VALUES ('{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM address WHERE id={{slaveId}} AND '{{myStreet:FE:alnumx:s}}'='' AND '{{myCity:FE:alnumx:s}}'='' LIMIT 1 }}
    • With sqlHonorFormElements. Parameter:

      sqlHonorFormElements = myStreet, myCity     # Non Templategroup
      sqlInsert = {{INSERT INTO address (`street`, `city`) VALUES ('{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM address WHERE id={{slaveId}} LIMIT 1 }}
      # For Templategroups: sqlHonorFormElements = myStreet%d, myCity%d

Situation 2: (1:n)

  • Name the action element different to any column name of the master record (or no name).

  • Determine the slaveId: slaveId={{SELECT id FROM slave WHERE{{…}} LIMIT 1}}

    • {{slaveId}} == 0 ? ‘sqlInsert’ will be fired.
    • {{slaveId}} != 0 ? ‘sqlUpdate’ will be fired.
  • Two FormElements myStreet and myCity. The person is the master record, address is the slave:

    • Without sqlHonorFormElements. Parameter:

      slaveId = {{SELECT id FROM address WHERE personId={{id}} ORDER BY id LIMIT 1 }}
      sqlInsert = {{INSERT INTO address (`personId`, `street`, `city`) VALUES ({{id}}, '{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM address WHERE id={{slaveId}} AND '{{myStreet:FE:alnumx:s}}'='' AND '{{myCity:FE:alnumx:s}}'='' LIMIT 1 }}
    • With sqlHonorFormElements. Parameter:

      slaveId = {{SELECT id FROM address WHERE personId={{id}} ORDER BY id LIMIT 1 }}
      sqlHonorFormElements = myStreet, myCity       # Non Templategroup
      sqlInsert = {{INSERT INTO address (`personId`, `street`, `city`) VALUES ({{id}}, '{{myStreet:FE:alnumx:s}}', '{{myCity:FE:alnumx:s}}') }}
      sqlUpdate = {{UPDATE address SET `street` = '{{myStreet:FE:alnumx:s}}', `city` = '{{myCity:FE:alnumx:s}}'  WHERE id={{slaveId}} LIMIT 1 }}
      sqlDelete = {{DELETE FROM address WHERE id={{slaveId}} LIMIT 1 }}
      # For Templategroups: sqlHonorFormElements = myStreet%d, myCity%d

Type: sendmail

  • Send mail(s) will be processed after:

    • saving the record ,
    • processing all uploads,
    • processing afterSave action FormElements.
  • FormElement.value = <string> - Body of the email. See also: html-formatting

  • FormElement.parameter:

    • sendMailTo = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <>. If there is no recipient email address, no mail will be sent.
    • sendMailCc = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <>.
    • sendMailBcc = <string> - Comma-separated list of receiver email addresses. Optional: ‘realname <>.
    • sendMailFrom = <string> - Sender of the email. Optional: ‘realname <>’. Mandatory.
    • sendMailSubject = <string> - Subject of the email.
    • sendMailReplyTo = <string> - Reply this email address. Optional: ‘realname <>’.
    • sendMailAttachment = <string> - List of ‘sources’ to attach to the mail as files. Check attachment for options.
    • sendMailHeader = <string> - Specify custom header.
    • sendMailFlagAutoSubmit = <string> - on|off - If ‘on’ (default), the mail contains the header ‘Auto-Submitted: auto-send’ - this suppress a) OoO replies, b) forwarding of emails.
    • sendMailGrId = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId2 = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailXId3 = <string> - Will be copied to the mailLog record. Helps to setup specific logfile queries.
    • sendMailMode = <string> - html - if set, the e-mail body will be rendered as html.
    • sendMailSubjectHtmlEntity = <string> - encode|decode|none - the mail subject will be htmlspecialchar() encoded / decoded (default) or none (untouched).
    • sendMailBodyHtmlEntity*= `<string>` - **encode|decode|none* - the mail body will be htmlspecialchar() encoded, decoded (default) or none (untouched).
    • sqlBefore / sqlAfter = <string> - can be used like with other action elements (will be fired before/after sending the e-mail).
  • 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 primary record, use the STORE_RECORD. E.g. {{id:R}}.

  • By default, QFQ stores values ‘htmlspecialchars()’ encoded. If such values have to send by email, the html entities are unwanted. Therefore the default setting for ‘subject’ und ‘body’ is to decode the values via ‘htmlspecialchars_decode()’. If this is not wished, it can be turned off by sendMailSubjectHtmlEntity=none and/or sendMailBodyHtmlEntity=none.

  • For debugging, please check REDIRECT_ALL_MAIL_TO.

Example to attach one file1.pdf (with the attachment filename ‘readme.pdf’) and concatenate two PDF, created on the fly from the and ?export (with the attachment filename ‘personal.pdf’):

sendMailAttachmemt = F:fileadmin/file1.pdf|d:readme.pdf|C|u:|p:?id=export&r=123&_sip=1|d:personal.pdf

Type: paste

See also copy-form.

  • sql1 = {{<query>}} - e.g. {{!SELECT {{id:P}} AS id, ‘{{myNewName:FE:allbut}}’ AS name}} (only one record) or {{!SELECT AS id, {{basketId:P}} AS basketId FROM Item AS i WHERE i.basketId={{id:P}} }} (multiple records)
    • Pay attention to ‘!’.
    • For every row, a new record is created in recordDestinationTable.
    • Column ‘id’ is not copied.
    • The recordSourceTable together with column id will identify the source record.
    • Columns not specified, will be copied 1:1 from source to destination.
    • Columns specified, will overwrite the source value.
  • FormElement.parameter:
    • recordSourceTable = <tableName> - Optional: table from where the records will be copied. Default: <recordDestinationTable>
    • recordDestinationTable = <tableName> - table where the new records will be copied to.
    • translateIdColumn = <column name> - column name to update references of newly created id’s.

Form Magic


  • Table column id: QFQ expect that each table, which will be loaded in a form, contains an autoincrement column of name id. It’s not necessary to create a FormElement id in a form - but it won’t disturb.

  • Parameter (one or more) in the SIP url, which exist as a column in the form table (SIP parameter name is equal to a table column name), will be automatically saved in the record. This acts as ‘hidden magic’.

    Example: A slave record (e.g. an address of a person) has to be assigned to a master record (a person). Just give the pId in the link who calls the address form. The following creates a ‘new’ button for an address for all persons, and the pId will be automatically saved in the address table:

    SELECT CONCAT('p:{{pageAlias:T}}&form=address&r=0&pId=', AS _pagen FROM Person AS p

    Such parameter, which the form expects to be in the SIP url, should be specified in Form.permitNew and/or Form.permitEdit. It’s only a check for the webmaster, not to forgot a parameter in a SIP url.

  • FormElement.type = subrecord

    Subrecord’s will automatically create new, edit and delete links. To inject parameter in those automatically created links, use FormElement.parameter.detail . See subrecord-option.

  • FormElement.type = extra

    If a table column should be saved with a specific value, and the value should not be shown to the user, the FE.type=’extra’ will do the job. The value could be static or calculated on the fly. Often it’s easier to specify such a parameter/value in the SIP url, but if the form is called from multiple places, an extra element is more suitable.


  • Form.parameter.fillStoreVar / FormElement.parameter.fillStoreVar

    An SQL statement will fill STORE_VARS. Such values can be used during form load and/or save.


  • Action FE

    Via FormElement.parameter.requiredList an element can be enabled / disabled, depending of a user provided input in one of the specified required FEs.

Multi Form

Multi Forms are like a regular form with the difference that the shown FormElements are repeated for each selected record (defined by multiSql).

multiSql {{!SELECT id, name FROM Person}} Query to select MulitForm records
multiMgsNoRecord Default: No data Message shown if multiSql selects no records
  • Multi Form do not use ‘record-locking’ at all.

The Form is shown as a HTML table.

  • multiSql: Selects the records where the defined FormElements will work on each.
    • A uniq column ‘id’ or ‘_id’ (not shown) is mandatory and has to reflect an existing record id in table primary table.
    • Additional columns, defined in multiSql, will be shown on the form in the same line, before the FormElements.



  • It’s not possible to create new records in simple mode, only existing records can be modified.


  • Per row, the STORE_RECORD is filled with the whole record of the primary table, referenced by


  • The represents a column of the defined primary table.
  • The existing values of such FormElements are automatically loaded.
  • No further definition is required.


To handle foreign records (insert/update/delete), use the slaveId_ concept.

Typically the is not a column of the primary table.

Multiple languages

QFQ Forms might be configured for up to 5 different languages. Per language there is one extra field in the Form editor. Which field represents which language is configured in configuration.

  • The Typo3 installation needs to be configured to handle different languages - this is independent of QFQ and not covered here. QFQ will use the Typo3 internal variable ‘pageLanguage’, which typically correlates to the URL parameter ‘L’ in the URL.
  • In configuration the Typo3 language index (value of ‘L’) and a language label have to be configured for each language. Only than, the additional language fields in the Form editor will be shown.


Assuming the Typo3 page has the

  • default language, L=0
  • English, L=1
  • Spanish, L=2

Configuration in configuration:

formLanguageAId = 1
formLanguageALabel = English

formLanguageBId = 2
formLanguageBLabel = Spanish

The default language is not covered in configuration.

The Form editor now shows on the pill ‘Basic’ (Form and FormEditor) for both languages each an additional parameter input field. Any input field in the Form editor can be redeclared in the corresponding language parameter field. Any missing definition means ‘take the default’. E.g.:

  • Form: ‘person’

    Column Value
    title Eingabe Person
    languageParameterA title=Input Person
    languageParameterB title=Persona de entrada
  • FormElement ‘firstname’ in Form ‘person’:

    Column Value
    title Vorname
    note Bitte alle Vornamen erfassen
    note=Please give all firstnames
    title=Persona de entrada
    note=Por favor, introduzca todos los nombres

The following fields are possible:

  • Form: title, showButton, forwardMode, forwardPage, bsLabelColumns, bsInputColumns, bsNoteColumns, recordLockTimeoutSeconds
  • FormElement: label, mode, modeSql, class, type, subrecordOption, encode, checkType, ord, size, maxLength, bsLabelColumns, bsInputColumns, bsNoteColumns,rowLabelInputNote, note, tooltip, placeholder, value, sql1, feGroup

Dynamic Update

The ‘Dynamic Update’ feature makes a form more interactive. If a user changes 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’
  • ‘label’
  • ‘value’
  • ‘note’
  • ‘parameter.*’ - especially ‘itemList’

To make a form dynamic:

  • Mark all FormElements with dynamic update`=`enabled, which should initiate or receive updates.

See #3426 / Dynamic Update: Inputs loose the new content and shows the old value:

  • On all dynamic update FormElements an explicit definition of value, including a sanitize class, is necessary (except the field is numeric). A missing definition let’s the content overwrite all the time with the old value. A typical definition for value looks like (default store priority is: FSRVD):

    {{<FormElement name>::alnumx}}
  • Define the receiving FormElements in a way, that they will interpret the recent user change! The form variable of the specific sender FormElement {{<sender element>:F:<sanitize>}} should be part of one of the above fields to get an impact. E.g.:

    [receiving *FormElement*].parameter: itemList={{ SELECT IF({{carPriceRange:FE:alnumx}}='expensive','Ferrari,Tesla,Jaguar','General Motors,Honda,Seat,Fiat') }}

    Remember to specify a ‘sanitize’ class - a missing sanitize class means ‘digit’, every content, which is not numeric, violates the sanitize class and becomes therefore an empty string!

  • If the dynamic update should work on existing and new records, it’s important to guarantee that the query result is not empty! even if the primary record does not exist! E.g. use a LEFT JOIN. The following query is ok for new and edit.

    {{SELECT IF( IFNULL(adr.type,'') LIKE '%token%','show','hidden') FROM (SELECT 1) AS fake LEFT JOIN Address AS adr ON adr.type='{{type:FR0}}' LIMIT 1}}


  • Master FormElement ‘music’ is a radio/enum of ‘classic’, ‘jazz’, ‘pop’.

Content of a select list

  • Slave FormElement ‘interpret’ is ‘select’-list, depending of ‘music’
sql={{!SELECT name FROM interpret WHERE music={{music:FE:alnumx}} ORDER BY name}}

Show / Hide a FormElement

  • Slave ‘interpret’ is displayed only for ‘pop’. Field ‘modeSql’:
{{SELECT IF( '{{music:FR:alnumx}}'='pop' ,'show', 'hidden' ) }}

Form Layout

The forms will be rendered with Bootstrap CSS classes, based on the 12 column grid model (Bootstrap 3.x). Generally a 3 column layout for label columns on the left side, an input field column in the middle and a note column on the right side will be rendered.

The used default column (=bootstrap grid) width is 3,6,3 (col-md , col-lg) for label, input, note.

  • The system wide defaults can be changed via configuration.
  • Per Form settings can be done in the Form parameter field. They overwrite the system wide default.
  • Per FormElement settings can be done in the FormElement parameter field. They overwrite the Form setting.

A column will be switched off (no wrapping via <div class=’col-md-?>) by setting a 0 on the respective column.

Custom field width

Per FormElement set BS Label Columns, BS Input Columns or BS Note Columns to customize an individual width. If only a number is specified, it’s used as col-md-<number>. Else the whole text string is used as CSS class, e.g. col-md-3 col-lg-2.

Multiple Elements per row

Every row is by default wrapped in a <div class=’form-group’> and every column is wrapped in a <div class=’col-md-?’>. To display multiple input elements in one row, the wrapping of the FormElement row and of the three columns can be customized via the checkboxes of Label / Input / Note. Every open and every close tag can be individually switched on or off.

E.g. to display 2 FormElements in a row with one label (first FormElement) and one note (last FormElement) we need the following (switch off all non named):

  • First FormElement
    • open row tag: row ,
    • open and close label tag: label, /label,
    • open and close field tag: input, /input,
  • Second FormElement
    • open and close field tag: input, /input,
    • open and close note tag: note, /note,
    • close row tag: /row ,

Copy Form

Records (=master) and child records can be duplicated (=copied) by a regular Form, extended by FormElements of type ‘paste’. A ‘copy form’ works either in:

  • ‘copy and paste now’ mode: the ‘select’ and ‘paste’ Form is merged in one form, only one master record is possible,
  • ‘copy now, paste later’ mode: the ‘select’ Form selects master record(s), the ‘paste’ Form paste’s them later.


A ‘select action’ (e.g. a Form or a button click) creates record(s) in the table Clipboard. Each clipboard record contains:

  • the ‘id(s)’ of the record(s) to duplicate,
  • the ‘paste’ form id (that Form defines, to which table the master records belongs to, as well as rules of how to duplicate any slave records) and where to copy the new records
  • user identifier (QFQ cookie) to separate clipboard records of different users inside the Clipboard table.

The ‘select action’ is also responsible to delete old clipboard records of the current user, before new clipboard records are created.

The ‘paste form’ iterates over all master record id(s) in the Clipboard table. For each master record id, all FormElements of type paste are fired (incl. the creating of slave records).

E.g. if there is a basket with different items and you want to duplicate the whole basket including new items, create a form with the following parameter

  • Form
    • Name: copyBasket
    • Table: Clipboard
    • Show Button: only close and save
  • FormElement 1: Record id of the source record.
    • Name: idSrc
    • Lable: Source Form
    • Class: native
    • Type: select
    • sql1: {{! SELECT id, title FROM Basket }}
  • FormElement 2: New name of the copied record.
    • Name: myNewName
    • Class: native
    • Type: text
  • FormElement 3: a) Check that there is no name conflict. b)Purge any old clipboard content of the current user.
    • Name: clearClipboard
    • Class: action
    • Type: beforeSave
    • Parameter:
      • sqlValidate={{SELECT FROM Form AS f WHERE LIKE ‘{{myName:FE:alnumx}}’ LIMIT 1}}
      • expectRecords = 0
      • messageFail = There is already a form with this name
      • sqlAfter={{DELETE FROM Clipboard WHERE cookie=’{{cookieQfq:C0:alnumx}}’ }}
  • FormElement 4: Update the clipboard source reference, with current {{cookieQfq:C}} identifier.
    • Name: updateClipboardRecord
    • Class: action
    • Type: afterSave
    • Parameter: sqlAfter={{UPDATE Clipboard SET cookie=’{{cookieQfq:C0:alnumx}}’, formIdPaste={{formId:S0}} /* PasteForm */ WHERE id={{id:R}} LIMIT 1 }}
  • FormElement 5: Copy basket identifier.
    • Name: basketId
    • Class: action
    • Type: paste
    • sql1: {{!SELECT {{id:P}} AS id, ‘{{myNewName:FE:allbut}}’ AS name}}
    • Parameter: recordDestinationTable=Basket
  • FormElement 6: Copy items of basket.
    • Name: itemId
    • Class: action
    • Type: paste
    • sql1: {{!SELECT AS id, {{basketId:P}} AS basketId FROM Item AS i WHERE i.basketId={{id:P}} }}
    • Parameter: recordDestinationTable=Item

Table self referencing records

Records might contain references to other records in the same table. E.g. native FormElements might assigned to a fieldSet, templateGroup or pill, a fieldSet might assigned to other fieldsets or pills and so on. When duplicating a Form and the corresponding FormElements all internal references needs to be updated as well.

On each FormElement.type=`paste` record, the column to be updated is defined via:

  • parameter: translateIdColumn = <column name>

For the ‘copyForm’ this would be ‘feIdContainer’.

The update of the records is started after all records have been copied (of the specific FormElement.type=`paste` record).

Delete Record

Deleting record(s) via QFQ might be solved by either:

  • using the delete button on a form on the top right corner.
  • by letting report creating a special link (see below). The link contains the record id and:
    • a form name, or
    • a table name.

Deleting a record just by specifying a table name, will only delete the defined record (no slave records).

  • By using a delete button via report or in a subrecord row, a ajax request is send.
  • By using a delete button on the top right corner of the form, the form will be closed after deleting the record.

Example for report:

SELECT, CONCAT('U:form=person&r=', AS _paged FROM Person AS p
SELECT, CONCAT('U:table=Person&r=', AS _paged FROM Person AS p

To automatically delete slave records, use a form and create beforeDelete FormElement(s) on the form:

  • class: action
  • type: beforeDelete
  • parameter: sqlAfter={{DELETE FROM <slaveTable> WHERE <slaveTable>.<masteId>={{id:R}} }}

You might also check the form ‘form’ how the slave records ‘FormElement’ will be deleted.

Locking Record / Form

Support for record locking is given with mode:

  • exclusive: user can’t force a write.
    • Including a timeout (default 15 mins recordLockTimeoutSeconds in configuration) for maximum lock time.
  • advisory: user is only warned, but allowed to overwrite.
  • none: no bookkeeping about locks.

For ‘new’ records (r=0) there is no locking at all.

The record locking protection is based on the tablename and the record id. Different Forms, with the same primary table, will be protected by record locking. On the other side, action-FormElements updating non primary table records are not protected by ‘record locking’: the QFQ record locking is NOT 100%.

The ‘record locking’ mode will be specified per Form. If there are multiple Forms with different modes, and there is already a lock for a tablename / record id pair, the most restrictive will be applied.

Best practice

Custom default value only for ‘new records’

Method 1

On Form.parameter define a fillStoreVar query with a column name equal to a form field. That’s all.

Example: = technicalContact
Form.parameter.fillStoreVar = {{! SELECT CONCAT(p.firstName, ' ', AS technicalContact FROM Person AS p WHERE p.account='{{feUser:T}}' }}

What we use here is the default STORE prio FSRVD. If the form loads with r=0, ‘F’, ‘S’ and ‘R’ are empty. ‘V’ is filled. If r>0, than ‘F’ and ‘S’ are empty and ‘R’ is filled.

Method 2

In the specific FormElement set value={{columnName:RSE}}. The link to the form should be rendered with ‘”…&columnName=<data>&…” AS _page’. The trick is that the STORE_RECORD is empty for new records, and therefore the corresponding value from STORE_SIP will be returned. Existing records will use the already saved value.

Central configured values

Any variable in configuration can be used by {{<varname>:Y}} in form or report statements.



Could be used in an FormElement.type = sendmail with parameter setting sendMailFrom={{TECHNICAL_CONTACT:Y}}.

Debug Report

Writing “report’s” in the nested notation or long queries broken over several lines, might not interpreted as wished. Best for debugging is to specify in the tt-content record:

debugShowBodyText = 1

Note: Debug information is only display if it’s enabled in configuration by

  • showDebugInfo: yes or
  • showDebugInfo: auto and logged in in the same Browser as a Typo3 backend user.

More detailed error messages

If showDebugInfo is enabled, a full stacktrace and variable contents are displayed in case of an error.

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.

Version 1

Compute the next ‘ord’ in advance in the subrecord field of the primary form. Submit that value to the new record via SIP parameter to the secondary form.

On the secondary form: for ‘new’ records choose the computed value, for existing records leave the value unchanged.

  • Master form, subrecord FormElement, field parameter: set

    detail=id:formId,{{SELECT '&', IFNULL(fe.ord,0)+10 FROM Form AS f LEFT JOIN *FormElement* AS fe ON WHERE{{r:S0}} ORDER BY fe.ord DESC LIMIT 1}}:ord
  • Slave form, ord FormElement, field value: set


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 slave table: Address

Relation: = 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 don’t exist, the form should create it. Column Person.noteId points to

Form primary table: Person

Form slave table: Address

Relation: = 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{{r}} AND 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}} }}

Icons Template Group

This example will display graphics instead of text ‘add’ and ‘remove’. Also there is a distance between the templateGroups.

  • FormElement.parameter:

    tgClass = qfq-child-margin-top
    tgAddClass = btn alert-success
    tgAddText = <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
    tgRemoveClass = btn btn-danger alert-danger
    tgRemoveText = <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>


  • QFQ delivers a chart JavaScript lib: 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 accidentally 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>
        <script src="typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js"></script>
          $(function () {
            var ctx = document.getElementById("barchart");
            var barChart = new Chart(ctx, {
              type: 'bar',
                data: {
      10.tail =
      # 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( FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type
        head = datasets: [ {   data: [
        tail = ],  backgroundColor: "steelblue", label: "FormElements" } ]
        rsep = ,

Upload Form Simple

Table Person

Name Type
id int
name varchar(255)
pathFileNamePicture varchar(255)
pathFileNameAvatar varchar(255)
  • Form:

    • Name: UploadSimple
    • Table: Person
  • FormElements:

    • Name: name

      • Type: text
      • Label: Name
    • Name: pathFileNamePicture

      • Type: upload

      • Label: Picture

      • Parameter:

    • Name: pathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Parameter:


Upload Form Advanced 1

Table: Person

Name Type
id int
name varchar(255)

Table: Note

Name Type
id int
pId int
type varchar(255)
pathFileName varchar(255)
  • Form:

    • Name: UploadAdvanced1
    • Table: Person
  • FormElements

    • Name: name

      • Type: text
      • Label: Name
    • Name: mypathFileNamePicture

      • Type: upload

      • Label: Picture

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'picture', {{id:R0}}) }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
    • Name: mypathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='avatar' LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'avatar', {{id:R0}}) }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}

Upload Form Advanced 2

Table: Person

Name Type
id int
name varchar(255)
noteIdPicture int
noteIdAvatar int

Table: Note

Name Type
id int
pathFileName varchar(255)
  • Form:

    • Name: UploadAdvanced2
    • Table: Person
  • FormElements

    • Name: name

      • Type: text
      • Label: Name
    • Name: mypathFileNamePicture

      • Type: upload

      • Label: Picture

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        slaveId={{SELECT id FROM Note WHERE id={{noteIdPicture}} LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        sqlAfter={{UPDATE Person SET noteIdPicture={{slaveId}} WHERE id={{id:R0}} LIMIT 1
    • Name: mypathFileNameAvatar

      • Type: upload

      • Label: Avatar

      • Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }}

      • Parameter:

        slaveId={{SELECT id FROM Note WHERE id={{noteIdAvatar}} LIMIT 1}}
        sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }}
        sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}}
        sqlDelete={{DELETE FROM Note WHERE id={{slaveId}}  LIMIT 1}}
        sqlAfter={{UPDATE Person SET noteIdAvatar={{slaveId}} WHERE id={{id:R0}} LIMIT 1

Typeahead: SQL

Table: Person

Name Type
id int
name varchar(255)
  • Form:
    • Name: PersonNameTypeahead
    • Table: Person
  • FormElements
    • Name: name
      • Type: text
      • Label: Name
      • Parameter: typeAheadSql = SELECT name FROM Person WHERE name LIKE ? OR firstName LIKE ? LIMIT 100

Typeahead: LDAP with additional values

Table: Person

Name Type
id int
name varchar(255)
firstname varchar(255)
email varchar(255)
  • Form:

    • Name: PersonNameTypeaheadSetNames

    • Table: Person

    • Parameter:

      ldapServer =
      ldapBaseDn = ou=Addressbook,dc=example,dc=com
  • FormElements

    • Name: email

      • Class: native

      • Type: text

      • Label: Email

      • Note: Name: {{cn:LE}}<br>Email: {{mail:LE}}

      • dynamicUpdate: checked

      • Parameter:

        # Typeahead
        typeAheadLdapSearch = (|(cn=*?*)(mail=*?*))
        typeAheadLdapValuePrintf %s / %s’, cn, email
        typeAheadLdapIdPrintf  %s’, email
        # dynamicUpdate: show note
        ldapSearch = (mail={{email::alnumx}})
        ldapAttributes = cn, email
    • Name: fillLdapValues

      • Class: action

      • Type: afterSave

      • Parameter:

        ldapSearch = (mail={{email::alnumx}})
        ldapAttributes = cn, email
        sqlUpdate={{ UPDATE Person AS p SET'{{cn:L:alnumx:s}}' WHERE{{slaveId}} LIMIT 1 }}

Import/merge form

The form copyFormFromExt copies a form from table ExtForm / ExtFormElement to Form / FormElement. The import/merge form:

  • offers a drop down list with all forms of ExtForm,
  • an input element for the new form name,
  • create new
  • copied FormElements get the new
  • the copied form will be opened in the FormEditor.


  • Play (do all sql statements on your QFQ database, e.g. via mysql <dbname> < copyFormFromExt.sql or phpMyAdmin) the file <ext_dir>/Classes/Sql/copyFormFromExt.sql.

  • Insert a link/button ‘Copy form from ExtForm’ to open the import/merge form. A good place is the list of all forms (see form-editor). E.g.:

    10.head = {{'b|p:id={{pageAlias:T}}&form=copyFormFromExt|t:Copy form from ExtForm' AS _link }} ...

If there are several T3/QFQ instances and if forms should be imported frequently/easily, set up a one shot ‘import Forms from db xyz’ like:

10.sql = CREATE OR REPLACE table ExtForm SELECT * FROM <db xyz>.Form
20.sql = CREATE OR REPLACE table ExtFormElement SELECT * FROM <db xyz>.FormElement


QFQ content element

The QFQ extension is activated through tt-content records. One or more tt-content records per page are necessary to render forms and reports. Specify column and language per content record as wished.

The title of the QFQ content element will not be rendered. It’s only visible in the backend for orientation of the webmaster.

To display a report on any given TYPO3 page, create a content element of type ‘QFQ Element’ (plugin) on that page.

A simple example

Assume that the database has a table person with columns firstName and lastName. To create a simple list of all persons, we can do the following:

10.sql = SELECT firstName, lastName FROM person

The ‘10’ indicates a root level of the report (see section Structure). The expression ‘10.sql’ defines an SQL query for the specific level. When the query is executed, it will return a result having one single column name containing first and last name separated by a space character.

The HTML output, displayed on the page, resulting from only this definition, could look as follows:


I.e., QFQ will simply output the content of the SQL result row after row for each single level.

Format output: mix style and content

Variant 1: pure SQL

To format the upper example, just create additional columns:

10.sql = SELECT firstName, ", ", lastName, "<br>" FROM person

HTML output:

Doe, John<br>
Miller, Jane<br>
Star, Frank<br>
Variant 2: SQL plus QFQ helper

QFQ provides several helper functions to wrap rows and columns, or to separate them. In this example ‘fsep’=’field separate and ‘rend’ = row end:

10.sql = SELECT firstName, lastName FROM person 10.fsep = ‘, ‘ 10.rend = <br>

HTML output:

Doe, John<br>
Miller, Jane<br>
Star, Frank<br>

Check out all QFQ helpers under qfq_keywords.

Due to mixing style and content, this becomes harder to maintain with more complex layout.

Format output: separate style and content

The result of the query can be passed to the Twig template engine in order to fill a template with the data.:

10.sql = SELECT firstName, lastName FROM person
10.twig = {% for row in result %}
    {{ row.lastName }}, {{ row.firstName }<br />
{% endfor %}

HTML output:

Doe, John<br>
Miller, Jane<br>
Star, Frank<br>

Check out using-twig.

Syntax of report

All root level queries will be fired in the order specified by ‘level’ (Integer value).

For each row of a query (this means all queries), all subqueries will be fired once.

  • E.g. if the outer query selects 5 rows, and a nested query select 3 rows, than the total number of rows are 5 x 3 = 15 rows.

There is a set of variables that will get replaced before (‘count’ also after) the SQL-Query gets executed:

Variables from specific stores.

{{<name>:R}} - use case of the above generic definition. See also access-column-values.

Similar to {{<name>:R}} but more specific. There is no sanitize class, escape mode or default value.
{{<level>.line.count}} - Current row index
This variable is specific, as it will be replaced before the query is fired in case of <level> is an outer/previous level or it will be replaced after a query is fired in case <level> is the current level.
Total rows (MySQL num_rows for SELECT and SHOW, MySQL affected_rows for UPDATE and INSERT.
Last insert id for INSERT.
If the content of <level> have been stored, e.g. <level>.content=hide.

{{<level>.line.altCount}} - Like ‘line.count’ but for ‘alt’ query.

{{<level>.line.altTotal}} - Like ‘’ but for ‘alt’ query.

{{<level>.line.altInsertId}} - Like ‘line.insertId’ but for ‘alt’ query.

See Variable for a full list of all available variables.

Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW, REPLACE

Only SELECT and SHOW queries will fire subqueries.

Processing of the resulting rows and columns:

  • In general, all columns of all rows will be printed out sequentially.

  • On a per column base, printing of columns can be suppressed by starting the column name with an underscore ‘_’. E.g. SELECT id AS _id.

    This might be useful to store values, which will be used later on in another query via the {{id:R}} or {{<level>.columnName}} variable. To suppress printing of a column, use a underscore as column name prefix. E.g. SELECT id AS _id

Reserved column names have a special meaning and will be processed in a special way. See Processing of columns in the SQL result for details.

There are extensive ways to wrap columns and rows. See Wrapping rows and columns: Level

Using Twig

How to write Twig templates is documented by the Twig Project.

QFQ passes the result of a given query to the corresponding Twig template using the Twig variable result. So templates need to use this variable to access the result of a query.

Specifying a Template

By default the string passed to the twig-key is used as template directly, as shown in the simple example above:

10.twig = {% for row in result %}
    {{ row.lastName }}, {{ row.firstName }<br />
{% endfor %}

However, if the string starts with file:, the template is read from the file specified.:

10.twig = file:table_sortable.html.twig

The file is searched relative to <site path> and if the file is not found there, it’s searched relative to QFQ’s twig_template folder where the included base templates are stored.

The following templates are available:

A basic table with column headers, sorting and column filters using tablesorter and bootstrap.
A template to display the values of a single record in a vertical table.

Json Decode

A String can be JSON decoded in Twig the following way:

{% set decoded = '["this is one", "this is two"]' | json_decode%}

This can then be used as a normal object in Twig:

{{ decoded[0] }}

will render this is one.

Available Store Variables

QFQ also provides access to the following stores via the template context.

  • record
  • sip
  • typo3
  • user
  • system

All stores are accessed using their lower case name as attribute of the context variable store. The active Typo3 front-end user is therefore available as:

{{ store.typo3.feUser }}


The following block shows an example of a QFQ report.

10.sql selects all users who have been assigned files in our file tracker.

10.10 then selects all files belonging to this user, prints the username as header and then displays the files in a nice table with links to the files.

10.sql = SELECT assigned_to AS _user FROM file_tracker
            WHERE assigned_to IS NOT NULL
            GROUP BY _user
            ORDER BY _user

10.10.sql = SELECT id, path_scan FROM file_tracker
WHERE assigned_to = '{{user:R}}'
10.10.twig = <h2>{{ store.record.user }}</h2>
<table id="file-list" class="table table-hover tablesorter" id="{{pageAlias:T}}-twig">
  {% for row in result %}
      <td>{{ }}</td>
      <td>{{ ("d:|M:pdf|s|t:"~ row.path_scan ~"|F:" ~ row.path_scan ) | qfqlink }}</td>
  {% endfor %}

Debug the bodytext

The parsed bodytext could be displayed by activating ‘showDebugInfo’ (Debug) and specifying

debugShowBodyText = 1

A small symbol with a tooltip will be shown, where the content record will be displayed on the webpage. Note: Debug information will only be shown with showDebugInfo: yes in configuration.

Inline Report editing

For quick changes it can be bothersome to go to the TYPO3 backend to update the page content and reload the page. For this reason, QFQ offers an inline report editing feature whenever there is a TYPO3 BE user logged in. A small link symbol will appear on the right-hand side of each report record. Please note that the TYPO3 Frontend cache is also deleted upon each inline report save.

In order for the inline report editing to work, QFQ needs to be able to access the T3 database. This database is assumed to be accessible with the same credentials as specified with indexQfq.


A report can be divided into several levels. This can make report definitions more readable because it allows for splitting of otherwise excessively long SQL queries. For example, if your SQL query on the root level selects a number of person records from your person table, you can use the SQL query on the second level to look up the city where each person lives.

See the example below:

10.sql = SELECT id AS _pId, CONCAT(firstName, " ", lastName, " ") AS name FROM person
10.rsep = <br>

10.10.sql = SELECT CONCAT(postal_code, " ", city) FROM address WHERE pId = {{10.pId}}
10.10.rbeg = (
10.10.rend = )

This would result in:

John Doe (3004 Bern)
Jane Miller (8008 Zürich)
Frank Star (3012 Bern)

Text across several lines

To get better human readable SQL queries, it’s possible to split a line across several lines. Lines with keywords are on their own (QFQ Keywords (Bodytext) start a new line). If a line is not a ‘keyword’ line, it will be appended to the last keyword line. ‘Keyword’ lines are detected on:

  • <level>.<keyword> =
  • {
  • <level>[.<sub level] {


10.sql = SELECT 'hello world'
           FROM mastertable
10.tail = End

20.sql = SELECT 'a warm welcome'
           'some additional', 'columns'
           FROM another_table
           WHERE id>100

20.head = <h3>
20.tail = </h3>

Join mode: SQL

This is the default. All lines are joined with a space in between. E.g.:

10.sql = SELECT 'hello world'
           FROM mastertable

Results to: 10.sql = SELECT 'hello world' FROM mastertable

Notice the space between “…world’” and “FROM …”.

Join mode: strip whitespace

Ending a line with a \ strips all leading and trailing whitespaces of that line joins the line directly (no extra space in between). E.g.:

10.sql = SELECT 'hello world', 'd:final.pdf \
                                |p:id=export  \
                                |t:Download' AS _pdf \

Results to: 10.sql = SELECT 'hello world', 'd:final.pdf|p:id=export|t:Download' AS _pdf

Note: the \ does not force the joining, it only removes the whitespaces.

To get the same result, the following is also possible:

10.sql = SELECT 'hello world', CONCAT('d:final.pdf'
                                '|t:Download') AS _pdf

Nesting of levels

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 = ...

Leading / trailing spaces

By default, leading or trailing whitespaces are removed from strings behind ‘=’. E.g. ‘rend = test ‘ becomes ‘test’ for rend. To prevent any leading or trailing spaces, surround them by using single or double ticks. Example:

10.sql = SELECT name FROM Person
10.rsep = ' '
10.head = "Names: "

Braces character for nesting

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

Per QFQ tt-content record, only one type of nesting braces can be used.

Be careful to:

  • write nothing else than whitespaces/newline behind an open brace

  • the closing brace has to be alone on a line:

    10.sql = SELECT 'Yearly Report'
    20 {
          sql = SELECT companyName FORM Company LIMIT 1
          head = <h1>
          tail = </h1>
    30 {
          sql = SELECT depName FROM Department
          head = <p>
          tail = </p>
          5 {
                sql = SELECT 'detailed information for department'
                1.sql = SELECT name FROM Person LIMIT 7
                1.head = Employees:
    30.5.tail = More will follow
           sql = SELECT 'A query with braces on their own'

Access column values

Columns of the upper / outer level result can be accessed via variables in two ways

  • STORE_RECORD: {{pId:R}}
  • Level Key: {{10.pId}}

The STORE_RECORD will always be merged with previous content. The Level Keys are unique.

Multiple columns, with the same column name, can’t be accessed individually. Only the last column is available.

Retrieving the final value of special-column-names is possible via ‘{{&<column>:R}}. Example:

10.sql = SELECT 'p:home&form=Person|s|b:success|t:Edit' AS _link
10.20.sql = SELECT '{{link:R}}', '{{&link:R}}'

The first column of row 10.20 returns ‘p:home&form=Person|s|b:success|t:Edit’,the second column returns ‘<span class=”btn btn-success”><a href=”?home&s=badcaffee1234”>Edit</a></span>’.


10.sql= SELECT AS _pId, FROM Person AS p
10.5.sql = SELECT, 'dummy' AS _pId FROM Address AS adr WHERE adr.pId={{pId:R}}
10.5.20.sql = SELECT '{{pId:R}}'
10.10.sql = SELECT '{{pId:R}}'

The line ‘10.10’ will output ‘dummy’ in cases where there is at least one corresponding address. If there are no addresses (all persons) it reports the person id. If there is at least one address, it reports ‘dummy’, cause that’s the last stored content.

Example ‘level’:

10.sql= SELECT AS _pId, FROM Person AS p
10.5.sql = SELECT, 'dummy' AS _pId FROM Address AS adr WHERE adr.pId={{10.pId}}
10.5.20.sql = SELECT '{{10.pId}}'
10.10.sql = SELECT '{{10.pId}}'

Notes to the level:

Levels A report is divided into levels. The Example has levels 10, 20, 30, 30.5, 30.5.1, 50
Qualifier A level is divided into qualifiers 30.5.1 has 3 qualifiers 30, 5, 1
Root levels Is a level with one qualifier. E.g.: 10
Sub levels Is a level with more than one qualifier. E.g. levels 30.5 or 30.5.1
Child The level 30 has one child and child child: 30.5 and 30.5.1
Example 10, 20, 30, 50* are root level and will be completely processed one after each other. 30.5 will be executed as many times as 30 has row numbers. 30.5.1 will be executed as many times as 30.5 has row numbers.

Report Example 1:

# Displays current date

# Show all students from the person table
20.sql = SELECT AS pId, p.firstName, " - ", p.lastName FROM person AS p WHERE p.typ LIKE "student"

# Show all the marks from the current student ordered chronological
20.25.sql = SELECT e.mark FROM exam AS e WHERE e.pId={{20.pId}} ORDER BY

# This query will never be fired, cause there is no direct parent called 20.30.
20.30.10.sql = SELECT 'never fired'

Wrapping rows and columns: Level

Order and nesting of queries, will be defined with a TypoScript-alike syntax:

level.sublevel1.subsublevel2. ...
  • Each ‘level’ directive needs a final key, e.g: 20.30.10. sql.
  • A key sql is necessary in order to process a level.

See all QFQ Keywords (Bodytext).

Processing of columns in the SQL result

  • The content of all columns of all rows will be printed sequentially, without separator (except one is defined).
  • Rows with special-column-names will be rendered internally by QFQ and the QFQ output of such processing (if there is any) is taken as the content.

Special column names


Twig: respect that the ‘special column name’-columns are rendered before Twig becomes active. The recommended way by using Twig is not to use special column names at all. Use the Twig version qfqLink.

QFQ typically don’t care about the content of any SQL-Query - it just copy the content to the output (=Browser).

One exception are columns, whose name starts with ‘_’. E.g.:

10.sql = SELECT 'All', 'cats' AS red, 'are' AS _green, 'grey in the night' AS _link
  • The first and second column are regular columns. No QFQ processing.
  • The third column (alias name ‘green’) is no QFQ special column name, but has an ‘_’ at the beginning: this column content will be hidden.
  • The fourth column (alias name ‘link’) uses a QFQ special column name. Here, only in this example, it has no further meaning.


  • Special column names always start with ‘_’.
  • Columns starting with a ‘_’ but not defined as as QFQ special column name are hidden(!) - in other words: they are not printed as output.
  • SQL hint: If there is no column alias defined, the column name becomes the value of the first row. E.g. if the first selected row contains a ‘_’ as the first character, the column name becomes ‘_….’ - which will be hidden! To be safe: always define an alias!
  • Access to ‘hidden’ columns via {{<level>.<column>}} or {{<column>:R}} are possible and often used.
  • Important: to reference a column, the name has to be given without ‘_’. E.g. SELECT ‘cat’ AS _pet will be used with {{pet:R}} (notice the missing ‘_’).
  • For ‘special column names’: the column name together with the value controls how QFQ processes the column.
Reserved column name Purpose
_link column-link - Build links with different features.
_pageX or _PageX column_pageX - Shortcut version of the link interface for fast creation of internal links. The column name is composed of the string page/Page and a optional character to specify the type of the link.
_download download - single file (any type) or concatenate multiple files (PDF, ZIP)
_pdf, _file, _zip _Pdf, _File, _Zip column_pdf - Shortcut version of the link interface for fast creation of download links. Used to offer single file download or to concatenate several PDFs and printout of websites to one PDF file.
_savePdf column-save-pdf - pre render PDF files
_excel excel-export - creates Excel exports based on QFQ Report queries, optional with pre uploaded Excel template files
_yank copyToClipboard. Shortcut version of the link interface
_mailto column_mailto - Build email links. A click on the link will open the default mailer. The address is encrypted via JS against email bots.
_sendmail column_sendmail - Send emails.
_exec column_exec - Run batch files or executables on the webserver.
_vertical column_vertical - Render Text vertically. This is useful for tables with limited column width.
_img column_img - Display images.
_bullet Display a blue/gray/green/pink/red/yellow bullet. If none color specified, show nothing.
_check Display a blue/gray/green/pink/red/yellow checked sign. If none color specified, show nothing.
_nl2br All newline characters will be converted to <br>.
_striptags HTML Tags will be stripped.
_htmlentities Characters will be encoded to their HTML entity representation.
_fileSize Show file size of a given file
_mimeType Show mime type of a given file
_thumbnail thumbnail - Create thumbnails on the fly. See column-thumbnail.
_monitor column-monitor - Constantly display a file. See column-monitor.
_XLS Used for Excel export. Append a newline character at the end of the string. Token must be part of string. See excel-export.
_XLSs Used for Excel export. Prepend the token ‘s=’ and append a newline character at the string. See excel-export.
_XLSb Used for Excel export. Like ‘_XLSs’ but encode the string base64. See excel-export.
_XLSn Used for Excel export. Prepend ‘n=’ and append a newline character around the string. See excel-export.
_+<html-tag attributes> The content will be wrapped with ‘<html-tag attributes>’. Example: SELECT ‘example’ AS ‘_+a href=””’ creates ‘<a href=””>example</a>’
_=<varname> The content will be saved in store ‘user’ under ‘varname’. Retrieve it later via {{varname:U}}. Example: ‘SELECT “Hello world” AS _=text’. See STORE_USER, store_user_examples
_<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.

Render mode

The following table might be hard to read - but it’s really useful to understand. It solves a lot of different situations. If there are no special condition (like missing value, or suppressed links), render mode 0 is sufficient. But if the URL text is missing, or the URL is missing, OR the link should be rendered in sql row 1-10, but not 5, then render mode might dynamically control the rendered link.

  • Column Mode is the render mode and controls how the link is rendered.
Mode Given: url & text Given: only url Given: 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, independently of whether there is a url or not
2 <a href=url>text</a>     no link if text is empty
3 text url text
no link, only text or image, incl. Optional tooltip. For Bootstrap buttons
r:3 will set the button to disable and no link/sip is rendered.
4 url url text no link, show text, if text is empty, show url, incl. optional tooltip
5       nothing at all
6 pure text   pure text no link, pure text
7 pure url pure url   no link, pure url


10.sql = SELECT CONCAT('u:', p.homepage, IF(p.showHomepage='yes', '|r:0', '|r:5') ) AS _link FROM Person AS p


An easy way to switch between different options of rendering a link, incl. Bootstrap buttons, is to use the render mode.

  • no render mode or ‘r:0’ - the full functional link/button.
  • ‘r:3’ - the link/button is rendered with text/image/glyph/tooltip … but without a HTML a-tag! For Bootstrap button, the button get the ‘disabled’ class.
  • ‘r:5’ - no link/button at all.



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 separated 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’. To hide the second button: ‘-‘
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.


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]

The colum name is composed of the string page and a trailing character to specify the type of the link.


10.sql = SELECT "[options]" AS _page[<link type>]

with: [options] = [p:<page & param>][|t:<text>][|o:<tooltip>][|q:<question parameter>][|c:<class>][|g:<target>][|r:<render mode>]

<link type> = c,d,e,h,i,n,s
column name Purpose default value of question parameter Mandatory parameters
_page Internal link without a grafic empty p:<pageId/pageAlias>[&param]
_pagec Internal link without a grafic, with question Please confirm! p:<pageId/pageAlias>[&param]
_paged Internal link with delete icon (trash) Delete record ?
U:form=<formname>&r=<record id> or
U:table=<tablename>&r=<record id>
_pagee Internal link with edit icon (pencil) empty p:<pageId/pageAlias>[&param]
_pageh Internal link with help icon (question mark) empty p:<pageId/pageAlias>[&param]
_pagei Internal link with information icon (i) empty p:<pageId/pageAlias>[&param]
_pagen Internal link with new icon (sheet) empty p:<pageId/pageAlias>[&param]
_pages Internal link with show icon (magnifier) empty p:<pageId/pageAlias>[&param]
  • All parameter are optional.
  • Optional set of predefined icons.
  • Optional set of dialog boxes.
Parameter Description Default value Example
<page> TYPO3 page id or page alias. The current page: {{pageId}} 45 application application&N_param1=1045
<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  
<question> If there is a question text given, an alert will be opened. Only if the user clicks on ‘ok’, the link will be called Expected “=” to follow “see”  
<class> CSS Class for the <a> tag    
<target> Parameter for HTML ‘target=’. F.e.: Opens a new window empty P
<render mode> Show/render a link at all or not. See render-mode    
<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.


10.sql = SELECT "U:table=<tablename>&r=<record id>|q:<question>|..." AS _paged
10.sql = SELECT "U:form=<formname>&r=<record id>|q:<question>|..." AS _paged

If the record to delete contains column(s), whose column name 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.


10.sql = SELECT 'U:table=Person&r=123|q:Do you want delete John Doe?' AS _paged
10.sql = 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>[&param=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>[&param=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged.
"[form=<form name>&r-<record id>[&param=value&...] | [text] | [tooltip] | [question parameter] | [class] | [render mode]" as _Paged.

Column: _vertical

Render text vertically. This is useful for tables with limited column width. The vertical rendering is achieved via CSS tranformations (rotation) defined in the style attribute of the wrapping tag. You can optionally specify the rotation angle.


10.sql = SELECT "<text>|[<angle>]" 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

The text is surrounded by some HTML tags in an effort to make other elements position appropriately around it. This works best for angles close to 270 or 90.

Minimal Example

10.sql = SELECT "Hallo" AS _vertical
20.sql = SELECT "Hallo|90" AS _vertical
20.sql = SELECT "Hallo|-75" AS _vertical

Column: _mailto

Easily create Email links.


10.sql = SELECT "<email address>|[<link text>]" AS _mailto
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 link text. none

Minimal Example

10.sql = SELECT "" AS _mailto

Advanced Example

10.sql = SELECT "|John Doe" AS _mailto

Column: _sendmail


    [|A:<flag autosubmit: on/off>][|g:<grId>][|x:<xId>][|y:<xId2>][|z:<xId3>][|h:<mail header>]
    [|e:<subject encode: encode/decode/none>][E:<body encode: encode/decode/none>][|mode:html]
    [|C][d:<filename of the attachment>][|F:<file to attach>][|u:<url>][|p:<T3 uri>]

The following parameters can also be written as complete words for ease of use:

    [|autosubmit:<on/off>][|grid:<grid>][|xid:<xId>][|xid2:<xId2>][|xid3:<xId3>][|header:<mail header>]

Send emails. Every mail will be logged in the table mailLog. Attachments are supported.


10.sql = SELECT "||s:Reminder tomorrow|b:Please dont miss the meeting tomorrow" AS _sendmail
10.sql = SELECT "||s:Reminder tomorrow|b:Please dont miss the meeting tomorrow|A:off|g:1|x:2|y:3|z:4" AS _sendmail
short / long
Parameter Description Required
email FROM: Sender of the email. Optional: ‘realname <>’ yes
email[,email] TO: Comma separated list of receiver email addresses. Optional: realname <> yes
email[,email] CC: Comma separated list of receiver email addresses. Optional: ‘realname <>’ yes
email[,email] BCC: Comma separated list of receiver email addresses. Optional: ‘realname <>’ yes
REPLY-TO:email Reply-to: Email address to reply to (if different from sender) yes
Subject Subject: Subject of the email yes
Body Body: Message - see also: html-formatting yes
Mail header Custom mail header: Separate multiple header with \r\n yes
F Attach file Attachment: File to attach to the mail. Repeatable.  
u Attach created PDF of a given URL Attachment: Convert the given URL to a PDF and attach it the mail. Repeatable.  
p Attach created PDF of a given T3 URL Attachment: Convert the given URL to a PDF and attach it the mail. Repeatable.  
d Filename of the attachment Attachment: Useful for URL to PDF converted attachments. Repeatable.  
C Concat multiple F|p|u| together
Attachment: All following (until the next ‘C’) ‘F|p|u’ concatenated to one attachment.
flagAutoSubmit ‘on’ / ‘off’ If ‘on’ (default), add mail header ‘Auto-Submitted: auto-send’ - suppress OoO replies yes
grId Will be copied to the mailLog record. Helps to setup specific logfile queries yes
xId Will be copied to the mailLog record. Helps to setup specific logfile queries yes
xId2 Will be copied to the mailLog record. Helps to setup specific logfile queries yes
xId3 Will be copied to the mailLog record. Helps to setup specific logfile queries yes
e encode|decode|none Subject: will be htmlspecialchar() encoded, decoded (default) or none (untouched)  
E encode|decode|none Body: will be htmlspecialchar() encoded, decoded (default) or none (untouched).  
mode html Body: will be send as a HTML mail.  
  • e|E: By default, QFQ stores values ‘htmlspecialchars()’ encoded. If such values have to send by email, the html entities are unwanted. Therefore the default setting for ‘subject’ und ‘body’ is to decode the values via ‘htmlspecialchars_decode()’. If this is not wished, it can be turned off by e=none and/or E=none.

Minimal Example

10.sql = SELECT "||s:Latest News|b:The new version is now available." AS _sendmail

This will send an email with subject Latest News from to

Advanced Examples

10.sql = SELECT ",Firstname Lastname <>, Firstname Lastname <>| \\
       |s:Latest News|b:The new version is now available.||A:on|g:101|x:222||" AS _sendmail

This will send an email with subject Latest News from 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.

For debugging, please check REDIRECT_ALL_MAIL_TO.

Mail Body HTML Formatting

In order to send an email with HTML formatting, such as bold text or bullet lists, specify ‘mode=html’. The subsequent contents will be interpreted as HTML and is rendered correctly by most email programs.


The following options are provided to attach files to an email:

Token Example Comment
F F:fileadmin/file3.pdf Single file to attach
u… A URL, will be converted to a PDF and than attached.
p p:?id=export&r=123&_sip=1 A SIP protected local T3 page. Will be converted to a PDF and than attached.
d d:myfile.pdf Name of the attachment in the email.
C C|u:|F:file1.pdf|C|F:file2.pdf Concatenate all named sources to one PDF file. The souces has to be PDF files or a web page, which will be converted to a PDF first.

Any combination (incl. repeating them) are possible. Any source will be added as a single attachment.

Optional any number of sources can be concatenated to a single PDF file: ‘C|F:<file1>|F:<file2>|p:export&a=123’.

Examples in Report:

# One file attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf" AS _sendmail

# Two files attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf|F:fileadmin/detail.pdf" AS _sendmail

# Two files and a webpage (converted to PDF) are attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|F:fileadmin/summary.pdf|F:fileadmin/detail.pdf|p:?id=export&r=123|d:person.pdf" AS _sendmail

# Two webpages (converted to PDF) are attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|p:?id=export&r=123|d:person123.pdf|p:?id=export&r=234|d:person234.pdf" AS _sendmail

# One file and two webpages (converted to PDF) are *concatenated* to one PDF and attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|C|F:fileadmin/summary.pdf|p:?id=export&r=123|p:?id=export&r=234|d:complete.pdf" AS _sendmail

# One T3 webpage, protected by a SIP, are attached.
10.sql = SELECT "||s:Latest News|b:The new version is now available.|p:?id=export&r=123&_sip=1|d:person123.pdf" AS _sendmail

Column: _img

Renders images. Allows to define an alternative text and a title attribute for the image. Alternative text and title text are optional.

  • If no alternative text is defined, an empty alt attribute is rendered in the img tag (since this attribute is mandatory in HTML).
  • If no title text is defined, the title attribute will not be rendered at all.


10.sql = SELECT "<path to image>|[<alt text>]|[<title text>]" AS _img
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
<titletext> Text that will be set as image title in the title attribute of the img tag. no title attribute rendered

Minimal Example

10.sql = SELECT "fileadmin/img/img.jpg" AS _img

Advanced Examples

10.sql = SELECT "fileadmin/img/img.jpg|Aternative Text" AS _img            # alt="Alternative Text, no title
20.sql = SELECT "fileadmin/img/img.jpg|Aternative Text|" AS _img           # alt="Alternative Text, no title
30.sql = SELECT "fileadmin/img/img.jpg|Aternative Text|Title Text" AS _img # alt="Alternative Text, title="Title Text"
40.sql = SELECT "fileadmin/img/img.jpg|Alternative Text" AS _img           # alt="Alternative Text", no title
50.sql = SELECT "fileadmin/img/img.jpg" AS _img                            # empty alt, no title
60.sql = SELECT "fileadmin/img/img.jpg|" AS _img                           # empty alt, no title
70.sql = SELECT "fileadmin/img/img.jpg||Title Text" AS _img                # empty alt, title="Title Text"
80.sql = SELECT "fileadmin/img/img.jpg||" AS _img                          # empty alt, no title

Column: _exec

Run any command on the web server.

  • The command is run via web server, so with the uid of the web server.

  • The current working directory is the current web instance (e.g. /var/www/html) .

  • All text send to ‘stdout’ will be returned.

  • Text send to ‘stderr’ is not returned at all.

  • If ‘stderr’ should be shown, redirect the output:

    SELECT 'touch /root 2>&1' AS _exec
  • If ‘stdout’ / ‘stderr’ should not be displayed, redirect the output:

    SELECT 'touch /tmp >/dev/null' AS _exec
    SELECT 'touch /root 2>&1 >/dev/null' AS _exec
  • Multiple commands can be concatenated by ;:

    SELECT 'date; date' AS _exec
  • If the return code is not 0, the string ‘[<rc>] ‘, will be prepended.

  • If it is not wished to see the return code, just add true to fake rc of 0 (only the last rc will be reported):

    SELECT 'touch /root; true' AS _exec


Parameter Description Default value
<command> The command that should be executed on the server. none

Minimal Examples

10.sql = SELECT "ls -s" AS _exec
20.sql = SELECT "./" AS _exec

Column: _pdf | _file | _zip

Detailed explanation: download

Most of the other Link-Class attributes can be used to customize the link.

10.sql = SELECT "[options]" AS _pdf, "[options]" AS _file, "[options]" AS _zip

with: [options] = [d:<exportFilename][|p:<params>][|U:<params>][|u:<url>][|F:file][|t:<text>][|a:<message>][|o:<tooltip>][|c:<class>][|r:<render mode>]
  • Parameter are position independent.

  • <params>: see download-parameter-files

  • For column _pdf and _zip, the element sources p:…, U:…, u:…, F:… might repeated multiple times.

  • Example:

    10.sql = SELECT "F:fileadmin/test.pdf" as _pdf,  "F:fileadmin/test.pdf" as _file,  "F:fileadmin/test.pdf" as _zip
    10.sql = SELECT "p:id=export&r=1" as _pdf,  "p:id=export&r=1" as _file,  "p:id=export&r=1" as _zip
    10.sql = SELECT "t:Download PDF|F:fileadmin/test.pdf" as _pdf,  "t:Download PDF|F:fileadmin/test.pdf" as _file,  "t:Download ZIP|F:fileadmin/test.pdf" as _zip
    10.sql = SELECT "t:Download PDF|p:id=export&r=1" as _pdf,  "t:Download PDF|p:id=export&r=1" as _file,  "t:Download ZIP|p:id=export&r=1" as _zip
    10.sql = SELECT "d:complete.pdf|t:Download PDF|F:fileadmin/test1.pdf|F:fileadmin/test2.pdf" as _pdf, "|t:Download ZIP|F:fileadmin/test1.pdf|F:fileadmin/test2.pdf" as _zip
    10.sql = SELECT "d:complete.pdf|t:Download PDF|F:fileadmin/test.pdf|p:id=export&r=1|" AS _pdf

Column: _savePdf

Generated PDFs can be stored directly on the server with this functionality. The link query consists of the following parameters:

  • One or more element sources (such as F:, U:, p:, see download-parameter-files), including possible wkhtmltopdf parameters
  • The export filename and path as d: - for security reasons, this path has to start with fileadmin/


  • Please note that this option does not render anything in the front end, but is executed each time it is parsed. You may want to add a check to prevent multiple execution.
  • It is not advised to generate the filename with user input for security reasons.
  • If the target file already exists it will be overwriten. To save individual files, choose a new filename, for example by adding a timestamp.


SELECT "d:fileadmin/result.pdf|F:fileadmin/_temp_/test.pdf" AS _savePdf
SELECT "d:fileadmin/result.pdf|F:fileadmin/_temp_/test.pdf|U:id=test&--orientation=landscape" AS _savePdf

Column: _thumbnail

For file T:<pathFileName> a thumbnail will be rendered, saved (to be reused) and a HTML <img> tag is returned, With the SIP encoded thumbnail.

The thumbnail:

  • Size is specified via W:<dimension>. The file is only rendered once and subsequent access is delivered via a local QFQ cache.
  • Will be rendered, if the source file is newer than the thumbnail or if the thumbnail dimension changes.
  • The caching is done by building the MD5 of pathFileName and thumbnail dimension.
  • Of multi page files like PDFs, the first page is used as the thumbnail.

All file formats, which ‘convert’ ImageMagick ( supports, can be used. Office file formats are not supported. Due to speed and quality reasons, SVG files will be converted by inkscape. If a file format is not known, QFQ tries to show a corresponding file type image provided by Typo3 - such an image is not scaled.

In configuration the exact location of convert and inkscape can be configured (optional) as well as the directory names for the cached thumbnails.

Token Example Comment
T T:fileadmin/file3.pdf File render a thumbnail
W W:200x, W:x100, W:200x100 Dimension of the thumbnail: ‘<width>x<height>. Both parameter are otional. If non is given the default is W:150x
s s:1, s:0 Optional. Default: s:1. If SIP is enabled, the rendered URL is a link via api/download.php?... Else a direct pathFileName.
r r:7 Render Mode. Default ‘r:0’. With ‘r:7’ only the url will be delivered.

The render mode ‘7’ is useful, if the URL of the thumbnail have to be used in another way than the provided html-‘<img>’ tag. Something like <body style=”background-image:url(bgimage.jpg)”> could be solved with SELECT “<body style=”background-image:url(“, ‘T:fileadmin/file3.pdf’ AS _thumbnail, ‘)”>’


# SIP protected, IMG tag, thumbnail width 150px
10.sql = SELECT 'T:fileadmin/file3.pdf' AS _thumbnail

# SIP protected, IMG tag, thumbnail width 50px
20.sql = SELECT 'T:fileadmin/file3.pdf|W:50' AS _thumbnail

# No SIP protection, IMG tag, thumbnail width 150px
30.sql = SELECT 'T:fileadmin/file3.pdf|s:0' AS _thumbnail

# SIP protected, only the URL to the image, thumbnail width 150px
40.sql = SELECT 'T:fileadmin/file3.pdf|s:1|r:7' AS _thumbnail


By default, the thumbnail directories are never cleaned. It’s a good idea to install a cronjob which purges all files older than 1 year:

find /path/to/files -type f -mtime +365 -delete


Public thumbnails are rendered at the time when the T3 QFQ record is executed. Secure thumbnails are rendered when the ‘download.php?s=…’ is called. The difference is, that the ‘public’ thumbnails blocks the page load until all thumbnails are rendered, instead the secure thumbnails are loaded asynchonous via the browser - the main page is already delivered to browser, all thumbnails appearing after a time.

A way to pre render thumbnails, is a periodically called (hidden) T3 page, which iterates over all new uploaded files and triggers the rendering via column _thumbnail.

Thumbnail: secure

Mode ‘secure’ is activated via enabling SIP (s:1, default). The thumbnail is saved under the path thumbnailDirSecure as configured in configuration.

The secure path needs to be protected against direct file access by the webmaster / webserver configuration too.

QFQ returns a HTML ‘img’-tag:

<img src="api/download.php?s=badcaffee1234">

Thumbnail: public

Mode ‘public’ has to be explicit activated by specifying s:0. The thumbnail is saved under the path thumbnailDirPublic as configured in configuration.

QFQ returns a HTML ‘img’-tag:

<img src="{{thumbnailDirPublic:Y}}/<md5 hash>.png">

Column: _monitor

Detailed explanation: monitor


10.sql = SELECT 'file:<filename>|tail:<number of last lines>|append:<0 or 1>|interval:<time in ms>|htmlId:<id>' AS _monitor
Parameter Description Default value/behaviour
<filename> The path to the file. Relative to T3 installation directory or absolute. none
<tail> Number of last lines to show 30
<append> 0: Retrieved content replaces current. 1: Retrieved content will be added to current. 0
<htmlId> Reference to HTML element to whose content replaced by the retrieve one. monitor-1

Copy to clipboard

Token Example Comment
y[:<content>] y, y:some content Initiates ‘copy to clipboard’ mode. Source might given text or page or url
F:<pathFileName> F:fileadmin/protected/data.R pathFileName in DocumentRoot


10.sql = SELECT 'y:hello world (yank)|t:content direct (yank)' AS _yank,
                'y:hello world (link)|t:content direct (link)' AS _link,
                CONCAT('F:', p.pathFileName,'|t:File (yank)|o:', p.pathFileName) AS _yank,
                CONCAT('y|F:', p.pathFileName,'|t:File (link)|o:', p.pathFileName) AS _link
            FROM Person AS p

Special SQL Functions (prepared statements)

QBAR: Escape QFQ Delimiter

The SQL function QBAR(text) replaces “|” with “\|” in text to prevent conflicts with the QFQ special column notation. In general this function should be used when there is a chance that unplanned ‘|’-characters occur.


10.sql = SELECT CONCAT('p:notes|t:Information: ', QBAR(Note.title), '|b') AS _link FROM Note

In case ‘Note.title’ contains a ‘|’ (like ‘fruit | numbers’), it will confuse the ‘… AS _link’ class. Therefore it’s necessary to ‘escape’ (adding a ‘’ in front of the problematic character) the bar which is done by using QBAR().

QNL2BR: Convert newline to HTML ‘<br>’

The SQL function QNL2BR(text) replaces LF or CR/LF by <br>. This can be used for data (containing LF) to output on a HTML page with correctly displayed linefeed.


10.sql = SELECT QNL2BR(Note.title) FROM Note

One possibility how LF comes into the database is with form elements of type textarea if the user presses enter inside.

QMORE: Truncate Long Text - more/less

The SQL function QMORE(text, n) truncates text if it is longer than n characters and adds a “more..” button. If the “more…” button is clicked, the whole text is displayed. The stored procedure QMORE() will inject some HTML/CSS code.


10.sql = SELECT QMORE("This is a text which is longer than 10 characters", 10)


This is a `more..`

QIFEMPTY: if empty show token

The SQL function QIFEMPTY(input, token) returns ‘token’ if ‘input’ is ‘empty string’ / ‘0’ / ‘0000-00-00’ / ‘0000-00-00 00:00:00’.


10.sql = SELECT QIFEMPTY('hello world','+'), QIFEMPTY('','-')


hello world-

QDATE_FORMAT: format a timestamp, show ‘-‘ if empty

The SQL function QDATE_FORMAT(timestamp) returns ‘ hh:mm’, if ‘timestamp’ is 0 returns ‘-‘


10.sql = SELECT QDATE_FORMAT( '2019-12-31 23:55:41' ), ' / ', QDATE_FORMAT( 0 ), ' / ', QDATE_FORMAT( '' )


31.12.2019 23:55 / - / -

strip_tags: strip html tags

The SQL function strip_tags(input) returns ‘input’ without any HTML tags.


10.sql = SELECT strip_tags('<a href=""><b>my name</b> <i>is john</i></a> - end of sentence')


my name is john - end of sentence


Download offers:

  • Single file - download a single file (any type),
  • PDF create - one or concatenate several files (uploaded) and/or web pages (=HTML to PDF) into one PDF output file,
  • ZIP archive - filled with several files (‘uploaded’ or ‘HTML to PDF’-converted).
  • Excel - created from scratch or fill a template xlsx with database values.

The downloads are SIP protected. Only the current user can use the link to download files.

By using the _link column name:

  • the option d:… initiate creating the download link and optional specifies an export filename,
  • the optional M:… (Mode) specifies the export type (file, pdf, zip, export),
  • setting s:1 is recommended for the download function (file / path name is hidden to the user),
  • the alttext a:… specifies a message in the download popup.

By using _pdf, _Pdf, _file, _File, _zip, _Zip, _excel as column name, the options d, M and s will be set.

All files will be read by PHP - therefore the directory might be protected against direct web access. This is the preferred option to offer secure downloads via QFQ.

In case the download needs a persistant URL (no SIP, no user session), a regular link, pointing directly to a file, have to be used - the download functionality described here is not appropriate for such a scenario. If necessary, column-save-pdf can be used to generate such a file.

Parameter and (element) sources

  • download: d[:<exportFilename>]

    • exportFilename = <filename for save as> - Name, offered in the ‘File save as’ browser dialog. Default: ‘output.<ext>’.

      If there is no exportFilename defined, then the original filename is taken (if there is one, else: output…).

      The user typically expects meaningful and distinct file names for different download links.

  • popupMessage: a:<text> - will be displayed in the popup window during download. If the creating/download is fast, the window might disappear quickly.

  • mode: M:<mode>

    • mode = <file | pdf | zip | excel>

      • If M:file, the mime type is derived dynamically from the specified file. In this mode, only one element source is allowed per download link (no concatenation).
      • In case of multiple element sources, only pdf, zip and excel (template mode) is supported.
      • If M:zip is used together with p:…, U:… or u:.., those HTML pages will be converted to PDF. Those files get generic filenames inside the archive.
      • If not specified, the default ‘Mode’ depends on the number of specified element sources (=file or web page):
        • If only one file is specified, the default is file.
        • If there is a) a page defined or b) multiple elements, the default is pdf.
  • element sources - for M:pdf or M:zip, all of the following element sources may be specified multiple times. Any combination and order of these options are allowed.

    • file: F:<pathFileName> - relative or absolute pathFileName offered for a) download (single), or to be concatenated in a PDF or ZIP.

    • page: p:id=<t3 page>&<key 1>=<value 1>&<key 2>=<value 2>&…&<key n>=<value n>.

      • By default, the options given to wkhtml will not be encoded by a SIP!

      • To encode the parameter via SIP: Add ‘_sip=1’ to the URL GET parameter.

        E.g. p:id=form&_sip=1&form=Person&r=1.

        In that way, specific sources for the download might be SIP encrypted.

      • Any current HTML cookies will be forwarded to/via wkhtml. This includes the current FE Login as well as any QFQ session. Also the current User-Agent are faked via the wkhtml page request.

      • If there are trouble with accessing FE_GROUP protected content, please check wkhtmltopdf.

    • url: u:<url> - any URL, pointing to an internal or external destination.

    • uid: uid:<tt-content record id> - the tt_content.uid of a QFQ PageContent record (shown on hover in the backend). This will render only the specified QFQ content record, without any Typo3 layout elements (Menu, Body,…) QFQ will retrieve the tt-content’s bodytext from the Typo3 database, parse it, and render it as a PDF. Parameters can be passed: uid:<tt-content record id>[?arg1=value1][&arg2=value2][…] and will be available in the SIP store for the QFQ PageContent, or passed as wkhtmltopdf arguments, if applicable.

    • WKHTML Options for page, urlParam or url:

      • The ‘HTML to PDF’ will be done via wkhtmltopdf.
      • All possible options, suitable for wkhtmltopdf, can be submitted in the p:…, u:… or U:… element source. Check wkhtmltopdf.txt for possible options. Be aware that key/value tuple in the documentation is separated by a space, but to respect the QFQ key/value notation of URLs, the key/value tuple in p:…, u:… or U:… has to be separated by ‘=’. Please see last example below.
      • If an option contains an ‘&’ it must be escaped with double \ . See example.

    Most of the other Link-Class attributes can be used to customize the link as well.

Example _link:

# single `file`. Specifying a popup message window text is not necessary, cause a file directly accessed is fast.
SELECT "d:file.pdf|s|t:Download|F:fileadmin/pdf/test.pdf" AS _link

# single `file`, with mode
SELECT "d:file.pdf|M:pdf|s|t:Download|F:fileadmin/pdf/test.pdf" AS _link

# three sources: two pages and one file
SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1|F:fileadmin/pdf/test.pdf" AS _link

# three sources: two pages and one file
SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1|F:fileadmin/pdf/test.pdf" AS _link

# three sources: two pages and one file, parameter to wkhtml will be SIP encoded
SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1&_sip=1|p:id=detail2&r=1&_sip=1|F:fileadmin/pdf/test.pdf" AS _link

# three sources: two pages and one file, the second page will be in landscape and pagesize A3
SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail&r=1|p:id=detail2&r=1&--orientation=Landscape&--page-size=A3|F:fileadmin/pdf/test.pdf" AS _link

# One source and a header file. Note: the parameter to the header URL is escaped with double backslash.
SELECT "d:complete.pdf|s|t:Complete PDF|p:id=detail2&r=1&--orientation=Landscape&--header={{URL:R}}?indexp.php?id=head\\&L=1|F:fileadmin/pdf/test.pdf" AS _link

Example _pdf, _zip:

# File 1: p:id=1&--orientation=Landscape&--page-size=A3
# File 2: p:id=form
# File 3: F:fileadmin/file.pdf
SELECT 't:PDF|a:Creating a new PDF|p:id=1&--orientation=Landscape&--page-size=A3|p:id=form|F:fileadmin/file.pdf' AS _pdf

# File 1: p:id=1
# File 2: u:
# File 3: F:fileadmin/file.pdf
SELECT 't:PDF - 3 Files|a:Please be patient|p:id=1|u:|F:fileadmin/file.pdf' AS _pdf

# File 1: p:id=1
# File 2: p:id=form
# File 3: F:fileadmin/file.pdf
SELECT CONCAT('t:ZIP - 3 Pages|a:Please be patient|p:id=1|p:id=form|F:', p.pathFileName) AS _zip

Use the –print-media-type as wkhtml option to access the page with media type ‘printer’. Depending on the website configuration this switches off navigation and background images.

Rendering PDF letters

wkhtmltopdf, with the header and footer options, can be used to render multi page PDF letters (repeating header, pagination) in combination with dynamic content. Such PDFs might look-alike official letters, together with logo and signature.

Best practice:

  1. Create a clean (=no menu, no website layout) letter layout in a separated T3 branch:

    page = PAGE
    page.typeNum = 0
    page.includeCSS {
      10 = typo3conf/ext/qfq/Resources/Public/Css/qfq-letter.css
    // Grant access to any logged in user or specific development IPs
    [usergroup = *] || [IP =,192.168.1.* ]
      page.10 < styles.content.get
      page.10 = TEXT
      page.10.value = access forbidden
  2. Create a T3 body page (e.g. page alias: ‘letterbody’) with some content. Example static HTML content:

    <div class="letter-receiver">
    <div class="letter-sender">
     <p><b>firstName name</b><br>
      Phone +00 00 000 00 00<br>
      Fax +00 00 000 00 00<br>
    <div class="letter-date">
      Zurich, 01.12.2017
    <div class="letter-body">
     <p>Dear Mrs...</p>
     <p>Lucas ipsum dolor sit amet organa solo skywalker darth c-3p0 anakin jabba mara greedo skywalker.</p>
     <div class="letter-no-break">
     <img class="letter-signature" src="">
     <p>Firstname Name<br>Function</p>
  3. Create a T3 letter-header page (e.g. page alias: ‘letterheader’) , with only the header information:

    <img src="fileadmin/logo.png" class="letter-logo">
    <div class="letter-unit">
      <p class="letter-title">Department</p>
       Company name<br>
       Company department<br>
  4. Create a) a link (Report) to the PDF letter or b) attach the PDF (on the fly rendered) to a mail. Both will call the wkhtml via the download mode and forwards the necessary parameter.

Use in report:

sql = SELECT CONCAT('d:Letter.pdf|t:',p.firstName, ' ',
                     , '|p:id=letterbody&pId=',, '&_sip=1'
                     , '&--margin-top=50mm'
                     , '&--header-html={{BASE_URL_PRINT:Y}}?id=letterheader'

                     # IMPORTANT: set margin-bottom to make the footer visible!
                     , '&--margin-bottom=20mm'
                     , '&--footer-right="Seite: [page]/[toPage]"'
                     , '&--footer-font-size=8&--footer-spacing=10') AS _pdf

              FROM Person AS p ORDER BY

Sendmail. Parameter:

sendMailAttachment={{SELECT 'd:Letter.pdf|t:', p.firstName, ' ',, '|p:id=letterbody&pId=',, '&_sip=1&--margin-top=50mm&--margin-bottom=20mm&--header-html={{BASE_URL_PRINT:Y}}?id=letterheader&--footer-right="Seite: [page]/[toPage]"&--footer-font-size=8&--footer-spacing=10' FROM Person AS p WHERE{{id:S}} }}

Replace the static content elements from 2. and 3. by QFQ Content elements as needed:

10.sql = SELECT '<div class="letter-receiver"><p>', AS '_+br', p.street AS '_+br', AS '_+br', '</p>'
          FROM Person AS p WHERE{{pId:S}}

Export area

This description might be interesting if a page can’t be protected by SIP.

To offer protected pages, e.g. directly referenced files (remember: this is not recommended) in download links, the regular FE_GROUPs can’t be used, cause the download does not have the current user privileges (it’s a separate process, started as the webserver user).

Create a separated export tree in Typo3 Backend, which is IP access restricted. Only localhost or the FE_GROUP ‘admin’ is allowed to access:

tmp.restrictedIPRange =,::1
[IP = {$tmp.restrictedIPRange} ][usergroup = admin]
   page.10 < styles.content.get
   page.10 = TEXT
   page.10.value = Please access from localhost or log in as 'admin' user.

Excel export

This chapter explains how to create Excel files on the fly.

Hint: For up/downloading of excel files (without modification), check the generic Form input-upload element and the report ‘download’ (column_pdf) function.

The Excel file is build in the moment when the user request it by clicking on a download link.

Mode building:

  • New: The export file will be completely build from scratch.
  • Template: The export file is based on an earlier uploaded xlsx file (template). The template itself is unchanged.

Injecting data into the Excel file is done in the same way in both modes: a Typo3 page (rendered without any HTML header or tags) contains one or more Typo3 QFQ records. Those QFQ records will create plain ASCII output.

If the export file has to be customized (colors, pictures, headlines, …), the Template mode is the preferred option. It’s much easier to do all customizations via Excel and creating a template than by coding in QFQ / Excel export notation.


  • Create a special column name _excel (or _link) in QFQ/Report. As a source, define a T3 PageContent, which has to deliver the dynamic content (also excel-export-sample).

    SELECT CONCAT('d:final.xlsx|M:excel|s:1|t:Excel (new)|uid:<tt-content record id>') AS _link
  • Create a T3 PageContent which delivers the content.

    • It is recommended to use the uid:<tt-content record id> syntax for excel imports, because there should be no html code on the resulting content. QFQ will retrieve the PageContent’s bodytext from the Typo3 database, parse it, and pass the result as the instructions for filling the excel file.
    • Parameters can be passed: uid:<tt-content record id>?param=<value1>&param2=<value2> and will be accessible in the SIP Store (S) in the QFQ PageContent.
    • Use the regular QFQ Report syntax to create some output.
    • The newline at the end of every line needs to be CHAR(10). To make it simpler, the special column name … AS _XLS (see _XLS, _XLSs, _XLSb, _XLSn) can be used.
    • One option per line.
    • Empty lines will be skipped.
    • Lines starting with ‘#’ will be skipped (comments). Inline comment signs are NOT recognized as comment sign.
    • Separate <keyword> and <value> by ‘=’.
Keyword Example Description
‘worksheet’ worksheet=main Select a worksheet in case the excel file has multiple of them.
‘mode’ mode=insert Values: insert,overwrite.
‘position’ position=A1 Default is ‘A1’. Use the excel notation.
‘newline’ newline Start a new row. The column will be the one of the last ‘position’ statement.
‘str’, ‘s’ s=hello world Set the given string on the given position. The current position will be shift one to the right. If the string contains newlines, option’b’ (base64) should be used.
‘b’ b=aGVsbG8gd29ybGQK Same as ‘s’, but the given string has to Base64 encoded and will be decoded before export.
‘n’ n=123 Set number on the given position. The current position will be shift one to the right.
‘f’ f==SUM(A5:C6) Set a formula on the given position. The current position will be shift one to the right.

Create a output like this:

s=First Line
s=Second line

This fills D11, E11, F11, D12

In Report Syntax:

# With ... AS _XLS (token explicit given)
10.sql = SELECT 'position=D10' AS _XLS,
                's=Hello' AS _XLS,
                's=World' AS _XLS,
                's=First Line' AS _XLS,
                'newline' AS _XLS,
                's=Second line' AS _XLS,
                'n=123' AS _XLS,

# With ... AS _XLSs (token generated internally)
20.sql = SELECT 'position=D20' AS _XLS,
                'Hello' AS _XLSs,
                'World' AS _XLSs,
                'First Line' AS _XLSs,
                'newline' AS _XLS,
                'Second line' AS _XLSs,
                'n=123' AS _XLS,

# With ... AS _XLSb (token generated internally and content is base64 encoded)
30.sql = SELECT 'position=D30' AS _XLS,
                 '<some content with special characters like newline/carriage return>' AS _XLSb

Excel export samples (54 is a example <tt-content record id>):

# From scratch (both are the same, one with '_excel' the other with '_link')
SELECT CONCAT('d:new.xlsx|t:Excel (new)|uid:54') AS _excel
SELECT CONCAT('d:new.xlsx|t:Excel (new)|uid:54|M:excel|s:1') AS _link

# Template
SELECT CONCAT('d:final.xlsx|t:Excel (template)|F:fileadmin/template.xlsx|uid:54') AS _excel

# With parameter (via SIP) - get the Parameter on page 'exceldata' with '{{arg1:S}}' and '{{arg2:S}}'
SELECT CONCAT('d:final.xlsx|t:Excel (parameter)|uid:54&arg1=hello&arg2=world') AS _excel

Drag and drop

Order elements

Ordering of elements via HTML5 drag and drop is supported via QFQ. Any element to order should be represented by a database record with an order column. If the elements are unordered, they will be ordered after the first ‘drag and drop’ move of an element.

Functionality divides into:

  • Display: the records will be displayed via QFQ/report.
  • Order records: updates of the order column are managed by a specific definition form. The form is not a regular form (e.g. there are no FormElements), instead it’s only a container to held the SQL update query as well as providing access control via SIP. The form is automatically called via AJAX.

Part 1: Display list

Display the list of elements via a regular QFQ content record. All ‘drag and drop’ elements together have to be nested by a HTML element. Such HTML element:

  • With class=”qfq-dnd-sort”.
  • With a form name: {{‘form=<form name>’ AS _data-dnd-api}} (will be replaced by QFQ)
  • Only direct children of such element can be dragged.
  • Every children needs a unique identifier data-dnd-id=”<unique>”. Typically this is the corresponding record id.
  • The record needs a dedicated order column, which will be updated through API calls in time.

A <div> example HTML output (HTML send to the browser):

<div class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/Classes/Api/dragAndDrop.php?s=badcaffee1234">
    <div class="anyClass" id="<uniq1>" data-dnd-id="55">
        Numbero Uno
    <div class="anyClass" id="<uniq2>" data-dnd-id="18">
        Numbero Deux
    <div class="anyClass" id="<uniq3>" data-dnd-id="27">
        Numbero Tre

A typical QFQ report which generates those <div> HTML:

10 {
  sql = SELECT '<div id="anytag-',,'" data-dnd-id="',,'">' , n.note, '</div>'
               FROM Note AS n
               WHERE grId=28
               ORDER BY n.ord

  head = <div class="qfq-dnd-sort" {{'form=dndSortNote&grId=28' AS _data-dnd-api}}">
  tail = </div>

A <table> based setup is also possible. Note the attribute data-columns=”3” - this generates a dropzone which is the same column width as the outer table.

    <tbody class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/Classes/Api/dragAndDrop.php?s=badcaffee1234" data-columns="3">
        <tr> class="anyClass" id="<uniq1>" data-dnd-id="55">
            <td>Numbero Uno</td><td>Numbero Uno.2</td><td>Numbero Uno.3</td>
        <tr class="anyClass" id="<uniq2>" data-dnd-id="18">
            <td>Numbero Deux</td><td>Numbero Deux.2</td><td>Numbero Deux.3</td>
        <tr class="anyClass" id="<uniq3>" data-dnd-id="27">
            <td>Numbero Tre</td><td>Numbero Tre.2</td><td>Numbero Tre.3</td>

A typical QFQ report which generates this HTML:

10 {
  sql = SELECT '<tr id="anytag-',,'" data-dnd-id="',,'" data-columns="3">' , AS '_+td', n.note AS '_+td', n.ord AS '_+td', '</tr>'
               FROM Note AS n
               WHERE grId=28
               ORDER BY n.ord

  head = <table><tbody class="qfq-dnd-sort" {{'form=dndSortNote&grId=28' AS _data-dnd-api}} data-columns="3">
  tail = </tbody><table>
Show / update order value in the browser

The ‘drag and drop’ action does not trigger a reload of the page. In case the order number is shown and the user does a ‘drag and drop’, the order number shows the old. To update the dragable elements with the latest order number, a predefined html id has to be assigned them. After an update, all changed order number (referenced by the html id) will be updated via AJAX.

The html id per element is defined by qfq-dnd-ord-id-<id> where <id> is the record id. Same example as above, but with an updated n.ord column:

10 {
  sql = SELECT '<tr id="anytag-',,'" data-dnd-id="',,'" data-columns="3">' , AS '_+td', n.note AS '_+td',
               '<td id="qfq-dnd-ord-id-',, '">', n.ord, '</td></tr>'
               FROM Note AS n
               WHERE grId=28
               ORDER BY n.ord

  head = <table><tbody class="qfq-dnd-sort" {{'form=dndSortNote&grId=28' AS _data-dnd-api}} data-columns="3">
  tail = </tbody><table>

Part 2: Order records

A dedicated Form, without any FormElements, is used to define the reorder logic (database update definition).


  • Name: <custom form name> - used in Part 1 in the _data-dnd-api variable.
  • Table: <table with the element records> - used to the update the records specified by dragAndDropOrderSql.
  • Parameter:
Attribute Description
orderInterval = <number> Optional. By default ‘10’. Might be any number > 0.
orderColumn = <column name> Optional. By default ‘ord’.
dragAndDropOrderSql = {{!SELECT AS id, n.ord AS ord FROM Note AS n ORDER BY n.ord}} Query to selects the same records as the report in the same order! Inconsistencies results in order differences. The columns id and ord are mandatory.

The form related to the example of part 1 (‘div’ or ‘table’): dndSortNote
Form.table: Note
Form.parameter: orderInterval = 1
Form.parameter: orderColumn = ord
Form.parameter: dragAndDropOrderSql = {{!SELECT AS id, n.ord AS ord FROM Note AS n WHERE n.grId={{grId:S0}} ORDER BY n.ord}}


QFQ iterates over the result set of dragAndDropOrderSql. The value of column id have to correspond to the dragged HTML

element (given by data-dnd-id). Reordering always start with orderInterval and is incremented by orderInterval with each record of the result set. The client reports a) the id of the dragged HTML element, b) the id of the hovered element and c) the dropped position of above or below the hovered element. This information is compared to the result set and changes are applied where appropriate.

Take care that the query of part 1 (display list) does a) select the same records and b) in the same order as the query defined in part 2 (order records) via dragAndDropOrderSql.

If you find that the reorder does not work at expected, those two sql queries are not identical.

QFQ CSS Classes

  • qfq-table-50, qfq-table-80, qfq-table-100 - assigned to <table>, set min-width and column width to ‘auto’.
  • Background Color: qfq-color-grey-1, qfq-color-grey-2 - assigned to different tags (table, row, cell).
  • qfq-100 - assigned to different tags, makes an element ‘width: 100%’.
  • qfq-left- assigned to different tags, Text align left.
  • qfq-sticky - assigned to <thead>, makes the header sticky.


  • Table: table
  • Table > hover: table-hover
  • Table > condensed: table-condensed


10.sql = SELECT id, name, firstName, ...
10.head = <table class='table table-condensed qfq-table-50'>
  • qfq-100, qfq-left - makes e.g. a button full width and aligns the text left.


10.sql = SELECT "p:home&r=0|t:Home|c:qfq-100 qfq-left" AS _pagev


QFQ includes a third-party client-side table sorter:

To turn any table into a sortable table:

  • Ensure that your QFQ installation imports the appropriate js/css files, see setup-css-js.
  • Add the class=”tablesorter” to your <table> element.
  • Take care the <table> has a <thead> and <tbody> tag.
  • Evey table with active tablesorter should have a uniq HTML id.


Custom settings will be saved per table automatically in the browser local storage. To distinguish different table settings, define an uniq HTML id per table. Example: <table class=”tablesorter” id=”{{pageAlias:T}}-person”> - the {{pageAlias:T}} makes it easy to keep the overview over given name on the site.

The tablesorter options:

  • Class tablesorter-filter enables row filtering.

  • Class tablesorter-pager adds table paging functionality. A page navigation is shown.

  • Class tablesorter-column-selector adds a column selector widget.

  • Activate/Save/Delete views: Insert inside of a table html-tag the command:

    {{ '<uniqueName>' AS _tablesorter-view-saver }}

    This adds a menu to save the current view (column filters, selected columns, sort order).

    • <uniqueName> should be a name which is at least unique inside the typo3 content element. Example:

      <table {{ 'allperson' AS _tablesorter-view-saver }} class="tablesorter tablesorter-filter tablesorter-column-selector" id="{{pageAlias:T}}-demo"> ... </table>
    • ‘Views’ can be saved as:

      • public: every user will see the view and can modify it.
      • private: only the user who created the view will see/modify it.
      • readonly: manually mark a view as readonly (no FE User can change it) by setting column readonly=’true’ in table Setting of the corresponding view (identified by name).
    • Views will be saved in the table ‘Setting’.

    • Include ‘font-awesome’ CSS in your T3 page setup: typo3conf/ext/qfq/Resources/Public/Css/font-awesome.min.css to get the icons.

    • The view ‘Clear’ is always available and can’t be modified.

    • To preselect a view, append a HTML anker to the current URL. Get the anker by selecting the view and copy it from the browser address bar. Example:


      • ‘allperson’ is the ‘<uniqueName>’ of the tablesorter-view-saver command.
      • ‘public’ means the view is tagged as ‘public’ visible.
      • ‘email’ is the name of the view, as it is shown in the dropdown list.
    • If there is a public view with the name ‘Default’ and a user has no choosen a view earlier, that one will be selected.

Customization of tablesorter:

  • Add the desired classes or data attributes to your table html, e.g.:

    • Disable sorting class=”sorter-false” on a ‘<th>’ to disable sorting on that column (or: data-sorter=”false”).
    • Disable filter class=”filter-false” on a ‘<th>’ to hide the filter field for that column
    • see docs for more options:
  • You can pass in a default configuration object for the main tablesorter() function by using the attribute data-tablesorter-config on the table. Use JSON syntax when passing in your own configuration, such as:

    data-tablesorter-config='{"theme":"bootstrap","widthFixed":true,"headerTemplate":"{content} {icon}","dateFormat":"ddmmyyyy","widgets":["uitheme","filter","saveSort","columnSelector"],"widgetOptions":{"filter_columnFilters":true,"filter_reset":".reset","filter_cssFilter":"form-control","columnSelector_mediaquery":false} }'
  • If the above customization options are not enough, you can output your own HTML for the pager and/or column selector, as well as your own $(document).ready() function with the desired config. In this case, it is recommended not to use the above tablesorter classes since the QFQ javascript code could interfere with your javascript code.


10 {
  sql = SELECT id, CONCAT('form&form=person&r=', id) AS _Pagee, lastName, title FROM person
  head = <table class="table tablesorter tablesorter-filter tablesorter-pager tablesorter-column-selector" id="{{pageAlias:T}}-ts1">
      <thead><tr><th>Id</th><th class="filter-false sorter-false">Edit</th>
      <th>Name</th><th class="filter-select" data-placeholder="Select a title">Title</th>
  tail = </tbody></table>
  rbeg = <tr>
  rend = </tr>
  fbeg = <td>
  fend = </td>


Display a (log)file from the server, inside the browser, which updates automatically by a user defined interval. Access to the file is SIP protected. Any file on the server is possible.

  • On a Typo3 page, define a HTML element with a unique html-id. E.g.:

    10.head = <pre id="monitor-1">Please wait</pre>
  • On the same Typo3 page, define an SQL column ‘_monitor’ with the necessary parameter:

    10.sql = SELECT 'file:fileadmin/protected/log/sql.log|tail:50|append:1|refresh:1000|htmlId:monitor-1' AS _monitor
  • Short version with all defaults used to display system configured sql.log:

    10.sql = SELECT 'file:{{sqlLog:Y}}' AS _monitor, '<pre id="monitor-1" style="white-space: pre-wrap;">Please wait</pre>'

Report Examples

The following section gives some examples of typical reports.

Basic Queries

One simple query:

10.sql = SELECT "Hello World"


Hello World

Two simple queries:

10.sql = SELECT "Hello World"
20.sql = SELECT "Say hello"


Hello WorldSay hello

Two simple queries, with break:

10.sql = SELECT "Hello World<br>"
20.sql = SELECT "Say hello"


Hello World
Say hello

Accessing the database

Real data, one single column:

10.sql = SELECT p.firstName FROM exp_person AS p



Real data, two columns:

10.sql = SELECT p.firstName, p.lastName FROM exp_person AS p



The result of the SQL query is an output, row by row and column by column, without adding any formatting information. See Formatting Examples for examples of how the output can be formatted.

Formatting Examples

Formatting (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 use ‘level’ keys to define formatting information that will be put before/after/between all rows/columns of the actual levels result.

Two columns:

# Add the formatting information as a column
10.sql = SELECT p.firstName, " " , p.lastName, "<br>" FROM exp_person AS p


Billie Holiday
Elvis Presley
Louis Armstrong
Diana Ross

One column ‘rend’ as linebreak - no extra column ‘<br>’ needed:

10.sql = SELECT p.firstName, " " , p.lastName, " ", FROM exp_person AS p
10.rend = <br>


Billie Holiday USA
Elvis Presley USA
Louis Armstrong USA
Diana Ross USA

Same with ‘fsep’ (column ” ” removed):

10.sql = SELECT p.firstName, p.lastName, FROM exp_person AS p 10.rend = <br> 10.fsep = ” “


Billie Holiday USA
Elvis Presley USA
Louis Armstrong USA
Diana Ross USA

More HTML:

10.sql = SELECT FROM exp_person AS p
10.head = <ul>
10.tail = </ul>
10.rbeg = <li>
10.rend = </li>


o Billie Holiday
o Elvis Presley
o Louis Armstrong
o Diana Ross

The same as above, but with braces:

10 {
  sql = SELECT FROM exp_person AS p
  head = <ul>
  tail = </ul>
  rbeg = <li>
  rend = </li>

Two queries:

10.sql = SELECT FROM exp_person AS p
10.rend = <br>
20.sql = SELECT a.street FROM exp_address AS a
20.rend = <br>

Two queries: nested:

# outer query
10.sql = SELECT FROM exp_person AS p
10.rend = <br>

# inner query
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:

# outer query
10.sql = SELECT, FROM exp_person AS p
10.rend = <br>

# inner query
10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{}}'
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:

10.sql = SELECT AS _pId, FROM exp_person AS p
10.rend = <br>

# inner query
10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}'
10.10.rend = <br>

Same as above, but written in the nested notation:

10 {
  sql = SELECT AS _pId, FROM exp_person AS p
  rend = <br>

  10 {
  # inner query
    sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}'
    rend = <br>

Best practice recommendation for using parameter - see access-column-values:

10 {
  sql = SELECT AS _pId, FROM exp_person AS p
  rend = <br>

  10 {
  # inner query
    sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{pId:R}}'
    rend = <br>

Create HTML tables:

10 {
  sql = SELECT p.firstName, p.lastName, FROM Person AS p
  head = <table class="table">
  tail = </table>
  rbeg = <tr>
  rend = </tr>
  fbeg = <td>
  fend = </td>

Maybe a few columns belongs together and should be in one column.

Joining columns, variant A: firstName and lastName in one column:

10 {
  sql = SELECT CONCAT(p.firstName, ' ', p.lastName), FROM Person AS p
  head = <table class="table">
  tail = </table>
  rbeg = <tr>
  rend = </tr>
  fbeg = <td>
  fend = </td>

Joining columns, variant B: firstName and lastName in one column:

10 {
  sql = SELECT '<td>', p.firstName, ' ', p.lastName, '</td><td>',, '</td>' FROM Person AS p
  head = <table class="table">
  tail = </table>
  rbeg = <tr>
  rend = </tr>

Joining columns, variant C: firstName and lastName in one column:

10 {
  sql = SELECT '<td>', p.firstName, ' ', p.lastName, '</td>', FROM Person AS p
  head = <table class="table">
  tail = </table>
  rbeg = <tr>
  rend = </tr>
  fbeg = <td>
  fend = </td>
  fskipwrap = 1,2,3,4,5

Recent List

A nice feature is to show a list with last changed records. The following will show the 10 last modified (Form or FormElement) forms:

10 {
  sql = SELECT CONCAT('p:{{pageAlias:T}}&form=form&r=',, '|t:',,'|o:', GREATEST(MAX(fe.modified), f.modified)) AS _page
        FROM Form AS f
        LEFT JOIN FormElement AS fe ON fe.formId =
        GROUP BY
        ORDER BY GREATEST(MAX(fe.modified), f.modified) DESC
        LIMIT 10
  head = <h3>Recent Forms</h3>
  rsep = ,&ensp;

STORE_USER examples

Keep variables per user session.

Two pages (pass variable)

Sometimes it’s useful to have variables per user (=browser session). Set a variable on page ‘A’ and retrieve the value on page ‘B’.

Page ‘A’ - set the variable:

10.sql = SELECT 'hello' AS '_=greeting'

Page ‘B’ - get the value:

10.sql = SELECT '{{greeting:UE}}'

If page ‘A’ has never been opened with the current browser session, nothing is printed (STORE_EMPTY gives an empty string). If page ‘A’ is called, page ‘B’ will print ‘hello’.

One page (collect variables)

A page will be called with several SIP variables, but not at all at the same time. To still get all variables at any time:

# Normalize
10.sql = SELECT '{{order:USE:::sum}}' AS '_=order', '{{step:USE:::5}}' AS _step, '{{direction:USE:::ASC}}' AS _direction

# Different links
20.sql = SELECT 'p:{{pageAlias:T}}&order=count|t:Order by count|b|s' AS _link,
                'p:{{pageAlias:T}}&order=sum|t:Order by sum|b|s' AS _link,
                'p:{{pageAlias:T}}&step=10|t:Step=10|b|s' AS _link,
                'p:{{pageAlias:T}}&step=50|t:Step=50|b|s' AS _link,
                'p:{{pageAlias:T}}&direction=ASC|t:Order by up|b|s' AS _link,
                'p:{{pageAlias:T}}&direction=DESC|t:Order by down|b|s' AS _link

30.sql = SELECT * FROM items ORDER BY {{order:U}} {{direction:U}} LIMIT {{step:U}}

Simulate/switch user: feUser

Just set the STORE_USER variable ‘feUser’.

All places with {{feUser:T}} has to be replaced by {{feUser:UT}}:

# Normalize
10.sql = SELECT '{{feUser:UT}}' AS '_=feUser'

# Offer switching feUser
20.sql = SELECT 'p:{{pageAlias:T}}&feUser=account1|t:Become "account1"|b|s' AS _link,
                'p:{{pageAlias:T}}&feUser={{feUser:T}}|t:Back to own identity|b|s' AS _link,

Semester switch (remember last choice)

A current semester is defined via configuration in STORE_SYSTEM ‘{{semId:Y}}’. The first column in 10.sql ‘{{semId:SUY}}’ AS ‘_=semId’ saves the semester to STORE_USER via ‘_=semId’. The priority ‘SUY’ takes either the latest choose (STORE_SIP) or reuse the last used (STORE_USER) or (first time call during browser session) takes the default from config (STORE_SYSTEM):

# Semester switch
10 {
  sql = SELECT '{{semId:SUY}}' AS '_=semId',
               CONCAT('p:{{pageAlias:T}}&semId=',, '|t:', QBAR(, '|s|b|G:glyphicon-chevron-left') AS _link,
                       ' <button class="btn disabled ',   IF({{semId:Y0}}, 'btn-success', 'btn-default'), '">',, '</button> ',
                       CONCAT('p:{{pageAlias:T}}&semId=',, '|t:', QBAR(, '|s|b|G:glyphicon-chevron-right|R') AS _link
                   FROM semester AS sc
                   LEFT JOIN semester AS sp ON
                   LEFT JOIN semester AS sn ON AND sn.show_semester_from<=CURDATE()
                   WHERE{{semId:SUY}} AND '{{form:SE}}'=''
                   ORDER BY sc.semester_von
  head = <div class="btn-group" style="position: absolute; top: 15px; right: 25px;">
  tail = </div><p></p>


Via REST it’s possible to access the QFQ based application. Each REST API endpoint has to be defined as a QFQ Form. The QFQ REST api implements the four most used REST HTTP methods:

GET - Read

Shows a list of database records or a single record. The QFQ form holds the definition which and what to show.

List: curl -X GET "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/

Data (id=123): curl -X GET "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/123

POST - Create new record

The QFQ form defines wich columns will be written in which table. Most of QFQ Form functionality can be used. Example:

curl -X POST "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/" -d '{"name":"Miller","firstname":"Joe"}'

PUT - Update a record

Similar to POST, but a given record will be updated.

curl -X PUT "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/123" -d '{"name":"Miller","firstname":"Joe"}'

DELETE - Delete a record

Similar to a QFQ Delete form.

curl -X DELETE "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/123"

All data will imported / exported in JSON notation.

Any QFQ form becomes a REST form via: Form > Access > Permit REST: get / insert / update / delete



The basic REST API endpoint: <domain>/typo3conf/ext/qfq/Classes/Api/rest.php


Append level names and ids after …/rest.php/, each separated by ‘/’ .


  1. List of all persons: <domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person
  2. Data of person 123: <domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123
  3. Adresses of person 123: <domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address
  4. Adress details of address 45 from person 123: <domain>/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address/45

QFQ ‘Forms’ are used as a ‘container’ (to define all details).


The QFQ form name represents the level name.

Only the last <level> of an URI will be processed. The former ones are just to fulfil a good looking REST API.


Each level name (=form name) is available via STORE_CLIENT and name _formX. E.g. in example (1) {{_form1:C:alnumx}}=person and {{_form2:C:alnumx}}=address.

Each level id is available via STORE_CLIENT and name _idX. E.g. in example (2) {{_id1:C}}=123 and {{_id2:C}}=45.

Also the id after the last level in the URI path, 123 in example (2) and 45 in example (4), is copied to variable r in STORE_TYPO3, access it via {{r:T}}.

GET - Read

A REST (GET) form has two modes:

Specific content to a given id. Defined via ‘form.parameter.restSqlData’. This mode is selected if there is an id>0 given.
A list of records will be exported. Defined via ‘form.parameter.restSqlList’. This mode is selected if there is no id or id=0.


There are no native-FormElements necessary or loaded. Action FormElements will be processed.

To simplify access to id parameter of the URI, a mapping is possible via ‘form.parameter.restParam’. E.g. restParam=pId,adrId with example d) makes {{pId:C}}=123 and {{adrId:C}}=45. The order of variable names corresponds to the position in the URI. _id1 is always mapped to the first parameter name, _id2 to the second one and so on.

GET Variables provided via URL are available via STORE_CLIENT as usual.


Attribute Description
name <level> Mandatory. Level name (Endpoint) in URI.
table Mandatory. Name of the primary table
Permit REST get Mandatory. The form can be loaded in REST mode.


Attribute Description
restSqlData Mandatory. SQL query selects content shown in data mode. | restSqlData={{!SELECT id, name, gender FROM Person WHERE id='{{r:T0}}'' }}
restSqlList Mandatory. SQL query selects content shown in data mode. | restSqlData={{!SELECT id, name FROM Person }}
restParam Optional. CSV list of variable names. E.g.: restParam=pId,adrId
restToken Optional. User defined string or dynamic token (see restAuthorization).


There are no special-column-names available in restSqlData or restSqlList. Also there are no SIPs possible, cause REST typically does not offer sessions/cookies (which are necessary for SIPs).


If there is an ìd given, a record in the named primary with the specified table has to exist. If not, an error is thrown.

POST - Insert


Attribute Description
name <level> Mandatory. Level name (Endpoint) in URI.
table Mandatory. Name of the primary table
Permit REST insert Mandatory. The form can be loaded in REST mode.
id Missing or ‘0’.


Attribute Description
restParam Optional. CSV list of variable names. E.g.: restParam=pId,adrId
restToken Optional. User defined string or dynamic token (see restAuthorization).


  • For each column to save one FormElement with<column> is necessary.
  • A regular QFQ form can be used as REST Post endpoint

PUT - Update


Attribute Description
name <level> Mandatory. Level name (Endpoint) in URI.
table Mandatory. Name of the primary table
Permit REST update Mandatory. The form can be loaded in REST mode.
id >0


Attribute Description
restParam Optional. CSV list of variable names. E.g.: restParam=pId,adrId
restToken Optional. User defined string or dynamic token (see restAuthorization).


  • For each column to save one FormElement with<column> is necessary.
  • A regular QFQ form can be used as REST Post endpoint

DELETE - Delete


Attribute Description
name <level> Mandatory. Level name (Endpoint) in URI.
table Mandatory. Name of the primary table
Permit REST delete Mandatory. The form can be loaded in REST mode.
id >0


Attribute Description
restParam Optional. CSV list of variable names. E.g.: restParam=pId,adrId
restToken Optional. User defined string or dynamic token (see restAuthorization).


There are no native-FormElements necessary - but might exist for dependent records to delete. Action FormElements will be processed.


A QFQ form is only acessible via REST API, if Form.permitRest enables one of the HTTP Methods: get, post, put, delete

Permit New or Permit Edit don’t apply to QFQ forms called via REST.


By default, the REST API is public accessible.

If this is not wished:

  • HTTP AUTH might be used (configured via webserver)
  • Any other webserver based access restriction method
  • or the QFQ internal ‘HTTP header token based authorization’.

Token based authorization

A form will require a ‘token based authorization’, as soon as there is a form.parameter.restToken defined. Therefore the HTTP Header ‘Authorization’ has to be set with token=<secret token>. The ‘secret token’ will be checked against the server.



Test via commandline: curl -X GET -H 'Authorization: Token token=myCrypticString0123456789' "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/person/123/address/"

The static setup with form.parameter.restToken=myCrypticString0123456789 is fine, as long as only one token exist. In case of multiple tokens, replace the static string against a SQL query.


The HTML Header Authorization token is available in STORE_CLIENT via ‘{{Authorization:C:alnumx}}.

Best Practice: For example all created tokens are saved in a table ‘Auth’ with a column ‘token’. Define:

form.parameter.restToken={{SELECT a.token FROM Auth AS a WHERE a.token='{{Authorization:C:alnumx}}' }}

To restrict access to a subset of data, just save the limitations inside the Auth record and update the query to check it:

form.parameter.restToken={{SELECT a.token FROM Auth AS a WHERE a.token='{{Authorization:C:alnumx}}'}}
form.parameter.restSqlList={{!SELECT,, FROM Person AS p, Auth AS a WHERE a.token='{{Authorization:C:alnumx}}' AND a.attribute=p.attribute}}
form.parameter.restSqlData={{!SELECT p.* FROM Person AS p, Auth AS a WHERE a.token='{{Authorization:C:alnumx}}' AND a.attribute=p.attribute AND'{{r:T0}}' }}

If authorization is denied, the request will be answered with a delay of 3 seconds (configured via securityFailedAuthDelay).


FormEditor with usage

With a large number of forms, it’s important to know how often a form has been used, on which pages it’s used and when has the form be used. The following report includes the regular form-editor as well:

# Form
# a) List of forms: {{form:S}}='', {{formIdHistory:S}}=''
# b) Edit Form: {{form:S}} - Open form {{form:S}} with record {{r:S}} - typically the FormEditor
# c) Use history of a given form: {{formIdHistory:S}}
# {{form:S}}
# {{formIdHistory:S0}} - usage history of form '{{formIdHistory:S}}'


10 {
  # List of Forms: Do not show this list of forms if there is a form given by SIP.
  # Table header.
  sql = SELECT '<th data-sorter="false" class="filter-false">'
               , CONCAT('p:{{pageAlias:T}}&form=Form&') as _pagen
               , '</th><th>Name'
               , '</th><th>Title'
               , '</th><th>Table'
               , '</th><th>#'
               , '</th><th><em>First</em>'
               , '</th><th><em>Last</em>'
               , '</th><th><em>PageId</em></th>'
           FROM (SELECT '') AS fake
           WHERE  '{{form:SE}}'=''
             AND {{formIdHistory:S0}}=0
  head = <table class="table table-hover qfq-table-50 tablesorter tablesorter-filter" id="{{pageAlias:T}}-form">
  rbeg = <thead class="qfq-sticky"><tr>
  rend = </tr></thead><tbody>
  tail = </tbody></table>

  20 {
    # All forms
    sql = SELECT CONCAT('p:{{pageAlias:T}}&form=Form&r=', as _pagee
                 , CONCAT(, ' <span class="text-muted">(',, ')</span>')
                 , QMORE(strip_tags(f.title),50)
                 , f.tableName
                 , CONCAT('p:{{pageAlias:T}}&formIdHistory=',, '|s|b|t:<span class="badge">', COUNT(, '</span>'
                          , IF(COUNT(, '|r:3','')  ) as _link
                 , CONCAT( '<em><span title="',MIN(fsl.created), '">', DATE_FORMAT( MIN( fsl.created), '%d.%m.%Y'), '</span></em>')
                 , CONCAT( '<em><span title="',MAX(fsl.created), '">', DATE_FORMAT( MAX( fsl.created), '%d.%m.%Y'), '</span></em>')
                 , CONCAT('<em>', GROUP_CONCAT(DISTINCT fsl.pageId ORDER BY fsl.pageId), '<em>')
              FROM Form AS f
              LEFT JOIN FormSubmitLog AS fsl
              WHERE {{formIdHistory:S0}}=0
              GROUP BY
              ORDER BY
    rbeg = <tr>
    rend = </tr>
    fbeg = <td>
    fend = </td>

30 {
  # History of a Form {{formIdHistory:S0}}
  sql = SELECT
               , fsl.feUser
               , fsl.recordId
               , fsl.pageId
               , fsl.created

           FROM FormSubmitLog AS fsl, Form AS f
           WHERE fsl.formId={{formIdHistory:S0}}
           ORDER BY fsl.created DESC

  head = <table class="table table-hover qfq-table-50 tablesorter tablesorter-filter" id="{{pageAlias:T}}-formHistory">
         <thead class="qfq-sticky"><tr><th>Form</th><th>feUser</th><th>recordId</th><th>pageId</th><th>Submit</th></tr></thead><tbody>
  tail = </tbody></table>
  rbeg = <tr>
  rend = </tr>
  fbeg = <td>
  fend = </td>
  altsql = SELECT IF('{{formIdHistory:S0}}'='0', '', '<div class="alert alert-warning">History is empty</div>')


The AutoCron service fires periodically jobs like open a webpage (typically a QFQ page which does some database actions) or send mail.

  • AutoCron will be triggered via system cron. Minimal time distance therefore is 1 minute. If this is not sufficient, any process who starts …/typo3conf/ext/qfq/Classes/External/autocron.php via /usr/bin/php frequently might be used.
  • Custom start time and frequency.
  • Per job:
    • If a job still runs and receives the next trigger, the running job will be completed first.
    • If more than one trigger arrives during a run, only one trigger will be processed.
    • If the system misses a run, it will be played as soon as the system is online again.
    • If multiple runs are missed, only one run is fired as soon as the system is online again.
  • Running and processed jobs can easily be monitored via lastRun, lastStatus, nextRun, inProgress.


  • Setup a system cron entry, typically as the webserver user (‘www-data’ on debian).
  • Necessary privileges:
    • Read for …/typo3conf/ext/qfq/*
    • Write, if a logfile should be written (specified per cron job) in the custom specified directory.

Cron task:

* * * * * /usr/bin/php /var/www/html/typo3conf/ext/qfq/Classes/External/autocron.php

AutoCron Jobs of type ‘website’ needs the php.ini setting:

allow_url_fopen = On

Remember: if a cron job fails for whatever reason, the cron daemon will send a mail to the userid who started the cron job. E.g. www-data. Setup a email forward of such account to a frequently read email account.

Create / edit AutoCron jobs

Create a T3 page with a QFQ record (similar to the formeditor). Such page should be access restricted and is only needed to edit AutoCron jobs:


10 {
    # Table header.
    sql = SELECT CONCAT('p:{{pageAlias:T}}&form=cron') AS _pagen, 'id', 'Next run','Frequency','Comment',
                 'Last run','In progress', 'Status', 'Auto generated' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'=''
    head = <table class='table table-hover qfq-table-50'>
    tail = </table>
    rbeg = <thead><tr>
    rend = </tr></thead>
    fbeg = <th>
    fend = </th>

    10 {
    # All Cron Jobs
    sql = SELECT CONCAT('<tr class="',
                        IF(c.lastStatus LIKE 'Error%','danger',''),
                        IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),' warning',''),
                        IF(c.status='enable','',' text-muted'),'" ',

                        IF(c.inProgress!=0 AND DATE_ADD(c.inProgress, INTERVAL 10 MINUTE)<NOW(),'title="inProgress > 10mins"',
                        IF(c.lastStatus LIKE 'Error%','title="Status: Error"','')),
                    '<td>', CONCAT('p:{{pageAlias:T}}&form=cron&r=', AS _pagee, '</td><td>',
          , '</td><td>',
                    IF(c.nextrun=0,"", DATE_FORMAT(c.nextrun, "%d.%m.%y %H:%i:%s")), '</td><td>',
                    c.frequency, '</td><td>',
                    c.comment, '</td><td>',
                    IF(c.lastrun=0,"", DATE_FORMAT(c.lastrun,"%d.%m.%y %H:%i:%s")), '</td><td>',
                    IF(c.inProgress=0,"", DATE_FORMAT(c.inProgress,"%d.%m.%y %H:%i:%s")), '</td><td>',
                    LEFT(c.laststatus,40) AS '_+pre', '</td><td>',
                    CONCAT('U:form=cron&r=', AS _paged, '</td></tr>'
            FROM Cron AS c


The system cron service will call the QFQ AutoCron every minute. QFQ AutoCron checks if there is a pending job, by looking for jobs with nextRun <= NOW(). All found jobs will be fired - depending on their type, such jobs will send mail(s) or open a webpage. A webpage will mostly be a local T3 page with at least one QFQ record on it. Such a QFQ record might do some manipulation on the database or any other task.

A job with nextRun`=0 or `inProgress!=0 won’t never be started.

Due to checking inProgress, jobs will never run in parallel, even if a job needs more than 1 minute (interval system cron).

Job: repeating

  • frequency: ‘1 MINUTE’, ‘2 DAY’, ‘3 MONTH’, ….

After finishing a job, nextRun will be increased by frequency. If nextRun still points in the past, it will be increased by frequency again, until it points to the future.

Job: asynchronous

  • frequency: <empty>

An ‘AutoCron’ job becomes ‘asynchronous’ if frequency is empty. Then, auto repeating is switched off.

If nextRun is > 0 and in the past, the job will be fired. After the job has been done, nextRun will be set to 0.

This is useful for jobs which have to be fired from time to time.

To fire such an asynchronous job, just set nextRun=NOW() and wait for the next system cron run.

If such a job is running and a new nextRun=NOW() is applied, the ‘AutoCron’ job will be fired again during the next system cron run.

Type: Mail

At the moment there is a special sendmail notation - this will change in the future.

  • Mail:

    {{!SELECT '' AS sendMailTo, 'Custom subject' AS sendMailSubject, '' AS sendMailFrom, 123 AS sendMailGrId, 456 AS sendMailXId}}

AutoCron will send as many mails as records are selected by the SQL query in field Mail. Field Mail body provides the mail text.

Type: Website

The page specified in URL will be opened.

Optional the output of that page can be logged to a file (take care to have write permissions on that file).

  • Log output to file`=`output.log - creates a file in the Typo3 host directory.
  • Log output to file`=/var/log/output.log` - creates a file in /var/log/ directory.
Also overwrite or append can be selected for the output file. In case of append a file rotation should be setup on
OS level.

To check for a successful DB connection, it’s a good practice to report a custom token on the T3 page / QFQ record like ‘DB Connect: ok’. Such a string can be checked via Pattern to look for on output=/DB Connect: ok/. The pattern needs to be written in PHP PCRE syntax. For a simple search string, just surround them with ‘/’. If the pattern is found on the page, the job get’s ‘Ok’ - else ‘Error - …’.

Access restriction

To protect AutoCron pages not to be triggered accidental or by unprivileged access, access to those page tree might be limited to localhost. Some example Typoscript:

# Access allowed for any logged in user or via 'localhost'
[usergroup = *] || [IP =]
  page.10 < styles.content.get
  # Error Message
  page.10 = TEXT
  page.10.value = <h2>Access denied</h2>Please log in or access this page from an authorized host. Your current IP address:&nbsp;
  page.20 = TEXT = getenv : REMOTE_ADDR
AutoCron / website: HTTPS protocol
  • For https the PHP extension php_openssl has to be installed.
  • All certificates are accepted, even self signed without a correct chain or hostnames, not listed in the certificate. This is useful if there is a general ‘HTTP >> HTTPS’ redirection configured and the website is accessed via https://localhost/…

Application Test

With a framework like it’s possible to play and verify unattended test cases.

To assist such frameworks and to make the tests reliable, an individual tag might be assigned to HTML elements which have to interact with the test framework.


By default every FormElement contains an attribute ‘data-reference=<value>’, whereas the ‘<value>’ is either the name of the FormElement or a custom value, defined via ‘FormElement.parameter.dataReference=<value>’.


Any HTML output can be extended by a tag - that’s done by the webmaster. For QFQ generated links, an attribute like ‘data-reference’ might be injected via token ‘A’ (attribute).

SELECT 'p:personedit&form=person&r=1|b|s|A:data-reference="person-edit"|t:Edit person' AS _link

General Tips


  • Does the error happens on every page or only on a specific one?
  • Does the error happens on every form or only on a specific one?


  • On general errors:
    • Always check the Javascript console of your browser, see javascriptProblem.
    • Always check the Webserver log files.


Content, generated by QFQ, is generally not cached. But the QFQ content records are cached by Typo3. This means if there is a content element ‘Insert Records’ on a page ‘A’ which includes a QFQ record from page ‘B’ and such QFQ record is modified (the SQL definition, not the delivered output), the new definition becomes by default only visible if the cached is cleared.

To simplify the situation, set on the page of the QFQ record (B) in Page TS Config:

TCEMAIN.clearCacheCmd = pages

QFQ specific

A variable {{<var>}} is empty

The sanitize rule is violated and therefore the value has been removed. Set {{<var>:<store>:all}} as a test.

Page is white: no HTML code at all

This should not happen.

The PHP process stopped at all. Check the Apache error logfile, look for a stacktrace to find the latest function. Send a bug report.

Problem with query or variables

Specify the required sanitize class. Remember: for STORE_FORM and STORE_CLIENT the default is sanitize class is digit. This means if the variable content is a string, this violates the sanitize class and the variable will not be replaced.

Tip on Form: put the problematic variable or SQL statement in the ‘title’ or note ‘field’ of a FormElement. This should show the content. For SQL statements, remove the outer token (e.g. only one curly brace) to avoid triggering SQL:

FE.title: Person { SELECT ... WHERE id={{buggyVar:alnumx}} }

Tip on Report: In case the query did not contain any double ticks, just wrap all but ‘SELECT’ in double ticks:

Buggy query:  10.sql = SELECT id, ... FROM myTable WHERE status={{myVar}} ORDER BY status
Debug query:  10.sql = SELECT "id, ... FROM myTable WHERE status={{myVar}} ORDER BY status"

Error read file config.qfq.php: syntax error on line xx

Check the given line number. If it’s an SQL statement, enclose it in single or double ticks.

Output a text, substitute embedded QFQ variables

The content will be copied to ‘_text’. In 10.tail than the ‘{{text:R}}’ will be substituted with all known variables. Note the ‘-‘ in ‘{{text:RE::-}}’, this will prevent that QFQ escapes any character from the content.

10 {
  sql = SELECT no.text AS _text
           FROM Note AS no
           WHERE id=...
  tail = {{text:RE::-}}

TypeAhead list with T3 page alias names - use of the T3 DB

To define a typeahead list of T3 page alias names:

FE.type = text
FE.parameter.typeAheadSql = SELECT p.alias FROM {{dbNameT3:Y}}.pages AS p WHERE p.deleted=0 AND p.alias!='' AND p.alias LIKE ? ORDER BY p.alias LIMIT 20
FE.parameter.typeAheadMinLength = 1

Set FE-User password

To offer an FE User the possibility to change the own T3 FE password, create a form: = changeFePassword
f.title = Change Password
f.table = Person   (QFQ Table, not T3)
f.permitNew = never
f.permitEdit = sip

fe[1].name = myPassword
fe[1].title = Password
fe[1].class = native
fe[1].type = password
fe[1].mode = required
fe[1].parameter = retype

fe[2].class = action
fe[2].type = afterSave
fe[2].parameter = sqlAfter={{UPDATE {{dbNameT3:Y}}.fe_users SET password='{{myPassword:FE:all:p}}' WHERE username='{{feUser:T}}' AND deleted=0

Call the form via SIP on an existing record. Often QFQ has an own table for persons and also the current user exist in T3 fe_users table.


General webserver error log

For apache: /var/log/apache2/error_log

Especially if you got a blank page (no rendering at all), this is typically an uncaught PHP error. Check the error message and report the bug ( > Contact).

Call to undefined function qfq\mb_internal_encoding()

Check that all required php modules are installed. See preparation.

Error Messages

Internal Server Error

The browser shows a red popup with ‘Internal Server Error’. The message is generated in the browser. Happens e.g. an AJAX request response of QFQ (=Server) is broken. This might happen e.g. if PHP can’t start successfully or PHP fails to run due to a missing php module or broken configuration.

Oops, an error occurred! Code: 20180612205917761fc593

You see this message on all places where a QFQ content record should produce some output. Typically the extension fails to load. If the error message disappears when the QFQ extension is disabled (instead a message qfq_qfq can’t be rendered is shown), than QFQ is the problem.

Search the given code in typo3temp/logs/*, in this example 20180612205917761fc593. You’ll should find a stacktrace with a more detailed message.

The error might occur if there are problematic characters in config.qfq.php, like single or double ticks inside strings, wich are not enclosed (correctly).

sendEmail: Error => TLS setup failed

Switch off the TLS encryption. In configuration specify for config.sendEMailOptions:

-o tls=no

Javascript problem

Open the ‘Webdeveloper Tools’ (FF: F12, Chrome/Opera: Right mouse click > Inspect Element) in your browser, switch to ‘console’ and reload the page. Inspect the messages.


Glyph Icons in ‘<span>’

TinyMCE forbids by default HTML tag ‘span’ with ‘class’ attribute. E.g.:

<span class="glyphicon glyphicon-user"></span>

To allow it, add ‘span’ to the valid elements in the FormElement.parameter field:

editor-extended_valid_elements = span[class|style]

The HTML span tag has to be added via ‘source’ view. At least in TinyMCE 4.7.13, the glyph is still not shown in the editor but saved.