Newer
Older
10.sql = SELECT 'hello' AS '_=greeting'

Carsten Rose
committed
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)

Carsten Rose
committed
""""""""""""""""""""""""""""

Carsten Rose
committed
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::
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

Carsten Rose
committed
""""""""""""""""""""""""""""
Just set the STORE_USER variable 'feUser'.
All places with `{{feUser:T}}` has to be replaced by `{{feUser:UT}}`::
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)

Carsten Rose
committed
""""""""""""""""""""""""""""""""""""""
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

Carsten Rose
committed
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=', sp.id, '|t:', QBAR(sp.name), '|s|b|G:glyphicon-chevron-left') AS _link,
' <button class="btn disabled ', IF({{semId:Y0}}=sc.id, 'btn-success', 'btn-default'), '">',sc.name, '</button> ',
CONCAT('p:{{pageAlias:T}}&semId=', sn.id, '|t:', QBAR(sn.name), '|s|b|G:glyphicon-chevron-right|R') AS _link
FROM semester AS sc
LEFT JOIN semester AS sp ON sp.id=sc.id-1
LEFT JOIN semester AS sn ON sc.id+1=sn.id AND sn.show_semester_from<=CURDATE()
WHERE sc.id={{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>
}
REST
====
Via `REST <https://en.wikipedia.org/wiki/Representational_state_transfer>`_ 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``
Endpoint
.. tip::
The basic REST API endpoint: ``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php``
``<domain>/typo3conf/ext/qfq/Classes/Api/rest.php/<level1>/<id1>/<level2>/<id2>/.../?<var1>=<value1>&...``
Append level names and ids after `.../rest.php/`, each separated by '/' .
E.g.:
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`
8121
8122
8123
8124
8125
8126
8127
8128
8129
8130
8131
8132
8133
8134
8135
8136
8137
8138
8139
8140
8141
8142
QFQ 'Forms' are used as a 'container' (to define all details).
.. tip::
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.
.. note::
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
data
Specific content to a given id. Defined via 'form.parameter.restSqlData'. This mode is selected if there is an
id>0 given.
list
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.

Carsten Rose
committed
**Form**
+-------------------+------------------------------------------------------------------------------+
| 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. |
+-------------------+------------------------------------------------------------------------------+

Carsten Rose
committed
**Form.parameter**
+-------------------+----------------------------------------------------------------------------------+
| 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).
.. important::
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
-------------

Carsten Rose
committed
**Form**
+-------------------+------------------------------------------------------------------------------+
| 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'. |
+-------------------+------------------------------------------------------------------------------+

Carsten Rose
committed
**Form.parameter**
+-------------------+----------------------------------------------------------------------------------+
| Attribute | Description |
+===================+==================================================================================+
| restParam | Optional. CSV list of variable names. E.g.: ``restParam=pId,adrId`` |
+-------------------+----------------------------------------------------------------------------------+
| restToken | Optional. User defined string or dynamic token (see `restAuthorization`_). |
+-------------------+----------------------------------------------------------------------------------+
FormElement:
* For each column to save one FormElement with ``FE.name=<column>`` is necessary.
* A regular QFQ form can be used as REST Post endpoint
PUT - Update
------------

Carsten Rose
committed
**Form**
+-------------------+------------------------------------------------------------------------------+
| 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 |
+-------------------+------------------------------------------------------------------------------+

Carsten Rose
committed
**Form.parameter**
+-------------------+----------------------------------------------------------------------------------+
| Attribute | Description |
+===================+==================================================================================+
| restParam | Optional. CSV list of variable names. E.g.: ``restParam=pId,adrId`` |
+-------------------+----------------------------------------------------------------------------------+
| restToken | Optional. User defined string or dynamic token (see `restAuthorization`_). |
+-------------------+----------------------------------------------------------------------------------+
FormElement:
* For each column to save one FormElement with ``FE.name=<column>`` is necessary.
* A regular QFQ form can be used as REST Post endpoint
DELETE - Delete
---------------

Carsten Rose
committed
**Form**
+-------------------+------------------------------------------------------------------------------+
| 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 |
+-------------------+------------------------------------------------------------------------------+

Carsten Rose
committed
**Form.parameter**
+-------------------+----------------------------------------------------------------------------------+
| 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.
.. _`restAuthorization`:
Authorization
-------------
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.
.. important::
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.
Example: ::
form.parameter.restToken=myCrypticString0123456789
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.
.. tip::
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

Carsten Rose
committed
to check it::
form.parameter.restToken={{SELECT a.token FROM Auth AS a WHERE a.token='{{Authorization:C:alnumx}}'}}
form.parameter.restSqlList={{!SELECT p.id, p.name, p.email 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 p.id='{{r:T0}}' }}
If authorization is denied, the request will be answered with a delay of 3 seconds (configured via securityFailedAuthDelay).

Carsten Rose
committed
.. _`system`:

Carsten Rose
committed
======
.. _form-editor-usage:
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}}'
form={{form:SE}}
# 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">'
, '</th><th>Name'
, '</th><th>Title'
, '</th><th>Table'
, '</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">

Carsten Rose
committed
rbeg = <thead class="qfq-sticky"><tr>
rend = </tr></thead><tbody>
tail = </tbody></table>
# All forms
sql = SELECT CONCAT('p:{{pageAlias:T}}&form=Form&r=', f.id) as _pagee
, CONCAT(f.name, ' <span class="text-muted">(', f.id, ')</span>')
, QMORE(strip_tags(f.title),50)
, CONCAT('p:{{pageAlias:T}}&formIdHistory=', f.id, '|s|b|t:<span class="badge">', COUNT(fsl.id), '</span>'
, IF(COUNT(fsl.id)=0, '|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>')
LEFT JOIN FormSubmitLog AS fsl
ON fsl.formId=f.id
WHERE {{formIdHistory:S0}}=0
GROUP BY f.id
ORDER BY f.name
rbeg = <tr>
rend = </tr>
fbeg = <td>
fend = </td>
}
}
# History of a Form {{formIdHistory:S0}}
sql = SELECT f.name
, fsl.feUser
, fsl.recordId
, fsl.pageId
, fsl.created
FROM FormSubmitLog AS fsl, Form AS f
WHERE fsl.formId={{formIdHistory:S0}}
AND fsl.formId=f.id
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>')

Carsten Rose
committed
.. _`autocron`:

Carsten Rose
committed
The `AutoCron` service fires periodically jobs like `open a webpage` (typically a QFQ page which does some database
actions) or `send mail`.

Carsten Rose
committed
* 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.

Carsten Rose
committed
* 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.

Carsten Rose
committed
* Running and processed jobs can easily be monitored via *lastRun, lastStatus, nextRun, inProgress*.

Carsten Rose
committed
Setup
^^^^^
* 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: ::

Carsten Rose
committed
* * * * * /usr/bin/php /var/www/html/typo3conf/ext/qfq/Classes/External/autocron.php

Carsten Rose
committed
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.

Carsten Rose
committed
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Carsten Rose
committed
Create a T3 page with a QFQ record (similar to the formeditor). Such page should be access restricted and is only needed

Carsten Rose
committed
to edit `AutoCron` jobs::

Carsten Rose
committed
dbIndex={{indexQfq:Y}}
form={{form:S}}

Carsten Rose
committed
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"','')),
'>'),

Carsten Rose
committed
'<td>', CONCAT('p:{{pageAlias:T}}&form=cron&r=', c.id) AS _pagee, '</td><td>',
c.id, '</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>',
c.autoGenerated,
CONCAT('U:form=cron&r=', c.id) AS _paged, '</td></tr>'
FROM Cron AS c
ORDER BY c.id
}
}

Carsten Rose
committed

Carsten Rose
committed

Carsten Rose
committed
Usage
^^^^^
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

Carsten Rose
committed
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

Carsten Rose
committed
Job: repeating

Carsten Rose
committed
""""""""""""""

Carsten Rose
committed
* frequency: '1 MINUTE', '2 DAY', '3 MONTH', ....

Carsten Rose
committed
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

Carsten Rose
committed
"""""""""""""""""

Carsten Rose
committed
* frequency: <empty>
An 'AutoCron' job becomes 'asynchronous' if `frequency` is empty. Then, auto repeating is switched off.

Carsten Rose
committed
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.

Carsten Rose
committed
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

Carsten Rose
committed
Type: Mail

Carsten Rose
committed
""""""""""

Carsten Rose
committed
At the moment there is a special sendmail notation - this will change in the future.

Carsten Rose
committed

Carsten Rose
committed
{{!SELECT 'john@doe.com' AS sendMailTo, 'Custom subject' AS sendMailSubject, 'jane@doe.com' AS sendMailFrom, 123 AS sendMailGrId, 456 AS sendMailXId}}

Carsten Rose
committed
AutoCron will send as many mails as records are selected by the SQL query in field `Mail`. Field `Mail body` provides

Carsten Rose
committed
the mail text.

Carsten Rose
committed

Carsten Rose
committed
Type: Website

Carsten Rose
committed
"""""""""""""

Carsten Rose
committed

Carsten Rose
committed
The page specified in `URL` will be opened.

Carsten Rose
committed
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.

Carsten Rose
committed
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

Carsten Rose
committed
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 - ...'.

Carsten Rose
committed
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 = 127.0.0.1]
page.10 < styles.content.get
[else]
# 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:
page.20 = TEXT
page.20.data = getenv : REMOTE_ADDR
[global]
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/...`

Carsten Rose
committed

Carsten Rose
committed
.. _applicationTest:

Carsten Rose
committed
Application Test
================

Carsten Rose
committed
With a framework like https://www.seleniumhq.org/ 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

Carsten Rose
committed
interact with the test framework.

Carsten Rose
committed
Form
----

Carsten Rose
committed
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>'.

Carsten Rose
committed
Report
------

Carsten Rose
committed
Any HTML output can be extended by a tag - that's done by the webmaster. For QFQ generated links, an attribute like

Carsten Rose
committed
'data-reference' might be injected via token 'A' (attribute). ::

Carsten Rose
committed

Carsten Rose
committed
SELECT 'p:personedit&form=person&r=1|b|s|A:data-reference="person-edit"|t:Edit person' AS _link

Carsten Rose
committed
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?
Tips:
* On general errors:
* Always check the Javascript console of your browser, see `javascriptProblem`_.
* Always check the Webserver log files.
Caching
-------
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.

Carsten Rose
committed
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}} }

Carsten Rose
committed
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"

Carsten Rose
committed
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.

Carsten Rose
committed
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Carsten Rose
committed
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
8751
8752
8753
8754
8755
8756
8757
8758
8759
8760
8761
8762
8763
8764
8765
8766
8767
8768
8769
8770
8771
8772
.. _setFeUserPassword:
Set FE-User password
^^^^^^^^^^^^^^^^^^^^
To offer an FE User the possibility to change the own T3 FE password, create a form: ::
f.name = 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.
Logging
-------
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
Call to undefined function qfq\\mb_internal_encoding()

Carsten Rose
committed
""""""""""""""""""""""""""""""""""""""""""""""""""""""
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.

Carsten Rose
committed
Oops, an error occurred! Code: 20180612205917761fc593
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Carsten Rose
committed
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.

Carsten Rose
committed
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
.. _`javascriptProblem`:
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.
TinyMCE
-------
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.