Abandonment and recovery
Paid add-on. A form can offer the visitor to keep their input and send them a link to finish it later.
This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Resuming a form you started”.
Two consents, and the second is the real one
The administrator switches the feature on for a form. The visitor gives theirs, explicitly, before the first save — a checkbox that is never ticked in advance.
These do not count as consent: having clicked “next”, having typed an address, having filled in a generic consent field that concerns something else, or having failed to untick something. Refusing leaves the form fully usable: no row, no reminder, no journey data.
The consent is kept with the version of the text accepted. Knowing that somebody consented says nothing if you do not know to what: a text rewritten without changing version would make the agreement unenforceable. The admin shows both.
The default text says what is kept, why, for how long and that an email will be sent. It is an honest starting point, not legal advice: it is up to the site to reconcile it with its privacy policy and its market.
Nothing is duplicated from the core
The core already knows how to save a draft, strip its sensitive values, make a
signed resume link, promote it into a submission and purge it. This module has
neither a save route nor a notion of draft of its own: saving goes through
SubmissionHandler::save_draft(), with its anti-spam checks, its token, its
availability rules and its sanitisation.
It listens to the event that follows — solis_forms_draft_saved — and works on
what the core has already kept.
A defect fixed along the way
Until now, every save created a new entry. Five clicks on “save” gave five drafts, five valid resume links and five copies of the same personal input in the database; an autosave would have produced one a minute.
DraftManager::save() now accepts the draft to resume, resolved through its
signed reference — an id alone does not allow writing into somebody else’s
input.
The journey is reduced, server-side
The aim is a useful reminder, not navigation analytics. What is kept:
- the referrer reduced to its origin and path, with no query string;
- the campaign parameters of the entry page, and only the five
utm_*keys; - the internal pages already seen, deduplicated, limited to ten and truncated;
- the step at which the input stopped;
- the first and last activity.
Everything else is discarded. The reduction happens on the server: a forged request must not make it possible to file an external URL, a query string or arbitrary keys into a site’s database — which would turn it, unbeknownst to its owner, into a store of browsing data.
Why those refusals specifically:
- a referrer’s query string regularly carries a session id, a reset token or a search term;
- external addresses: following somebody off the site is exactly what this feature promises not to do;
- the fragment does not normally leave the browser, and receiving one already signals a hand-crafted request.
No IP address, no user agent, no field content, no session id, no page element. The core’s entry keeps its own context under its own rules; this module does not replicate it.
The address is not copied
Only its fingerprint — a HMAC — appears in the Pro table, and it serves to establish that two drafts aim at the same inbox without any list displaying an address. The real address is read from the draft at the moment of sending, and disappears with it.
A second copy would have outlived its source and would have had to be purged separately.
One reminder, and one only
It goes out when every condition is met: recovery is active, consent exists, a valid address appears in the draft, the draft is still a draft, and the delay has passed.
The scheduled task does not trust whatever scheduled it. In between, the input may have been submitted, the consent withdrawn, the form deactivated, the address changed. Everything is re-read before sending, and a task that has become pointless ends without dispatching anything.
“Send now”, from the admin, is no exception: the reminder stays limited to one per draft. An extra manual reminder is a product decision, not an open door to spam — and it is precisely from that screen that one would be tempted to insist.
The message
The form’s name, the deadline, two links. No submitted value, no journey data, no summary. The message goes to an inbox, and what it contains leaves with it.
The links, and their revocation
The core knows how to make a signed resume link. It does not know how to revoke one: a signature knows only its own validity, and has no idea that someone wanted to withdraw it. Yet a withdrawal of consent must stop a link that has already gone out.
The email therefore carries an add-on link, of which only the fingerprint is stored. Revoking regenerates the secret: the old one no longer matches anything. The core’s link is made only at the moment of a valid click, and exists only for the duration of a redirect — it stays neither in the history, nor in the address bar, nor in the access log.
The two links — resume, erase — are signed in distinct contexts. Signing them in the same one would allow presenting one to the other’s route whenever the row numbers coincided, and erasing somebody’s draft with the link meant to give it back to them.
The erase link works without signing in, erases the draft immediately along with everything kept about it, and shows a confirmation naming neither the address, nor the form, nor what was erased: it can be opened by whoever received the email forwarded.
Without JavaScript
No autosave, no journey data, and no consent checkbox — asking agreement for something that will not happen would make no sense. The core’s manual resume button stays available.
Retention and purge
The duration follows that of “save and resume”, bounded between one and thirty days. Deleting a submission erases its recovery; an hourly task sends the reminders that are due and removes recoveries whose draft has disappeared — by a direct query, or while the add-on was deactivated.
Input that goes through moves to resumed: no further reminder goes out, and its
journey does not survive as standalone data.
What the module does not do
No third-party cookie, no pixel, no browser fingerprint, no geolocation, no session replay, no keystroke capture. No sequence of several emails, no A/B testing, no marketing score, no segmentation, no sending through an external provider.
It does not follow pages that do not contain the form, does not reconstruct the browser’s history and follows nobody from one site to another.
