sirius-press: a site with passwords off could not be signed into
Three faults, all on the same screen, all only visible once passwords were actually turned off — which until now nobody had done on a live instance. wp-login.php runs wp_signon() on its way to drawing the form, even for a plain GET. Core tolerates that by discarding the result, but only when the error codes are exactly the pair its own password filters produce. Removing those filters is the whole point of turning passwords off, so what was left behind was the generic authentication_failed — printed, in red, above a form with no username and no password, on a site that has neither. Every visitor was told their sign-in had failed before they tried. The paste-a-signature path had no way to submit. It borrowed core's login button, and the stylesheet that retires the password form hid that button too. The one route for people whose keys live on another machine — sign in Electron Cash, paste the result — was therefore broken exactly on the sites most committed to not having passwords. It now carries its own plain submit button, so it also works on the screens where there is no core login form to borrow from, and with JavaScript off. And the username field outlived the password field, although authenticate() never reads it either. "Username or Email Address" sat at the top of a wallet login form: the one thing this fork exists to remove, in the first place anyone looks. Both go now, with their required attributes cleared, because a hidden required field does not fail quietly — it refuses the submit and points an error bubble at a control nobody can see.
This commit is contained in:
parent
4c5d43c4f0
commit
373394b534
5 changed files with 128 additions and 13 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -116,6 +116,28 @@ merge now runs in a scratch worktree and a failed run can be retried.
|
|||
|
||||
### Fixed while building a real site on it
|
||||
|
||||
- **Turning passwords off made the login screen fail on arrival.**
|
||||
`wp-login.php` calls `wp_signon()` on its way to drawing the form, even for
|
||||
a plain GET, and core throws the result away only when the error codes are
|
||||
exactly the pair its own password filters produce. Removing those filters —
|
||||
which is the entire point of turning passwords off — left the generic
|
||||
`authentication_failed` behind, so every visitor met a red "Invalid
|
||||
username, email address or incorrect password" above a form with neither
|
||||
field, on a site with no passwords.
|
||||
- **The paste-a-signature path had no way to submit.** It borrowed core's
|
||||
login button, and the stylesheet that retires the password form was hiding
|
||||
that button along with it. So the one route meant for people whose keys live
|
||||
on another machine — sign in Electron Cash, paste the result — was a dead
|
||||
end precisely on the sites most likely to need it. It now carries its own
|
||||
submit button, which also makes the block work on the screens where there is
|
||||
no core login form to borrow from.
|
||||
- **The username field outlived the password field.** With passwords off,
|
||||
`authenticate()` never reads the username either, yet "Username or Email
|
||||
Address" stayed at the top of the form — the exact thing this fork exists to
|
||||
remove, in the first position a visitor looks. Both fields are now retired
|
||||
together, and their `required` attributes are cleared so a hidden field
|
||||
cannot silently refuse a submit.
|
||||
|
||||
- **The image had no wp-cli.** `docs/publishing.md` documents
|
||||
`wp sirius export` and `wp sirius status`, and the export plugin registers
|
||||
both — on a stack built from `docker/`, neither existed. wp-cli is now in
|
||||
|
|
|
|||
|
|
@ -183,3 +183,8 @@
|
|||
margin: 10px 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* The paste-a-signature path's own submit button. It lives inside a <details>
|
||||
that is closed by default, so it needs the breathing room core's form gave
|
||||
the button it used to borrow. */
|
||||
.sirius-wallet__submit { margin-top: 10px; }
|
||||
|
|
|
|||
|
|
@ -198,7 +198,28 @@
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Let the form submit once core's credential fields have been retired.
|
||||
*
|
||||
* With passwords off they are hidden, unread and still marked `required`,
|
||||
* and a hidden required field does not fail quietly: the browser refuses
|
||||
* the submit and tries to focus a control that is not on screen. The wallet
|
||||
* buttons work around this with `noValidate`, but the paste-a-signature
|
||||
* path uses the form's own submit button and never reaches that code.
|
||||
*/
|
||||
function retireCredentialFields() {
|
||||
if (CONFIG.passwords !== false) return;
|
||||
["user_login", "user_pass"].forEach((id) => {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) return;
|
||||
field.required = false;
|
||||
field.removeAttribute("required");
|
||||
field.removeAttribute("autocomplete");
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
retireCredentialFields();
|
||||
document.querySelectorAll(".sirius-wallet").forEach(enhance);
|
||||
enhanceDerive();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ final class SPA_Login {
|
|||
// page at all.
|
||||
remove_filter( 'authenticate', 'wp_authenticate_username_password', 20 );
|
||||
remove_filter( 'authenticate', 'wp_authenticate_email_password', 20 );
|
||||
add_action( 'login_head', array( __CLASS__, 'hide_password_fields' ) );
|
||||
add_action( 'login_head', array( __CLASS__, 'hide_credential_fields' ) );
|
||||
add_filter( 'wp_login_errors', array( __CLASS__, 'quiet_empty_form_error' ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,11 +172,14 @@ final class SPA_Login {
|
|||
'<script>window.SIRIUS_PRESS = %s;</script>',
|
||||
wp_json_encode(
|
||||
array(
|
||||
'prefix' => SP_Settings::prefix(),
|
||||
'path' => SP_Settings::derivation_path(),
|
||||
'network' => SP_Settings::network(),
|
||||
'site' => home_url( '/' ),
|
||||
'strings' => array(
|
||||
'prefix' => SP_Settings::prefix(),
|
||||
'path' => SP_Settings::derivation_path(),
|
||||
'network' => SP_Settings::network(),
|
||||
'site' => home_url( '/' ),
|
||||
// So the page can stop core's username and password inputs
|
||||
// from blocking a submit they are no longer part of.
|
||||
'passwords' => self::passwords_allowed(),
|
||||
'strings' => array(
|
||||
'signing' => __( 'Signing…', 'sirius-press' ),
|
||||
'noWallet' => __( 'No wallet found in this browser.', 'sirius-press' ),
|
||||
'generated' => __( 'Write these words down. They are the only way back into this account — nobody, including this site, can reset them for you.', 'sirius-press' ),
|
||||
|
|
@ -284,6 +288,22 @@ final class SPA_Login {
|
|||
<p class="sirius-wallet__hint">
|
||||
<?php esc_html_e( 'Your wallet shows this next to the signature. It is checked against the signature, so a mistyped address is caught rather than quietly creating the wrong account.', 'sirius-press' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
/*
|
||||
* This path's own submit button.
|
||||
*
|
||||
* It used to borrow core's, which is wrong twice over: on
|
||||
* wp-login.php that button is hidden once passwords are off,
|
||||
* leaving a pasted signature with no way to be sent; and on
|
||||
* every other screen this block appears on there is no core
|
||||
* login button to borrow. Plain markup, so it still works with
|
||||
* JavaScript switched off — which is the whole reason this
|
||||
* path exists.
|
||||
*/
|
||||
?>
|
||||
<button type="submit" class="button button-primary sirius-wallet__submit">
|
||||
<?php esc_html_e( 'Use this signature', 'sirius-press' ); ?>
|
||||
</button>
|
||||
</details>
|
||||
|
||||
<p class="sirius-wallet__status" role="status" aria-live="polite"></p>
|
||||
|
|
@ -302,15 +322,62 @@ final class SPA_Login {
|
|||
}
|
||||
|
||||
/**
|
||||
* Visually retire the password field when passwords are off.
|
||||
* Stop the login screen greeting every visitor with a failed sign-in.
|
||||
*
|
||||
* The submit button stays. It is what the paste-a-signature path — the one
|
||||
* for people whose keys are on another machine — uses to send the form,
|
||||
* and hiding it would leave that path with no way to submit at all.
|
||||
* `wp-login.php` calls `wp_signon()` on its way to drawing the form, even
|
||||
* for a plain GET with nothing submitted. Core tolerates that by throwing
|
||||
* the result away — but only when the error codes are exactly the pair its
|
||||
* own two password filters produce for an empty form. Removing those
|
||||
* filters, which is the entire point of turning passwords off, leaves the
|
||||
* generic `authentication_failed` instead. Core does not recognise it, so
|
||||
* it prints it: a red "Invalid username, email address or incorrect
|
||||
* password" above a form that has neither field, on a site that has no
|
||||
* passwords.
|
||||
*
|
||||
* @param WP_Error $errors
|
||||
* @return WP_Error
|
||||
*/
|
||||
public static function hide_password_fields() {
|
||||
public static function quiet_empty_form_error( $errors ) {
|
||||
// Nothing was submitted, so there is nothing to forge: this only reads
|
||||
// whether the request carried a body at all.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
if ( ! empty( $_POST ) ) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if ( ! is_wp_error( $errors ) || array( 'authentication_failed' ) !== $errors->get_error_codes() ) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return new WP_Error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visually retire the username and password fields when passwords are off.
|
||||
*
|
||||
* Both of them, not just the password. With passwords off, `authenticate()`
|
||||
* never reads the username either — the account is found from the address
|
||||
* recovered out of the signature — so leaving "Username or Email Address"
|
||||
* at the top of the form puts the exact thing this fork exists to remove
|
||||
* in the first position a visitor looks.
|
||||
*
|
||||
* Core's own submit button goes too. The paste-a-signature path — the one
|
||||
* for people whose keys are on another machine — carries its own, so it no
|
||||
* longer depends on a control that belongs to the password form it is
|
||||
* replacing. The `required` attributes those two inputs carry are cleared
|
||||
* in login.js,
|
||||
* because a hidden required field does not fail validation quietly: it
|
||||
* refuses the submit and points an error bubble at a control nobody can
|
||||
* see.
|
||||
*/
|
||||
public static function hide_credential_fields() {
|
||||
echo '<style>'
|
||||
. '#user_pass, label[for="user_pass"], .user-pass-wrap, .forgetmenot + p { display:none !important; }'
|
||||
. '#user_login, label[for="user_login"],'
|
||||
. '#user_pass, label[for="user_pass"], .user-pass-wrap,'
|
||||
. '.forgetmenot + p { display:none !important; }'
|
||||
// Core wraps each field in a bare <p>, so hiding the input alone
|
||||
// leaves its margins behind as a gap above the wallet block.
|
||||
. 'form p:has( > #user_login ), form p:has( .user-pass-wrap ) { display:none !important; }'
|
||||
. '</style>';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'SIRIUS_PRESS_AUTH_VERSION', '0.1.0' );
|
||||
define( 'SIRIUS_PRESS_AUTH_VERSION', '0.1.1' );
|
||||
define( 'SIRIUS_PRESS_AUTH_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'SIRIUS_PRESS_AUTH_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue