diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3dcf8..c62cef9 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/plugins/sirius-press-auth/assets/login.css b/plugins/sirius-press-auth/assets/login.css index 2bd6d84..ed8141b 100644 --- a/plugins/sirius-press-auth/assets/login.css +++ b/plugins/sirius-press-auth/assets/login.css @@ -183,3 +183,8 @@ margin: 10px 0 4px; font-size: 13px; } + +/* The paste-a-signature path's own submit button. It lives inside a
+ 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; } diff --git a/plugins/sirius-press-auth/assets/login.js b/plugins/sirius-press-auth/assets/login.js index 2069d20..36a9fd1 100644 --- a/plugins/sirius-press-auth/assets/login.js +++ b/plugins/sirius-press-auth/assets/login.js @@ -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(); } diff --git a/plugins/sirius-press-auth/includes/class-spa-login.php b/plugins/sirius-press-auth/includes/class-spa-login.php index 86e4947..f1c086e 100644 --- a/plugins/sirius-press-auth/includes/class-spa-login.php +++ b/plugins/sirius-press-auth/includes/class-spa-login.php @@ -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 { '', 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 {

+ +

@@ -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 ''; } } diff --git a/plugins/sirius-press-auth/sirius-press-auth.php b/plugins/sirius-press-auth/sirius-press-auth.php index e31e27e..385f928 100644 --- a/plugins/sirius-press-auth/sirius-press-auth.php +++ b/plugins/sirius-press-auth/sirius-press-auth.php @@ -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__ ) );