CoolCMS API

AccountDeletionManager
in package

FinalYes

Account deletion requested by the user — the whole flow in one place:

  1. request — the owner confirms with the password (self::requestDeletion()); an account without a password (OAuth) gets a confirmation link instead (self::confirmDeletion()),
  2. grace period (account.deletion.graceDays) — every session is revoked, sign-in is refused with a short-lived ticket (self::buildPendingSignInRefusal()), the deletion can be cancelled from the e-mail, after a sign-in, or by the administration (self::cancelDeletion()),
  3. anonymization by the cron (self::anonymizeDue()) or by the administration at once: every module step (AccountAnonymizationStepInterface) and then the account row.

Manager tier (process orchestrator): it owns no table, the cms_system_users writes go through UserManager. Every state change is written to the accountAudit log channel (user id and the way it happened, never the e-mail).

Table of Contents

Constants

AUDIT_CHANNEL  : string = 'accountAudit'
DELETED_LABEL_KEY  : string = 'front.general.user.deleted'
Translation of the author name shown instead of a deleted account.
MAX_CONSECUTIVE_FAILURES  : int = 5
The cron stops the batch after this many failed accounts in a row (database outage, not a bad account).
VIA_ADMIN  : string = 'admin'
VIA_CRON  : string = 'cron'
VIA_EMAIL  : string = 'email'
VIA_PASSWORD  : string = 'password'
VIA_TICKET  : string = 'sign-in'

Properties

$accountDeletionTicket  : AccountDeletionTicket
$anonymizationSteps  : array<string|int, mixed>
$passwordHasher  : PasswordHasher
$translator  : Translator
$userExternalLoginManager  : UserExternalLoginManager
$userManager  : UserManager
$userRefreshTokenManager  : UserRefreshTokenManager

Methods

__construct()  : mixed
anonymizeDue()  : array{anonymized: int, failed: int, graceDays: int, aborted: bool}
Cron — anonymizes accounts whose grace period has passed (oldest requests first). Every account runs in isolation: an exception (or a failed result) is logged by user id only, counted in `failed` and the batch goes on, so the account is retried by the next run. After {@see self::MAX_CONSECUTIVE_FAILURES} failures in a row the batch stops (most likely a database outage) and `aborted` is `true`.
anonymizeNow()  : Result
Administration: anonymize a pending account at once instead of waiting for the grace period.
buildPendingSignInRefusal()  : Result
Sign-in refused because the deletion is pending (password and OAuth sign-in, token refresh).
cancelDeletion()  : Result
Cancels a pending deletion — by the cancel token from the e-mail, or by the ticket handed out at a refused sign-in (`<userId>.<expires>.<hmac>`). Unknown, used and expired keys share one answer (`USER_DELETION_KEY_INVALID`): the action is public.
cancelDeletionByAdmin()  : Result
Administration: cancel the pending deletion of an account.
confirmDeletion()  : Result
The confirmation link of an account without a password was clicked — the deletion starts.
getDeletedUserLabel()  : string
"Deleted user" in the current language — shown instead of the author of a deleted account.
requestDeletion()  : Result
The signed-in owner asks for the deletion of their own account. `$userId` must come from the verified identity (session / JWT), never from the request.
anonymize()  : Result
IMPORTANT: a plain read-then-write by id would overwrite a cancel confirmed in between (e-mail, ticket, admin) and anonymize a restored account. Therefore: 1. atomic claim of exactly the request read (`deletion_requested_at` + nonce) — a cancel is refused from this moment (until the claim goes stale, {@see AccountDeletion::CLAIM_STALE_SECONDS}), 2. the claim is re-checked before every irreversible step; a lost claim stops the run, 3. the final overwrite of the row is one UPDATE guarded by the claim.
audit()  : void
claimLost()  : Result
The run lost its claim (cancelled after it went stale, new request, another run) — no further step.
clearRequest()  : Result
IMPORTANT: compare-and-set on the request read by the caller — refused (`$refusedCode`, nothing written) when an anonymization holds a fresh claim, the account was anonymized or the request changed in the meantime. OK means the account IS restored.
findByToken()  : User|null
Account whose stored deletion token hash matches the plain token (fresh from the DB).
requestEmailConfirmation()  : Result
startDeletion()  : Result

Constants

DELETED_LABEL_KEY

Translation of the author name shown instead of a deleted account.

public string DELETED_LABEL_KEY = 'front.general.user.deleted'

MAX_CONSECUTIVE_FAILURES

The cron stops the batch after this many failed accounts in a row (database outage, not a bad account).

public int MAX_CONSECUTIVE_FAILURES = 5

Properties

Methods

__construct()

public __construct(UserManager $userManager, UserRefreshTokenManager $userRefreshTokenManager, UserExternalLoginManager $userExternalLoginManager, PasswordHasher $passwordHasher, AccountDeletionTicket $accountDeletionTicket, Translator $translator, array<string|int, AccountAnonymizationStepInterface> $anonymizationSteps) : mixed
Parameters
$userManager : UserManager
$userRefreshTokenManager : UserRefreshTokenManager
$userExternalLoginManager : UserExternalLoginManager
$passwordHasher : PasswordHasher
$accountDeletionTicket : AccountDeletionTicket
$translator : Translator
$anonymizationSteps : array<string|int, AccountAnonymizationStepInterface>

every module's share (DI collects them)

anonymizeDue()

Cron — anonymizes accounts whose grace period has passed (oldest requests first). Every account runs in isolation: an exception (or a failed result) is logged by user id only, counted in `failed` and the batch goes on, so the account is retried by the next run. After {@see self::MAX_CONSECUTIVE_FAILURES} failures in a row the batch stops (most likely a database outage) and `aborted` is `true`.

public anonymizeDue([int $limit = 50 ]) : array{anonymized: int, failed: int, graceDays: int, aborted: bool}
Parameters
$limit : int = 50
Return values
array{anonymized: int, failed: int, graceDays: int, aborted: bool}

anonymizeNow()

Administration: anonymize a pending account at once instead of waiting for the grace period.

public anonymizeNow(int $userId) : Result
Parameters
$userId : int
Return values
Result

buildPendingSignInRefusal()

Sign-in refused because the deletion is pending (password and OAuth sign-in, token refresh).

public buildPendingSignInRefusal(User $user) : Result

The caller has just proven the account (password / provider), so the answer carries the date and a short-lived ticket that cancels the deletion — the sign-in itself never restores the account on its own.

Parameters
$user : User
Return values
Result

cancelDeletion()

Cancels a pending deletion — by the cancel token from the e-mail, or by the ticket handed out at a refused sign-in (`<userId>.<expires>.<hmac>`). Unknown, used and expired keys share one answer (`USER_DELETION_KEY_INVALID`): the action is public.

public cancelDeletion(string $key) : Result
Parameters
$key : string
Return values
Result

cancelDeletionByAdmin()

Administration: cancel the pending deletion of an account.

public cancelDeletionByAdmin(int $userId) : Result
Parameters
$userId : int
Return values
Result

confirmDeletion()

The confirmation link of an account without a password was clicked — the deletion starts.

public confirmDeletion(string $plainToken) : Result
Parameters
$plainToken : string
Return values
Result

getDeletedUserLabel()

"Deleted user" in the current language — shown instead of the author of a deleted account.

public getDeletedUserLabel() : string
Return values
string

requestDeletion()

The signed-in owner asks for the deletion of their own account. `$userId` must come from the verified identity (session / JWT), never from the request.

public requestDeletion(int $userId, string $password) : Result
  • account with a password: the password is verified (INVALID_PASSWORD), then the deletion starts at once — data scheduledAt,
  • account without a password (created by an OAuth sign-in): a confirmation link is e-mailed, USER_DELETION_CONFIRMATION_SENT; nothing else changes until it is clicked.
Parameters
$userId : int
$password : string
Return values
Result

anonymize()

IMPORTANT: a plain read-then-write by id would overwrite a cancel confirmed in between (e-mail, ticket, admin) and anonymize a restored account. Therefore: 1. atomic claim of exactly the request read (`deletion_requested_at` + nonce) — a cancel is refused from this moment (until the claim goes stale, {@see AccountDeletion::CLAIM_STALE_SECONDS}), 2. the claim is re-checked before every irreversible step; a lost claim stops the run, 3. the final overwrite of the row is one UPDATE guarded by the claim.

private anonymize(User $user, string $via) : Result

A failed step keeps the claim: the cron (or the administration) retries after it went stale.

Parameters
$user : User
$via : string
Return values
Result

audit()

private audit(string $event, int $userId[, array<string, scalar> $meta = [] ]) : void
Parameters
$event : string
$userId : int
$meta : array<string, scalar> = []

claimLost()

The run lost its claim (cancelled after it went stale, new request, another run) — no further step.

private claimLost(int $userId, string $via, string $step) : Result
Parameters
$userId : int
$via : string
$step : string
Return values
Result

clearRequest()

IMPORTANT: compare-and-set on the request read by the caller — refused (`$refusedCode`, nothing written) when an anonymization holds a fresh claim, the account was anonymized or the request changed in the meantime. OK means the account IS restored.

private clearRequest(User $user, string $via, ResultCode $refusedCode) : Result
Parameters
$user : User
$via : string
$refusedCode : ResultCode
Return values
Result

findByToken()

Account whose stored deletion token hash matches the plain token (fresh from the DB).

private findByToken(string $plainToken) : User|null
Parameters
$plainToken : string
Return values
User|null
On this page

Search results