Docs: Sign-in, forms and consent

Sign-in, forms and consent

Template API calls for forms a theme draws itself — sign-up checks, captcha boxes, CSRF tokens, the cookie banner and link, held scripts and the head extras an administrator sets.

Core's own views already call everything on this page. You need these calls only when your theme ships its own form, banner or head. All of them arrived in Tilspire 1.18.0 unless noted. Examples use $esc, the shipped theme's escaper: static fn ($v): string => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'). Text kinds: E escaped for you, P plain text to escape as you print it, M markup core made safe.

Sign-in, sign-up and captcha #

signupChecks() #

Prints the sign-up form's hidden trap field, its served-time stamp and, when the site's captcha is on, the captcha box. Returns nothing (since 1.17.3). Call it inside the <form> of views/auth-register.php, before the submit button. Without it, sign-up on that theme falls back to the per-address limit alone and Health warns. Any other form that posts to /account/register (a sign-up box in a sidebar or landing page) must call it too: only auth-register.php is looked at to decide whether the checks apply, so a sign-up from a second form without the call is refused.

<form method="post" action="/account/register">
    …
    <?php $this->signupChecks(); ?>
    <button type="submit">Create account</button>
</form>

captchaBox(string $surface) #

Prints the captcha box for one form, whichever provider the site chose (reCAPTCHA v2, reCAPTCHA v3 or Cloudflare Turnstile), plus the script v3 and Turnstile need. Returns nothing. $surface is the form's own name: signup, signin, password_reset, comment, new_ticket or reply_ticket. It prints nothing when that form asks for none, so it is safe to leave in. Pass exactly one of those six names: any other, a typo for example, throws InvalidArgumentException.

  • Put it inside the <form>, before the submit button.
  • Your views/auth-login.php, views/auth-forgot.php and any comment or ticket form you draw yourself call it. Without it a sign-in or forgot view is held to the sign-in throttle alone and Health warns.
  • A comment or ticket form without it can only draw a v2 box. Under any other provider its posts are refused, and Health warns.
  • Draw nothing of your own for v3 or Turnstile. The call also prints captcha.js, once a page, itself rather than through assets('js').
<?php $this->captchaBox('comment'); ?>
<button type="submit">Post comment</button>

signinIdentifier() #

Returns what the sign-in form asks for, one of three words: both, username or email. A database fault reads both. Core's views read the strings signin.identity_both, signin.identity_username and signin.identity_email.

<label for="identity"><?= $esc(t('signin.identity_' . $this->signinIdentifier())) ?></label>

formToken() and csrfField() #

formToken() returns the session's CSRF token (P: escape it) for a form on a page that is never cached, such as the account pages. It answers a guest too, so calling it takes the render out of the page cache.

csrfField() returns the hidden csrf_token input (E) for a POST to the visitor's logout_url or appearance_url. It returns '' for a guest, because a guest's page may be cached and one visitor's token must never reach the next.

<form method="post" action="<?= $esc($me['logout_url']) ?>"><?= $this->csrfField() ?><button type="submit">Sign out</button></form>

A sign-out is always a POST, never a link: any page could load a sign-out address. $this::PASSWORD_MIN_LENGTH is the shortest password core accepts, for a field's minlength and its hint.

A site may ask visitors which optional cookies and outside content they allow. While the Cookies and consent card on Settings ▸ Security is on, core holds back what you route through it and asks once; the answer is a cookie. When it is off, consent()['enabled'] is false, the banner and link calls return '', heldScript() returns a plain live tag, and nothing is held.

Every visitor gets the same page. The banner is hidden, held scripts are type="text/plain", and consent.js reveals or starts what the stored answer allows. Never branch on the answer in PHP: the page cache would store one visitor's version for everyone. In the browser, read it with window.cmsConsent.allows('media') or the cms:consent event on document; window.cmsConsent.open() opens the Choose panel.

consentBanner() #

Returns core's cookie banner and Choose panel in your theme's consent_style (M), or '' while consent is off. Calling it is also the claim of contract 10: <?php $this->consentBanner(); ?> declines core's banner without printing, for a theme that draws its own. Place it right after <body>, first in reading and tab order. A theme that never calls it gets core's banner anyway while consent is on.

<body>
<?= $this->consentBanner() ?>

Returns the "Cookie settings" button that reopens the Choose panel (M), with $class added to its own classes; '' while consent is off. It stays hidden until consent.js shows it, so a visitor without JavaScript never sees a dead control. Calling it claims contract 11.

<?= $this->consentLink('footer-link') ?>

Returns {enabled, version, cookie, days, style, categories, policy_url} and claims nothing. policy_url is plain text and may be null (leave the "Cookie policy" link out); the rest is safe to print. categories is the switches the panel shows, necessary first; style is your consent_style option, else bar.

heldScript(string $category, string $src, array $data = [], bool $async = false) #

Returns a <script> that starts only once the visitor allows $category (M). It escapes both values, so pass them raw. Use it for a third-party script, and keep any loader in a file of the theme, never inline.

  • Consent on: <script type="text/plain" data-cms-consent="statistics" data-src="…">, replaced by a classic script once allowed. Your $data keys become data-<key> attributes, so document.currentScript works inside it.
  • Consent off, or category necessary: a live <script src="…">, with async when $async is true.
  • A category core does not know is held until the visitor allows every optional one.
<?php if ($ga_id !== ''): ?>
    <?= $this->heldScript('statistics', 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode($ga_id), [], true) ?>
    <?= $this->heldScript('statistics', $this->asset('ga.js') . '?v=' . $this->assetVersion('ga.js'), ['ga-id' => $ga_id]) ?>
<?php endif; ?>
  • Categories: necessary (always on), preferences, statistics, media. preferences has no switch in the panel until your theme.json says "consent": ["preferences"] (or a module uses it).
  • Embeds need nothing from you. Every <iframe> to another host in the finished page becomes a placeholder offering to play or show it, with "Always allow external media". Mark a frame that must always load data-cms-consent="necessary".
  • The cookie is site_consent, value v<version>.p<0|1>.s<0|1>.m<0|1> (for example v1.p0.s1.m0), Path=/, SameSite=Lax, Secure on https, 180 days, written by consent.js only. A malformed value, or one of another version, is no answer.
  • Not held: your own plain <script src> tags and the site's custom head code, unless written as type="text/plain" data-cms-consent="statistics". Health says so.

Drawing your own banner #

Call <?php $this->consentBanner(); ?> and write the markup from consent().

  • The root carries data-cms-consent-root, data-cms-consent-version="<version>", data-cms-consent-cookie="<cookie>" and data-cms-consent-days="<days>".
  • Controls are data-cms-consent-action="accept|reject|choose|save|open|close".
  • A switch is an <input type="checkbox" data-cms-consent-category="statistics">. Start it unchecked; never pre-tick.
  • The panel is a <dialog data-cms-consent-panel hidden> and the banner has data-cms-consent-banner hidden.
  • Give Reject and Accept the same look. Write no inline script and no on*= handler: consent.js wires every control wherever it is.

Head extras #

headExtras() #

Returns {analytics_id, head_code}: the site's stored Analytics id and the custom head code an administrator entered under Settings ▸ Security. analytics_id is plain text (P) and '' when none is set. head_code is raw HTML, printed as it is, as the setting promises. It comes from the same single settings read as site(), so a theme never queries the settings table. Pass the Analytics id through heldScript() so cookie consent can hold it.

<?php $extras = $this->headExtras(); ?>
<?php if ($extras['head_code'] !== '') { echo $extras['head_code']; } ?>