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_pnquestionnaire_saved_result holds one row per completed run.

Column Purpose
token 32 hex characters, 128 bits of entropy, from random_bytes(). Unique index. The only key to the row
result_url 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/Routing/SavedResult.yaml provides a route enhancer producing /{plugin-page}/saved-result/{token}. 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_url 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
Copied!

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 SystemEmail layout, so it inherits the styling already configured for TYPO3 mails.

Rich text inside the mail runs through lib.parseFunc_pnQuestionnaireMail , a copy of lib.parseFunc with forceAbsoluteUrl on. A relative href is a dead link in a mail client, and no Fluid ViewHelper can reach inside stored rich text — <f:format.html> 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/Services.yaml 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, reverseProxyIP has to be configured or every visitor looks like the same client.
  • 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.cookieSameSite 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_condition on tx_pnquestionnaire_advice_block inverts a specific_answer condition. It is applied in ResultResolverService , 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.