Skip to content

WordPress accounts

Paid add-on. A form can create a WordPress account from a submission, or update the signed-in visitor’s own. Memberships, courses, client portals, paid registrations.

This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Creating member accounts”.

The idea

The setting lives in the form’s settings, under user_account. There is no configuration table: a form exported and re-imported takes its account action with it, and a duplicated form duplicates it.

{
  "user_account": {
    "enabled": true,
    "mode": "create",
    "email_field": "email_1",
    "role": "subscriber",
    "profile": {
      "first_name": "name_1_first",
      "last_name": "name_1_last",
      "display_name": "name_1_first"
    },
    "meta": [
      { "key": "membership_number", "field": "text_4" }
    ],
    "send_password_setup": true
  }
}

A reference designates a field — email_1 — or one of its parts for composite fields — name_1_first, address_1_postal_code.

The two modes, and the one that is missing

create creates an account. update_current updates the signed-in visitor’s, and only theirs.

The third mode that comes to mind — finding an account by its email address on an anonymous form — does not exist, and its absence is a decision. Knowing somebody else’s address would be enough to rewrite their profile: that is an account takeover, not an update.

In update mode the identity always comes from the session. Hiding the form from anonymous visitors protects nothing, since a forged request never goes through the rendering; the “Login required” access rule is a courtesy, the server-side check is the control.

No password goes through the form

The account is born with wp_generate_password(), a secret nobody reads — not even the code that makes it, which hands it straight to wp_insert_user(). The member then receives WordPress’s standard email, with a time-limited link to set their own.

A “password” field on the form would have carried the secret in clear through the request, through the server logs and, on a badly configured form, into the notification sent to the administrator.

If sending fails, the account stays created: undoing it would lead the administrator to create a second one. The reason mail_failed is written to the audit, and a manual reset is enough.

The role, and the two safeguards

By default, only subscriber can be assigned. Opening the list takes code:

add_filter(
	'solis_forms_pro_allowed_registration_roles',
	static fn ( array $roles ): array => array_merge( $roles, array( 'member' ) )
);

A second check applies after that filter: a role carrying a capability that could take over the site is set aside even when it was explicitly allowed — manage_options, edit_users, install_plugins, unfiltered_html, solis_forms_manage_settings and a few others. administrator is refused by name on top of that check.

The reason is not the administrator’s malice, it is their mistake: a “Manager” role created by a third-party plugin can carry edit_users without its name hinting at it, and the form that assigns it looks like every other.

A role configured outside that list does not quietly fall back to subscriber: the whole action is refused, because a setting that says something other than what will be done is worse than a setting that does nothing.

Metadata

Keys are checked before writing. Refused are:

  • those beginning with an underscore, WordPress’s convention for “internal”;
  • those beginning with wp_, with the site’s table prefix or with the network’s — on a site whose tables are called mysite_, capabilities live under mysite_capabilities, and on the second site of a network under wp_2_capabilities;
  • capabilities, user_level, session_tokens, role, user_pass, user_login, user_email and a few others, along with anything ending in _capabilities or _user_level;
  • those whose shape falls outside [A-Za-z][A-Za-z0-9_-]*.

On the profile side the list is a whitelist, not a blacklist: only first_name, last_name and display_name are written. Enumerating what is allowed means an omission closes rather than opens.

user_email is not among them. Changing an existing account’s address requires a confirmation at the old address, failing which a form is enough to hijack an account; that is outside this version’s scope.

An address already taken

Creation fails, the submission stays recorded, and the visitor learns nothing. The form’s confirmation is the one they would have had anyway.

That silence is the most delicate point of the module. Answering “this address already has an account” would answer the question “is this person registered here?” for any address someone cared to try, one at a time. The reason email_taken is written to the audit, which only an administrator reads.

It is also why this module does not subscribe to solis_forms_submission_errors, unlike payments or the checklist: an error there would be shown to the visitor.

At most one account per submission

The audit table’s primary key is the submission’s id. Two operations for the same entry are structurally impossible, and a replayed submission creates no duplicate and sends no second email.

Should a failure occur between creating the account and writing the audit, the retry finds the account again through an internal meta placed on it, _solis_forms_account_entry, never through the address: two people can share a departmental address, and a namesake must not find somebody else’s request attached to them. The leading underscore puts that meta out of the mapping’s reach.

Payment

The account is born on solis_forms_entry_created, at priority 8 — after the payment is attached, at priority 5.

What matters, though, lies elsewhere: a paid submission whose payment is not confirmed produces no entry at all. The payment guard answers on solis_forms_submission_errors, well before anything is recorded. This event is therefore only reached with a verified payment; the priority order merely writes that down in the code.

No creation is ever triggered directly by a gateway webhook.

What the audit keeps

The mode, the status, the account id, the role, the reason, the destinations written, the date. Never the password, never the reset token, never the address, and never the values written — only where they were written.

The previous values of an updated profile are not kept either: they would constitute a history of personal data the member believes they have corrected.

Reasons are codes — email_taken, not_logged_in — translated at display time. Writing WordPress’s own message there would freeze one language into the database and bring into it the address that triggered it.

Deletion

Deleting a submission erases its audit trace and its note. The WordPress account survives. The member has signed in, may have ordered something, may appear in another form: they do not belong to the entry that brought them into being, and a cascading deletion would be a surprise with no way back.

Deleting an account is a matter for WordPress’s user administration, and for the site’s retention policy.

What the module does not do

No social login, no SSO, no MFA, no LDAP directory, no groups. No address change, no account merging, no account deletion, no role creation, no multiple roles. No manual approval either: a deferred activation needs a distinct member state, and adding it halfway would leave accounts in an in-between that nothing describes.

A visitor who is already signed in and fills in a creation form does not get a second account: their submission is attached to the one they have.

Rights

Configuring it requires solis_forms_manage_forms. The audit block follows solis_forms_view_entries and the form’s access rules. The link to the member’s profile appears only to those who can edit it.