From f5759e0265366e3a21c9b6286cf771f5ffa948fb Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Fri, 7 Aug 2026 13:04:54 -0400 Subject: [PATCH] Change-password form: surface validation (strength, mismatch, inline, client-side) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The profile Security form flagged fields with a red icon but no explanation, and the password strength meter never appeared. Three real gaps: - Field errors weren't localized. A validator can set its message to a semantic key (Tiger_Validate_Password → password.too_short); _formErrors shipped that raw key to the client. Now _formErrors translates each field message the same way messages[] already are (non-key prose passes through). Adds the password.* policy strings + core.form.password_mismatch to core lang. - The confirm field returned Zend's default "The two given tokens do not match". Give the Identical validator a custom message key → "Passwords do not match." - The strength meter (tiger.password-strength.js) was only loaded by the auth layout, so data-tiger-strength did nothing on the account-layout profile page. Load it in the Security partial. The view now renders each field's (localized) message inline in an .invalid-feedback, clears it on input, and does a client-side new/confirm match check before the round trip. Verified on dev-com: weak → "Password is too short…", mismatch → "Passwords do not match." Co-Authored-By: Claude Opus 4.8 (1M context) --- core/languages/en/core.php | 8 ++ library/Tiger/Service/Service.php | 16 ++++ modules/profile/forms/Password.php | 7 +- .../views/scripts/index/_security.phtml | 74 +++++++++++++------ 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/core/languages/en/core.php b/core/languages/en/core.php index 2daaf36..e1e1078 100644 --- a/core/languages/en/core.php +++ b/core/languages/en/core.php @@ -35,6 +35,14 @@ 'core.auth.twofa.bad_code' => 'That code is incorrect or has expired.', 'core.auth.twofa.unavailable' => 'Two-factor authentication is not available on this install.', + // --- Form validation (field-level; localized by Tiger_Service_Service::_formErrors) --- + 'core.form.password_mismatch' => 'Passwords do not match.', + + // --- Password policy (Tiger_Policy_Password violation keys) --- + 'password.too_short' => 'Password is too short — please use at least 8 characters.', + 'password.needs_complexity' => 'Add upper- and lower-case letters, a number, and a symbol.', + 'password.reused' => "You've used this password before — please choose a new one.", + // --- Error pages --- 'core.error.403.title' => "You don't have access to that.", 'core.error.404.title' => "That page doesn't exist.", diff --git a/library/Tiger/Service/Service.php b/library/Tiger/Service/Service.php index 6b0bb03..6c38141 100644 --- a/library/Tiger/Service/Service.php +++ b/library/Tiger/Service/Service.php @@ -181,6 +181,22 @@ protected function _formErrors(Zend_Form $form) return; } + // Localize each field's validator messages before they go to the client. A validator may set its + // message to a semantic KEY (e.g. 'password.too_short', 'core.form.password_mismatch') expecting + // translation — the same courtesy messages[] already get via Tiger_Model_MessageObject. Non-key + // prose (a stock Zend message) isn't translatable, so it passes through unchanged. + $translate = Zend_Registry::isRegistered('Zend_Translate') ? Zend_Registry::get('Zend_Translate') : null; + if ($translate) { + foreach ($errors as $field => $messages) { + if (!is_array($messages)) { continue; } + foreach ($messages as $key => $text) { + if (is_string($text) && $translate->isTranslated($text)) { + $errors[$field][$key] = $translate->translate($text); + } + } + } + } + $this->_response->form = $errors; $this->_response->messages[] = new Tiger_Model_MessageObject('core.api.error.form', 'error'); } diff --git a/modules/profile/forms/Password.php b/modules/profile/forms/Password.php index 77268da..85a2192 100644 --- a/modules/profile/forms/Password.php +++ b/modules/profile/forms/Password.php @@ -41,7 +41,12 @@ protected function elements(): array ]], ['password', 'confirm_password', [ 'required' => true, - 'validators' => [['Identical', false, ['token' => 'new_password']]], + // A clear, localized "Passwords do not match." instead of Zend's default + // "The two given tokens do not match" (the key is translated in _formErrors). + 'validators' => [['Identical', false, [ + 'token' => 'new_password', + 'messages' => ['notSame' => 'core.form.password_mismatch'], + ]]], 'attribs' => ['class' => 'form-control', 'autocomplete' => 'new-password'], ]], ]; diff --git a/modules/profile/views/scripts/index/_security.phtml b/modules/profile/views/scripts/index/_security.phtml index ad85067..4f4be42 100644 --- a/modules/profile/views/scripts/index/_security.phtml +++ b/modules/profile/views/scripts/index/_security.phtml @@ -2,12 +2,17 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. /** - * Security tab — change your own password (verify current → set new). Two-factor auth has its own - * home at /auth/security; this tab links to it. Rendered as a partial: the parent passes the model - * (incl. `passwordForm` for its CSRF token). Saves via /api (Profile_Service_Security). + * Security tab — change your own password. Rendered as a partial: the parent passes the model + * (incl. `passwordForm` for its CSRF token). The new-password field carries a live strength meter + * (tiger.password-strength.js, loaded here since this partial lives in the account layout, not auth); + * the two fields are matched client-side before the round trip, and server field errors render inline. + * Saves via /api (Profile_Service_Security). */ -$t = static fn($k) => Zend_Registry::get('Zend_Translate')->translate($k); +$t = static fn($k) => Zend_Registry::get('Zend_Translate')->translate($k); +$ta = isset($this->themeAssets) && $this->themeAssets ? $this->themeAssets : '/_theme'; ?> + +
passwordForm->getElement('_csrf') ?> @@ -15,10 +20,12 @@ $t = static fn($k) => Zend_Registry::get('Zend_Translate')->translate($k);
+
+
@@ -31,24 +38,47 @@ $t = static fn($k) => Zend_Registry::get('Zend_Translate')->translate($k);