Stored results and the result mail
Both are off by default and are switched on per plugin instance, with settings.db_save_result_enabled and settings.mail_result_enabled. This chapter covers what happens once they are on — the editor-facing side as well as the technical one.
What the visitor gets
With storing on, the result page gains a link the visitor can save or share. Opening it later brings back the same result, without a login and without the visitor's session.
Nothing identifying is stored: no name, no IP address, no e-mail address, not even in hashed form. What is stored is the given answers, the score and a random code that appears in the link. Someone without the link cannot find the result, and the answers cannot be traced back to a person.
Two consequences worth knowing:
- The result disappears once the retention period expires. Say so in the text around the link if visitors are likely to rely on it.
- The link is the only key. A visitor who loses it cannot recover the result, and neither can an administrator.
Point settings.privacy_link at the page where your organisation explains this. The extension only links to it — the text itself is yours to write, and it should mention the retention period and that no personal data is stored.
With mailing on, the result page shows a form asking for an e-mail address. The visitor receives the full result plus the retrieval link. The address is used to send that one mail and is not stored — not in the database, not in a log, not on the stored result.
The table
tx_ holds one row per completed run.
| Column | Purpose |
|---|---|
token | 32 hex characters, 128 bits of entropy, from random_.
Unique index. The only key to the row |
result_ | The generated retrieval URL, cached so the mail and the result page do not have to rebuild it |
questionnaire | The questionnaire the run belongs to |
answers | The given answers, as JSON |
score | The calculated score |
expires | Unix timestamp after which the row is due for removal |
crdate, tstamp, deleted | Standard TYPO3 fields; the table uses soft delete |
The
crdate
timestamp is the only field that could in theory be correlated
with an access log, which is a reason not to extend the retention period beyond
what is needed.
The retrieval route
Configuration/ provides a route enhancer
producing /. The site configuration
has to import it; see The retrieval route.
Without that import the retrieval link still works, but as a query-string URL.
The first segment follows the site language — saved-result,
bewaarde-uitslag, gespeichertes-ergebnis. Add a locale to the
localeMap for another language, or override the whole
PnQuestionnaireSavedResult key after the import to change the wording. The
token requirement is pinned to [0-9a-f]{32} so the enhancer never claims
an unrelated URL on a page without the plugin.
After a domain change or a change to this route, the stored
result_
values are stale. They can be regenerated from the tokens — the URL is derived
data, not a source.
Retention and purging
The retention period comes from settings.db_save_result_lifetime_days, falling
back to settings.db_save_result_lifetime_days. It is applied when a result is
stored, by writing
expires
.
Expired rows are removed by a console command:
vendor/bin/typo3 pnquestionnaire:purgesavedresults
vendor/bin/typo3 pnquestionnaire:purgesavedresults --dry-run
Register it as a scheduler task to keep the table clean. Unlike a backend deletion, the command removes rows for real rather than flagging them deleted — a stored result that has expired should not linger in the recycler.
The result mail
The mail is sent as HTML and plain text, using the installation's own
System layout, so it inherits the styling already configured for
TYPO3 mails.
Rich text inside the mail runs through
lib., a copy of
lib. with
force on. A relative
href
is a dead link in a mail client, and no Fluid ViewHelper can reach
inside stored rich text —
<f: hands the HTML straight to
parseFunc. The nested parseFunc references for lists, preformatted text and
table cells are repointed one by one; without that a link inside a list would
still come out relative.
The sender falls back through three levels: the plugin's FlexForm, then TypoScript, then the installation's default sender. See settings.mail_from_address.
The send limit
Configuration/ defines a rate limiter for the mail form:
three sends per hour, counted separately per recipient address and per
client address. It uses the Symfony rate limiter that ships with the core, so
there is no custom counting.
Two things to be aware of:
- The per-client limit is only as reliable as the proxy configuration. Behind
a reverse proxy,
reversehas to be configured or every visitor looks like the same client.Proxy IP - settings.mail_rate_limit_disabled switches the limit off. It is meant for testing an installation and should be off on a live site.
The form's protection against cross-site submission relies on
FE. being lax or stricter. That is a requirement, not a
detail.
Usage counters
With settings.statistics_enabled on, the questionnaire record
keeps two tallies:
starts
, incremented when a visitor begins, and
completions
, incremented on a completed run. They are plain counters on
the questionnaire record — no per-visitor data is involved.
Inverted conditions
negate_ on
tx_ inverts a
specific_answer condition. It is applied in
Result, at
the point where the block's trigger answer is matched against the given answers.
An absent answer counts as "not given", so an inverted block also appears for a
question the visitor never answered — which is why
Step 7 — Add advice blocks recommends making such questions required.