CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m57s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m44s
CI / Build Plugin Zip (pull_request) Skipped
The password was only ever checked for length. It is now checked on both sides, with each side doing the job it can actually do. The browser scores it with zxcvbn, through WordPress's own password-strength-meter script rather than a second opinion of our own, and refuses to submit below "medium". That is the nuanced test — it knows Tr0ub4dor&3 is weaker than it looks — but it is advice a client can decline to take. Auth\PasswordPolicy runs on the server and is the rule that holds. It does not try to reproduce a strength score in PHP; it rejects the categorically bad, which is what a server can check without shipping a dictionary: too short, a well-known leaked password, fewer than four distinct characters, or the user's own name or email inside it. No composition rules — NIST advises against them, and they mostly produce predictable substitutions. Both thresholds come from the same two constants, handed to JavaScript by wp_localize_script, so the sides cannot drift into disagreeing about what was accepted. The verdict is attached to the field with setCustomValidity() rather than by disabling a button. The form has up to three submits plus a "Next" that already gates on checkValidity(), and an invalid field stops all of them without any of them needing to know why. Email validation moved ahead of the password check, since the password is now checked against the email. A blank form therefore reports the email first, which also matches the order the fields appear in. Verified the browser half against a controllable scorer: each score band blocks or allows as intended, the identity list reaches the meter, and the gate stays open while zxcvbn's dictionary is still loading — the server covers that window. Closes #150 Co-Authored-By: Claude Opus 5 <[email protected]>
186 lines
12 KiB
PHP
186 lines
12 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use Unsupervised\Schedular\Auth\PasswordPolicy;
|
|
use Unsupervised\Schedular\Registration\Question;
|
|
use Unsupervised\Schedular\Registration\QuestionField;
|
|
|
|
if (! defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* @var \Unsupervised\Schedular\Auth\Invite|null $invite
|
|
* @var bool $inviteValid Whether $invite can still be redeemed — only then is the email fixed.
|
|
* @var string $token Raw invite token from the request (only its hash is stored).
|
|
* @var bool $canRegister
|
|
* @var string $inviteOnlyMessage Text shown when registration is closed and no valid invite is present.
|
|
* @var bool $open Whether open (self-approval) registration is enabled.
|
|
* @var string $successType '' | 'confirm' (check email) | 'confirm_group' (check email; auto-approved on confirm). The invited-student success is rendered by RegistrationPage::render() itself, which returns before this template for logged-in visitors.
|
|
* @var string $confirmResult '' | '1' (email confirmed, awaiting approval) | 'ready' (confirmed + auto-approved) | 'expired'.
|
|
* @var string $loginUrl Where the post-confirmation sign-in link points.
|
|
* @var string $error
|
|
* @var list<array{policy: \Unsupervised\Schedular\Policy\Policy, version: \Unsupervised\Schedular\Policy\PolicyVersion}> $policyForms
|
|
* @var list<Question> $accountQuestions Studio-wide questions answered as step two.
|
|
*/
|
|
|
|
?>
|
|
<div class="us-register-form">
|
|
<?php if ($successType === 'confirm') : ?>
|
|
<p class="us-success"><?php esc_html_e('Your account has been created. Check your email for a link to confirm your address — once you do, a studio admin will review and approve your account.', 'unsupervised-schedular'); ?></p>
|
|
<?php elseif ($successType === 'confirm_group') : ?>
|
|
<p class="us-success"><?php esc_html_e('Your account has been created. Check your email for a link to confirm your address — once you do, your account is ready to use.', 'unsupervised-schedular'); ?></p>
|
|
<?php elseif ($confirmResult === 'ready') : ?>
|
|
<p class="us-success"><?php esc_html_e('Thanks — your email is confirmed and your account is ready to use.', 'unsupervised-schedular'); ?></p>
|
|
<p><a href="<?php echo esc_url($loginUrl); ?>"><?php esc_html_e('Sign in to your account', 'unsupervised-schedular'); ?></a></p>
|
|
<?php elseif ($confirmResult === '1') : ?>
|
|
<p class="us-success"><?php esc_html_e('Thanks — your email is confirmed. Your account is now awaiting studio approval; we will email you when it is ready.', 'unsupervised-schedular'); ?></p>
|
|
<p><a href="<?php echo esc_url($loginUrl); ?>"><?php esc_html_e('Sign in to your account', 'unsupervised-schedular'); ?></a></p>
|
|
<?php else : ?>
|
|
<?php if ($confirmResult === 'expired') : ?>
|
|
<p class="us-error" role="alert"><?php esc_html_e('That confirmation link is invalid or has expired. Please contact the studio.', 'unsupervised-schedular'); ?></p>
|
|
<?php endif; ?>
|
|
|
|
<?php if (! $canRegister) : ?>
|
|
<p><?php echo esc_html($inviteOnlyMessage); ?></p>
|
|
<?php else : ?>
|
|
<?php if ($error !== '') : ?>
|
|
<p class="us-error" role="alert"><?php echo esc_html($error); ?></p>
|
|
<?php endif; ?>
|
|
|
|
<?php $hasQuestions = ! empty($accountQuestions); ?>
|
|
<form method="post" action="" <?php echo $hasQuestions ? 'data-steps="1"' : ''; ?>>
|
|
<?php wp_nonce_field('us_student_register'); ?>
|
|
<input type="hidden" name="us_invite" value="<?php echo esc_attr($token); ?>">
|
|
|
|
<div class="us-reg-step" data-step="1">
|
|
<p>
|
|
<label for="us-reg-email"><?php esc_html_e('Email', 'unsupervised-schedular'); ?></label>
|
|
<?php if ($inviteValid && $invite !== null && ! $invite->isGroup()) : ?>
|
|
<input type="email" id="us-reg-email" value="<?php echo esc_attr($invite->email); ?>" readonly>
|
|
<?php else : ?>
|
|
<input type="email" name="email" id="us-reg-email" autocomplete="email" required>
|
|
<?php endif; ?>
|
|
</p>
|
|
<p>
|
|
<label for="us-reg-name"><?php esc_html_e('Your name', 'unsupervised-schedular'); ?></label>
|
|
<input type="text" name="display_name" id="us-reg-name" autocomplete="name" required>
|
|
</p>
|
|
<p>
|
|
<label for="us-reg-pass"><?php esc_html_e('Password', 'unsupervised-schedular'); ?></label>
|
|
<input type="password" name="password" id="us-reg-pass" autocomplete="new-password" minlength="<?php echo esc_attr((string) PasswordPolicy::MIN_LENGTH); ?>" required aria-describedby="us-reg-pass-strength">
|
|
<?php
|
|
/*
|
|
* Filled in by register.js. `aria-live` announces the verdict as
|
|
* it changes, and it starts empty so nothing is announced — or
|
|
* takes up space — before anything has been typed.
|
|
*/
|
|
?>
|
|
<span class="us-password-strength" id="us-reg-pass-strength" role="status" aria-live="polite"></span>
|
|
</p>
|
|
|
|
<fieldset class="us-guardian">
|
|
<legend><?php esc_html_e('Who are you registering?', 'unsupervised-schedular'); ?></legend>
|
|
<p>
|
|
<label>
|
|
<input type="checkbox" name="us_is_guardian" id="us-is-guardian" value="1">
|
|
<?php esc_html_e("I'm registering as a parent or guardian, for one or more students", 'unsupervised-schedular'); ?>
|
|
</label>
|
|
</p>
|
|
|
|
<?php /* Revealed by the checkbox; without JS it is simply always visible. */ ?>
|
|
<div class="us-children" id="us-children">
|
|
<p class="us-children-intro"><?php esc_html_e('Add each student you will be booking lessons for. They do not need their own login — you book and pay for them from this account.', 'unsupervised-schedular'); ?></p>
|
|
|
|
<?php /* The first block is the template the "Add another student" button clones. */ ?>
|
|
<div class="us-child" data-child-index="0">
|
|
<p>
|
|
<label for="us-child-0-name"><?php esc_html_e("Student's name", 'unsupervised-schedular'); ?></label>
|
|
<input type="text" name="children[0][name]" id="us-child-0-name">
|
|
</p>
|
|
<p>
|
|
<label for="us-child-0-birth-year"><?php esc_html_e('Birth year', 'unsupervised-schedular'); ?></label>
|
|
<input type="number" name="children[0][birth_year]" id="us-child-0-birth-year" min="1900" max="<?php echo esc_attr(current_time('Y')); ?>" step="1" inputmode="numeric" placeholder="<?php esc_attr_e('YYYY', 'unsupervised-schedular'); ?>">
|
|
</p>
|
|
<?php foreach ($accountQuestions as $question) : ?>
|
|
<?php
|
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
|
echo QuestionField::render(
|
|
$question,
|
|
'children[0][answers][' . (int) $question->id . ']',
|
|
'us-child-0-q-' . (int) $question->id,
|
|
enforceRequired: false
|
|
);
|
|
?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<p>
|
|
<button type="button" class="us-add-child"><?php esc_html_e('Add another student', 'unsupervised-schedular'); ?></button>
|
|
</p>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<?php if (! empty($policyForms)) : ?>
|
|
<fieldset class="us-policies">
|
|
<legend><?php esc_html_e('Policies', 'unsupervised-schedular'); ?></legend>
|
|
<?php foreach ($policyForms as $form) : ?>
|
|
<div class="us-policy">
|
|
<h4><?php echo esc_html($form['policy']->title); ?></h4>
|
|
<div class="us-policy-body"><?php echo wp_kses_post($form['version']->bodyHtml()); ?></div>
|
|
<label>
|
|
<input type="checkbox" name="accept[]" value="<?php echo esc_attr((string) $form['version']->id); ?>" required>
|
|
<?php
|
|
/* translators: %s: policy title */
|
|
echo esc_html(sprintf(__('I have read and agree to the %s.', 'unsupervised-schedular'), $form['policy']->title));
|
|
?>
|
|
</label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</fieldset>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($hasQuestions) : ?>
|
|
<p>
|
|
<button type="button" class="us-reg-next"><?php esc_html_e('Next', 'unsupervised-schedular'); ?></button>
|
|
<?php
|
|
/*
|
|
* In parent/guardian mode the questions are asked per child,
|
|
* up in the children section, so step two has nothing left to
|
|
* ask and "Next" leads nowhere. This submit takes its place —
|
|
* hidden until the guardian box is ticked (and never shown at
|
|
* all without JS, where both steps are visible anyway and the
|
|
* step-two submit does the job).
|
|
*/
|
|
?>
|
|
<input type="submit" name="us_register" class="us-reg-submit-early" hidden value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
|
</p>
|
|
<?php else : ?>
|
|
<p>
|
|
<input type="submit" name="us_register" value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
|
</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if ($hasQuestions) : ?>
|
|
<div class="us-reg-step" data-step="2">
|
|
<fieldset class="us-reg-questions">
|
|
<legend><?php esc_html_e('Registration information', 'unsupervised-schedular'); ?></legend>
|
|
<?php foreach ($accountQuestions as $question) : ?>
|
|
<?php
|
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- QuestionField::render() escapes every interpolated value.
|
|
echo QuestionField::render($question, 'us_answers[' . (int) $question->id . ']', 'us-reg-q-' . (int) $question->id);
|
|
?>
|
|
<?php endforeach; ?>
|
|
</fieldset>
|
|
<p>
|
|
<button type="button" class="us-reg-back"><?php esc_html_e('Back', 'unsupervised-schedular'); ?></button>
|
|
<input type="submit" name="us_register" value="<?php esc_attr_e('Create Account', 'unsupervised-schedular'); ?>">
|
|
</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</form>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|