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__ ) );