From f9d55a23b96015b45f21dbba4a47af5e755d3669 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Fri, 24 Jul 2026 15:38:44 +0200 Subject: [PATCH 01/13] refactor: adjust text domain to yard warden --- src/Bootstrap.php | 2 +- src/LimitLogin/LimitLoginAdminPage.php | 14 ++-- src/LimitLogin/LimitLoginServiceProvider.php | 2 +- src/Login/LoginServiceProvider.php | 2 +- src/Login/LostPasswordServiceProvider.php | 2 +- src/Onboarding/OnboardingServiceProvider.php | 12 ++-- src/Password/PasswordUIServiceProvider.php | 72 ++++++++++---------- src/Password/StrengthValidator.php | 4 +- yard-warden.php | 3 +- 9 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 5d34bbd..5238ae5 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -39,7 +39,7 @@ private static function registerPluginTextDomain(): void public static function loadPluginTextDomain(): void { \load_plugin_textdomain( - YARD_WARDEN_TEXT_DOMAIN, + 'yard-warden', false, YARD_WARDEN_PLUGIN_NAME . '/languages/' ); diff --git a/src/LimitLogin/LimitLoginAdminPage.php b/src/LimitLogin/LimitLoginAdminPage.php index c40b5d7..0ac73e3 100644 --- a/src/LimitLogin/LimitLoginAdminPage.php +++ b/src/LimitLogin/LimitLoginAdminPage.php @@ -32,8 +32,8 @@ public function register(): void public function registerPage(): void { add_options_page( - __('Yard Warden', YARD_WARDEN_TEXT_DOMAIN), - __('Yard Warden', YARD_WARDEN_TEXT_DOMAIN), + __('Yard Warden', 'yard-warden'), + __('Yard Warden', 'yard-warden'), 'manage_options', self::MENU_SLUG, [$this, 'renderPage'] @@ -62,7 +62,7 @@ public function handleClearRequest(): void add_settings_error( self::MENU_SLUG, 'limits_cleared', - __('All login limits have been cleared.', YARD_WARDEN_TEXT_DOMAIN), + __('All login limits have been cleared.', 'yard-warden'), 'success' ); @@ -84,16 +84,16 @@ public function renderPage(): void ?>
-

+

-

-

+

+

remove('confirm'); $errors->add( 'confirm', - __('If an account with this username or email address exists, you will receive an email with a link to reset your password.', YARD_WARDEN_TEXT_DOMAIN), + __('If an account with this username or email address exists, you will receive an email with a link to reset your password.', 'yard-warden'), 'message' ); diff --git a/src/Onboarding/OnboardingServiceProvider.php b/src/Onboarding/OnboardingServiceProvider.php index a8b54a1..1d415f3 100644 --- a/src/Onboarding/OnboardingServiceProvider.php +++ b/src/Onboarding/OnboardingServiceProvider.php @@ -93,7 +93,7 @@ public function filterWelcomeSubject($subject): string $custom = sprintf( /* translators: %s: site name */ - __('Welcome to %s!', YARD_WARDEN_TEXT_DOMAIN), + __('Welcome to %s!', 'yard-warden'), $siteName ); @@ -175,18 +175,18 @@ private function composeBody(string $userLogin, string $resetUrl): string $lines = [ sprintf( /* translators: %s: user login */ - __('Welcome %s,', YARD_WARDEN_TEXT_DOMAIN), + __('Welcome %s,', 'yard-warden'), $userLogin ), '', - __('Your account has been activated.', YARD_WARDEN_TEXT_DOMAIN), + __('Your account has been activated.', 'yard-warden'), '', - __('Use the link below to set a password and log in:', YARD_WARDEN_TEXT_DOMAIN), + __('Use the link below to set a password and log in:', 'yard-warden'), $resetUrl, '', - __('For security reasons, this link is valid for a limited time only.', YARD_WARDEN_TEXT_DOMAIN), + __('For security reasons, this link is valid for a limited time only.', 'yard-warden'), '', - __('Thanks!', YARD_WARDEN_TEXT_DOMAIN), + __('Thanks!', 'yard-warden'), ]; return implode("\n", $lines); diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index 3ebe7e5..63ef6bb 100644 --- a/src/Password/PasswordUIServiceProvider.php +++ b/src/Password/PasswordUIServiceProvider.php @@ -57,23 +57,23 @@ public function enqueue(): void 'minScore' => $minScore, 'strings' => [ 'hint' => sprintf( - __('Use at least %1$d characters and a strength of %2$d/%3$d.', YARD_WARDEN_TEXT_DOMAIN), + __('Use at least %1$d characters and a strength of %2$d/%3$d.', 'yard-warden'), $minLength, $minScore, 4 ), - 'tooShort' => __('Too short', YARD_WARDEN_TEXT_DOMAIN), + 'tooShort' => __('Too short', 'yard-warden'), 'requiredScore' => sprintf( - __('Minimum strength: %1$d/%2$d.', YARD_WARDEN_TEXT_DOMAIN), + __('Minimum strength: %1$d/%2$d.', 'yard-warden'), $minScore, 4 ), 'scoreLabels' => [ - __('Very weak', YARD_WARDEN_TEXT_DOMAIN), - __('Weak', YARD_WARDEN_TEXT_DOMAIN), - __('Fair', YARD_WARDEN_TEXT_DOMAIN), - __('Strong', YARD_WARDEN_TEXT_DOMAIN), - __('Very strong', YARD_WARDEN_TEXT_DOMAIN), + __('Very weak', 'yard-warden'), + __('Weak', 'yard-warden'), + __('Fair', 'yard-warden'), + __('Strong', 'yard-warden'), + __('Very strong', 'yard-warden'), ], ], 'zxcvbn' => $this->zxcvbnTranslations(), @@ -91,63 +91,63 @@ private function zxcvbnTranslations(): array return [ 'warnings' => [ 'Straight rows of keys are easy to guess' => - __('Straight rows of keys are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Straight rows of keys are easy to guess', 'yard-warden'), 'Short keyboard patterns are easy to guess' => - __('Short keyboard patterns are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Short keyboard patterns are easy to guess', 'yard-warden'), 'Use a longer keyboard pattern with more turns' => - __('Use a longer keyboard pattern with more turns', YARD_WARDEN_TEXT_DOMAIN), + __('Use a longer keyboard pattern with more turns', 'yard-warden'), 'Repeats like "aaa" are easy to guess' => - __('Repeats like "aaa" are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Repeats like "aaa" are easy to guess', 'yard-warden'), 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"' => - __('Repeats like "abcabcabc" are only slightly harder to guess than "abc"', YARD_WARDEN_TEXT_DOMAIN), + __('Repeats like "abcabcabc" are only slightly harder to guess than "abc"', 'yard-warden'), 'Sequences like abc or 6543 are easy to guess' => - __('Sequences like abc or 6543 are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Sequences like abc or 6543 are easy to guess', 'yard-warden'), 'Recent years are easy to guess' => - __('Recent years are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Recent years are easy to guess', 'yard-warden'), 'Dates are often easy to guess' => - __('Dates are often easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Dates are often easy to guess', 'yard-warden'), 'This is a top-10 common password' => - __('This is a top-10 common password', YARD_WARDEN_TEXT_DOMAIN), + __('This is a top-10 common password', 'yard-warden'), 'This is a top-100 common password' => - __('This is a top-100 common password', YARD_WARDEN_TEXT_DOMAIN), + __('This is a top-100 common password', 'yard-warden'), 'This is a very common password' => - __('This is a very common password', YARD_WARDEN_TEXT_DOMAIN), + __('This is a very common password', 'yard-warden'), 'This is similar to a commonly used password' => - __('This is similar to a commonly used password', YARD_WARDEN_TEXT_DOMAIN), + __('This is similar to a commonly used password', 'yard-warden'), 'A word by itself is easy to guess' => - __('A word by itself is easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('A word by itself is easy to guess', 'yard-warden'), 'Names and surnames by themselves are easy to guess' => - __('Names and surnames by themselves are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Names and surnames by themselves are easy to guess', 'yard-warden'), 'Common names and surnames are easy to guess' => - __('Common names and surnames are easy to guess', YARD_WARDEN_TEXT_DOMAIN), + __('Common names and surnames are easy to guess', 'yard-warden'), ], 'suggestions' => [ 'Use a few words, avoid common phrases' => - __('Use a few words, avoid common phrases', YARD_WARDEN_TEXT_DOMAIN), + __('Use a few words, avoid common phrases', 'yard-warden'), 'No need for symbols, digits, or uppercase letters' => - __('No need for symbols, digits, or uppercase letters', YARD_WARDEN_TEXT_DOMAIN), + __('No need for symbols, digits, or uppercase letters', 'yard-warden'), 'Add another word or two. Uncommon words are better.' => - __('Add another word or two. Uncommon words are better.', YARD_WARDEN_TEXT_DOMAIN), + __('Add another word or two. Uncommon words are better.', 'yard-warden'), 'Use a longer keyboard pattern with more turns' => - __('Use a longer keyboard pattern with more turns', YARD_WARDEN_TEXT_DOMAIN), + __('Use a longer keyboard pattern with more turns', 'yard-warden'), 'Avoid repeated words and characters' => - __('Avoid repeated words and characters', YARD_WARDEN_TEXT_DOMAIN), + __('Avoid repeated words and characters', 'yard-warden'), 'Avoid sequences' => - __('Avoid sequences', YARD_WARDEN_TEXT_DOMAIN), + __('Avoid sequences', 'yard-warden'), 'Avoid recent years' => - __('Avoid recent years', YARD_WARDEN_TEXT_DOMAIN), + __('Avoid recent years', 'yard-warden'), 'Avoid years that are associated with you' => - __('Avoid years that are associated with you', YARD_WARDEN_TEXT_DOMAIN), + __('Avoid years that are associated with you', 'yard-warden'), 'Avoid dates and years that are associated with you' => - __('Avoid dates and years that are associated with you', YARD_WARDEN_TEXT_DOMAIN), + __('Avoid dates and years that are associated with you', 'yard-warden'), "Capitalization doesn't help very much" => - __("Capitalization doesn't help very much", YARD_WARDEN_TEXT_DOMAIN), + __("Capitalization doesn't help very much", 'yard-warden'), 'All-uppercase is almost as easy to guess as all-lowercase' => - __('All-uppercase is almost as easy to guess as all-lowercase', YARD_WARDEN_TEXT_DOMAIN), + __('All-uppercase is almost as easy to guess as all-lowercase', 'yard-warden'), "Reversed words aren't much harder to guess" => - __("Reversed words aren't much harder to guess", YARD_WARDEN_TEXT_DOMAIN), + __("Reversed words aren't much harder to guess", 'yard-warden'), "Predictable substitutions like '@' instead of 'a' don't help very much" => - __("Predictable substitutions like '@' instead of 'a' don't help very much", YARD_WARDEN_TEXT_DOMAIN), + __("Predictable substitutions like '@' instead of 'a' don't help very much", 'yard-warden'), ], ]; } diff --git a/src/Password/StrengthValidator.php b/src/Password/StrengthValidator.php index 3a539c8..c5da169 100644 --- a/src/Password/StrengthValidator.php +++ b/src/Password/StrengthValidator.php @@ -36,7 +36,7 @@ public function validate(string $password, ?WP_User $user = null): ?WP_Error 'weak_password_length', sprintf( /* translators: %d: minimum password length */ - __('Password must be at least %d characters.', YARD_WARDEN_TEXT_DOMAIN), + __('Password must be at least %d characters.', 'yard-warden'), $minLength ) ); @@ -46,7 +46,7 @@ public function validate(string $password, ?WP_User $user = null): ?WP_Error $result = $this->zxcvbn->passwordStrength($password, $this->userInputs($user)); if ((int) ($result['score'] ?? 0) < $minScore) { - $feedback = $result['feedback']['warning'] ?? __('This password is too easy to guess.', YARD_WARDEN_TEXT_DOMAIN); + $feedback = $result['feedback']['warning'] ?? __('This password is too easy to guess.', 'yard-warden'); return new WP_Error('weak_password_score', (string) $feedback); } diff --git a/yard-warden.php b/yard-warden.php index 54973cc..d392909 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -13,7 +13,7 @@ * Author: Yard | Digital Agency * Author URI: https://www.yard.nl * License: GPLv2 or later - * Text Domain: yard_warden + * Text Domain: yard-warden * Domain Path: /languages * Requires at least: 6.0 * Requires PHP: 7.4 @@ -34,7 +34,6 @@ define('YARD_WARDEN_PLUGIN_FILE', __FILE__); define('YARD_WARDEN_PLUGIN_URL', plugins_url('/', YARD_WARDEN_PLUGIN_FILE)); define('YARD_WARDEN_PLUGIN_DIR_PATH', plugin_dir_path(YARD_WARDEN_PLUGIN_FILE)); -define('YARD_WARDEN_TEXT_DOMAIN', 'yard_warden'); require_once __DIR__ . '/src/Bootstrap.php'; From b4dc3842fa97384ed0ba5e4792447a13836a736e Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Fri, 24 Jul 2026 15:59:16 +0200 Subject: [PATCH 02/13] refactor: plugin check comments --- resources/js/password-ui.js | 2 +- src/Password/PasswordServiceProvider.php | 2 +- src/Password/PasswordUIServiceProvider.php | 7 +++++-- src/Password/StrengthValidator.php | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/js/password-ui.js b/resources/js/password-ui.js index 0970cce..9927752 100644 --- a/resources/js/password-ui.js +++ b/resources/js/password-ui.js @@ -72,7 +72,7 @@ } const label = cfg.strings.scoreLabels[result.score] || ''; - const parts = [`${label} (${result.score}/4)`]; + const parts = [`${label} (${result.score}/${cfg.maxScore})`]; if (result.score < cfg.minScore) { parts.push(cfg.strings.requiredScore); diff --git a/src/Password/PasswordServiceProvider.php b/src/Password/PasswordServiceProvider.php index 0848f30..9bc3e46 100644 --- a/src/Password/PasswordServiceProvider.php +++ b/src/Password/PasswordServiceProvider.php @@ -36,7 +36,7 @@ public function handlePasswordReset(WP_Error $errors, $user): void private function maybeAddValidationError(WP_Error $errors, $user): void { - $password = (string) ($_POST['pass1'] ?? ''); + $password = (string) (wp_unslash($_POST['pass1']) ?? ''); if ('' === $password) { return; diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index 63ef6bb..8c62c43 100644 --- a/src/Password/PasswordUIServiceProvider.php +++ b/src/Password/PasswordUIServiceProvider.php @@ -55,18 +55,21 @@ public function enqueue(): void wp_localize_script(self::SCRIPT_HANDLE, 'YardWarden', [ 'minLength' => $minLength, 'minScore' => $minScore, + 'maxScore' => StrengthValidator::MAX_SCORE, 'strings' => [ + /* translators: 1: minimum password length, 2: minimum password strength score, 3: maximum password strength score */ 'hint' => sprintf( __('Use at least %1$d characters and a strength of %2$d/%3$d.', 'yard-warden'), $minLength, $minScore, - 4 + StrengthValidator::MAX_SCORE ), 'tooShort' => __('Too short', 'yard-warden'), + /* translators: 1: minimum password strength score, 2: maximum password strength score */ 'requiredScore' => sprintf( __('Minimum strength: %1$d/%2$d.', 'yard-warden'), $minScore, - 4 + StrengthValidator::MAX_SCORE ), 'scoreLabels' => [ __('Very weak', 'yard-warden'), diff --git a/src/Password/StrengthValidator.php b/src/Password/StrengthValidator.php index c5da169..c61c49d 100644 --- a/src/Password/StrengthValidator.php +++ b/src/Password/StrengthValidator.php @@ -15,6 +15,7 @@ class StrengthValidator { public const DEFAULT_MIN_LENGTH = 16; public const DEFAULT_MIN_SCORE = 4; + public const MAX_SCORE = 4; protected Zxcvbn $zxcvbn; From cb7fa63adc9f896f7051df4d6d8830dbcf93fcb8 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Fri, 24 Jul 2026 16:52:44 +0200 Subject: [PATCH 03/13] chore: further plugin check comments and prepping for review --- .distignore | 13 ++++ README.md | 9 ++- readme.txt | 74 ++++++++++++++++++++ src/Bootstrap.php | 15 ---- src/LimitLogin/LimitLoginAdminPage.php | 9 +++ src/LimitLogin/LimitLoginServiceProvider.php | 9 ++- src/LimitLogin/Limiter.php | 7 ++ src/Login/LoginServiceProvider.php | 7 ++ src/Login/LostPasswordServiceProvider.php | 9 +++ src/Onboarding/OnboardingServiceProvider.php | 9 +++ src/Password/PasswordServiceProvider.php | 15 +++- src/Password/PasswordUIServiceProvider.php | 7 ++ src/Password/StrengthValidator.php | 7 ++ yard-warden.php | 3 +- 14 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 .distignore create mode 100644 readme.txt diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..5eedf39 --- /dev/null +++ b/.distignore @@ -0,0 +1,13 @@ +.git +.github +.gitignore +.gitattributes +.distignore +.editorconfig +.markdownlint.yml +.php-cs-fixer.php +.php-cs-fixer.cache +.phpunit.result.cache +.DS_Store +wp-content +tests diff --git a/README.md b/README.md index f108de3..c1c7874 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# Yard Warden +# Yard | Warden + +Requires at least: 6.0 +Tested up to: 7.0 +Requires PHP: 7.4 +Stable tag: 1.0.2 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html Enhanced password and login security for WordPress. diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..33682dd --- /dev/null +++ b/readme.txt @@ -0,0 +1,74 @@ +=== Yard | Warden === +Contributors: yarddigitalagency +Tags: security, password, login, brute force, multisite +Requires at least: 6.0 +Tested up to: 7.0 +Requires PHP: 7.4 +Stable tag: 1.0.2 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Enhanced password and login security for WordPress. + +== Description == + +Yard Warden hardens core WordPress password and login flows: + +* **Password strength** via [zxcvbn-php](https://github.com/bjeavons/zxcvbn-php). Scores passwords 0-4 (default 4) based on real guessability, not arbitrary character-class rules. User inputs (login, email, display name) are fed to zxcvbn so passwords containing them score lower. +* **Minimum length** enforcement (default 16) on top of the zxcvbn score. +* **Generic login errors** to prevent username enumeration on the wp-login form. Always on, no toggle. Only applies to authentication failures; password-reset and profile-update validation errors remain verbose so users can correct their input. +* **Safer multisite onboarding.** Auto-activates new signups server-side so no activation link is emailed and the wp-activate.php landing page (which would print username + plaintext password) is never reached. The welcome email is rewritten to contain a one-time password-reset link instead of a generated password. +* **Login limiting** via transient-based counters across three dimensions (IP+Username, IP, Username). Locks out brute-force attempts. Admin can clear all counters via Settings > Yard Warden. + += Filters = + +* `yard::warden/password/min-length` (default `16`) - Minimum character count. +* `yard::warden/password/min-score` (default `4`) - Minimum zxcvbn score (0-4). +* `yard::warden/login/generic-error` (default `Invalid credentials.`) - Replacement login error message. +* `yard::warden/login/leaky-error-codes` (default `['invalid_username', 'invalid_email', 'incorrect_password']`) - WP_Error codes to rewrite. +* `yard::warden/onboarding/welcome-subject` (default `Welcome to !`) - Multisite welcome email subject. +* `yard::warden/onboarding/welcome-body` (default reset-link body) - Full welcome email body. Receives `$user`, `$resetUrl`, original body. +* `yard::warden/limit-login/enabled` (default `true`) - Set to `false` to disable login limiting entirely. +* `yard::warden/limit-login/client-ip` (default `REMOTE_ADDR`) - Override IP detection (e.g. for reverse proxies). +* `yard::warden/limit-login/error-message` (default `Too many failed login attempts...`) - Lockout error shown to the user. +* `yard::warden/limit-login/skip-error-codes` (default `['expired_session']`) - WP_Error codes that do not count as failed attempts. +* `yard::warden/limit-login/threshold/{dimension}` (default `5` / `50` / `3`) - Attempts before lockout per dimension (`ip_user` / `ip` / `username`). +* `yard::warden/limit-login/window/{dimension}` (default `300` / `3600` / `1500`) - Counting window in seconds per dimension. +* `yard::warden/limit-login/lockout/{dimension}` (default `300` / `3600` / `1500`) - Lockout duration in seconds per dimension. + += Policy override example = + +` +add_filter('yard::warden/password/min-score', function (int $score) { + return max($score, 4); +}); +` + +== Installation == + +1. Upload the plugin files to `/wp-content/plugins/yard-warden`, or install through the Plugins screen directly. +2. Activate the plugin through the "Plugins" screen in WordPress. +3. Configure login-limit thresholds via Settings > Yard Warden, or override any default via the filters above. + +== Frequently Asked Questions == + += Does this add password expiry or forced rotation? = + +No. Forced password rotation is not implemented; it pushes users toward weaker, predictable passwords rather than improving security. + += Can I disable login limiting? = + +Yes, via the `yard::warden/limit-login/enabled` filter. + +== Changelog == + += 1.0.2 = +* Text domain adjusted to yard-warden. + += 1.0.0 = +* Initial release. + +== Upgrade Notice == + += 1.0.2 = +Text domain change only, no action required. diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 5238ae5..8b91cf2 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -25,26 +25,11 @@ class Bootstrap { public static function bootstrap(): void { - self::registerPluginTextDomain(); self::registerPasswordProviders(); self::registerLoginProviders(); self::registerOnboardingProviders(); } - private static function registerPluginTextDomain(): void - { - \add_action('init', [self::class, 'loadPluginTextDomain']); - } - - public static function loadPluginTextDomain(): void - { - \load_plugin_textdomain( - 'yard-warden', - false, - YARD_WARDEN_PLUGIN_NAME . '/languages/' - ); - } - private static function registerPasswordProviders(): void { (new PasswordServiceProvider(new StrengthValidator()))->register(); diff --git a/src/LimitLogin/LimitLoginAdminPage.php b/src/LimitLogin/LimitLoginAdminPage.php index 0ac73e3..9719186 100644 --- a/src/LimitLogin/LimitLoginAdminPage.php +++ b/src/LimitLogin/LimitLoginAdminPage.php @@ -6,6 +6,13 @@ use Yard\Logging\Log; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Registers a native WordPress admin page under Settings > Yard Warden * with a button to clear all login-limit transients. @@ -78,6 +85,8 @@ public function renderPage(): void return; } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display + // flag set by our own redirect in handleClearRequest(), not a form submission. if (isset($_GET['settings-updated'])) { settings_errors(self::MENU_SLUG); } diff --git a/src/LimitLogin/LimitLoginServiceProvider.php b/src/LimitLogin/LimitLoginServiceProvider.php index 8415368..01196ca 100644 --- a/src/LimitLogin/LimitLoginServiceProvider.php +++ b/src/LimitLogin/LimitLoginServiceProvider.php @@ -8,6 +8,13 @@ use WP_User; use Yard\Logging\Log; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Wires the login attempt Limiter into WordPress's login lifecycle to enforce * brute-force protection across three dimensions (IP+Username, IP, Username). @@ -197,7 +204,7 @@ private function isEnabled(): bool private function clientIp(): string { $ip = isset($_SERVER['REMOTE_ADDR']) - ? filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, ['options' => ['default' => '']]) + ? filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP, ['options' => ['default' => '']]) : ''; return (string) apply_filters(self::FILTER_CLIENT_IP, $ip); diff --git a/src/LimitLogin/Limiter.php b/src/LimitLogin/Limiter.php index dfc6bbc..c32cb9e 100644 --- a/src/LimitLogin/Limiter.php +++ b/src/LimitLogin/Limiter.php @@ -4,6 +4,13 @@ namespace Yard\Warden\LimitLogin; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Tracks failed login attempts via WordPress transients and enforces * lockouts when configurable thresholds are exceeded. diff --git a/src/Login/LoginServiceProvider.php b/src/Login/LoginServiceProvider.php index f41f4a6..40db50b 100644 --- a/src/Login/LoginServiceProvider.php +++ b/src/Login/LoginServiceProvider.php @@ -6,6 +6,13 @@ use WP_Error; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Hardens login by replacing enumeration-friendly errors with a generic message. * diff --git a/src/Login/LostPasswordServiceProvider.php b/src/Login/LostPasswordServiceProvider.php index 9b9d22f..bb3068d 100644 --- a/src/Login/LostPasswordServiceProvider.php +++ b/src/Login/LostPasswordServiceProvider.php @@ -7,6 +7,13 @@ use WP_Error; use WP_User; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Prevents username enumeration via the lost-password form. * @@ -57,6 +64,8 @@ public function suppressUserNotFoundLeak(WP_Error $errors, $user_data): void */ public function ambiguousConfirmMessage(WP_Error $errors): WP_Error { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect + // flag from wp_safe_redirect() above, not a form submission; no nonce to check. if (! isset($_GET['checkemail']) || 'confirm' !== $_GET['checkemail']) { return $errors; } diff --git a/src/Onboarding/OnboardingServiceProvider.php b/src/Onboarding/OnboardingServiceProvider.php index 1d415f3..cb61972 100644 --- a/src/Onboarding/OnboardingServiceProvider.php +++ b/src/Onboarding/OnboardingServiceProvider.php @@ -8,6 +8,13 @@ use WP_User; use Yard\Logging\Log; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Removes plaintext passwords from the multisite user-onboarding flow. * @@ -150,6 +157,8 @@ public function filterWelcomeBody($welcome_email, $user_id, $password, $meta): s private function welcomeEmailDisabled(): bool { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only opt-out + // flag (e.g. a bulk-import query param), doesn't change any state itself. return isset($_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]) && '' !== $_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]; } diff --git a/src/Password/PasswordServiceProvider.php b/src/Password/PasswordServiceProvider.php index 9bc3e46..486c5b3 100644 --- a/src/Password/PasswordServiceProvider.php +++ b/src/Password/PasswordServiceProvider.php @@ -7,6 +7,13 @@ use WP_Error; use WP_User; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + class PasswordServiceProvider { private StrengthValidator $validator; @@ -36,7 +43,13 @@ public function handlePasswordReset(WP_Error $errors, $user): void private function maybeAddValidationError(WP_Error $errors, $user): void { - $password = (string) (wp_unslash($_POST['pass1']) ?? ''); + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- fires from + // user_profile_update_errors / validate_password_reset, both only invoked + // by WP core after its own nonce check on the profile/reset-password form. + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitizing + // would mutate the value (trimmed/stripped) so the strength check would run against a + // different string than the password WP actually sets. Never stored or output as-is. + $password = isset($_POST['pass1']) ? (string) wp_unslash($_POST['pass1']) : ''; if ('' === $password) { return; diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index 8c62c43..f84e392 100644 --- a/src/Password/PasswordUIServiceProvider.php +++ b/src/Password/PasswordUIServiceProvider.php @@ -4,6 +4,13 @@ namespace Yard\Warden\Password; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Replaces WordPress's default password hint, strength indicator and * "Confirm use of weak password" checkbox on login / profile screens with a diff --git a/src/Password/StrengthValidator.php b/src/Password/StrengthValidator.php index c61c49d..013efa0 100644 --- a/src/Password/StrengthValidator.php +++ b/src/Password/StrengthValidator.php @@ -8,6 +8,13 @@ use WP_User; use ZxcvbnPhp\Zxcvbn; +/** + * Exit when accessed directly. + */ +if (! defined('ABSPATH')) { + exit; +} + /** * Validates password strength using zxcvbn-php https://github.com/bjeavons/zxcvbn-php. */ diff --git a/yard-warden.php b/yard-warden.php index d392909..f226804 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -28,9 +28,10 @@ exit; } +require_once __DIR__ . '/vendor/autoload.php'; + define('YARD_WARDEN_VERSION', '1.0.2'); define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.0'); -define('YARD_WARDEN_PLUGIN_NAME', basename(__DIR__)); define('YARD_WARDEN_PLUGIN_FILE', __FILE__); define('YARD_WARDEN_PLUGIN_URL', plugins_url('/', YARD_WARDEN_PLUGIN_FILE)); define('YARD_WARDEN_PLUGIN_DIR_PATH', plugin_dir_path(YARD_WARDEN_PLUGIN_FILE)); From 3714d50123e637af329123a22593f7c6e8dbd22e Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Fri, 24 Jul 2026 17:21:32 +0200 Subject: [PATCH 04/13] fix: adjust translator comment and phpcs ignores --- src/LimitLogin/LimitLoginAdminPage.php | 3 +-- src/Login/LostPasswordServiceProvider.php | 3 +-- src/Onboarding/OnboardingServiceProvider.php | 3 +-- src/Password/PasswordServiceProvider.php | 7 +------ src/Password/PasswordUIServiceProvider.php | 4 ++-- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/LimitLogin/LimitLoginAdminPage.php b/src/LimitLogin/LimitLoginAdminPage.php index 9719186..b3c974e 100644 --- a/src/LimitLogin/LimitLoginAdminPage.php +++ b/src/LimitLogin/LimitLoginAdminPage.php @@ -85,8 +85,7 @@ public function renderPage(): void return; } - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display - // flag set by our own redirect in handleClearRequest(), not a form submission. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag set by our own redirect in handleClearRequest(), not a form submission. if (isset($_GET['settings-updated'])) { settings_errors(self::MENU_SLUG); } diff --git a/src/Login/LostPasswordServiceProvider.php b/src/Login/LostPasswordServiceProvider.php index bb3068d..99de821 100644 --- a/src/Login/LostPasswordServiceProvider.php +++ b/src/Login/LostPasswordServiceProvider.php @@ -64,8 +64,7 @@ public function suppressUserNotFoundLeak(WP_Error $errors, $user_data): void */ public function ambiguousConfirmMessage(WP_Error $errors): WP_Error { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect - // flag from wp_safe_redirect() above, not a form submission; no nonce to check. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flag from wp_safe_redirect() above, not a form submission; no nonce to check. if (! isset($_GET['checkemail']) || 'confirm' !== $_GET['checkemail']) { return $errors; } diff --git a/src/Onboarding/OnboardingServiceProvider.php b/src/Onboarding/OnboardingServiceProvider.php index cb61972..87e2c52 100644 --- a/src/Onboarding/OnboardingServiceProvider.php +++ b/src/Onboarding/OnboardingServiceProvider.php @@ -157,8 +157,7 @@ public function filterWelcomeBody($welcome_email, $user_id, $password, $meta): s private function welcomeEmailDisabled(): bool { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only opt-out - // flag (e.g. a bulk-import query param), doesn't change any state itself. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only opt-out flag (e.g. a bulk-import query param), doesn't change any state itself. return isset($_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]) && '' !== $_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]; } diff --git a/src/Password/PasswordServiceProvider.php b/src/Password/PasswordServiceProvider.php index 486c5b3..7146fb6 100644 --- a/src/Password/PasswordServiceProvider.php +++ b/src/Password/PasswordServiceProvider.php @@ -43,12 +43,7 @@ public function handlePasswordReset(WP_Error $errors, $user): void private function maybeAddValidationError(WP_Error $errors, $user): void { - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- fires from - // user_profile_update_errors / validate_password_reset, both only invoked - // by WP core after its own nonce check on the profile/reset-password form. - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitizing - // would mutate the value (trimmed/stripped) so the strength check would run against a - // different string than the password WP actually sets. Never stored or output as-is. + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- fires from user_profile_update_errors/validate_password_reset, only invoked by WP core after its own nonce check; sanitizing would mutate the value so the strength check would run against a different string than the password WP actually sets, and it's never stored or output as-is. $password = isset($_POST['pass1']) ? (string) wp_unslash($_POST['pass1']) : ''; if ('' === $password) { diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index f84e392..1a318a0 100644 --- a/src/Password/PasswordUIServiceProvider.php +++ b/src/Password/PasswordUIServiceProvider.php @@ -64,16 +64,16 @@ public function enqueue(): void 'minScore' => $minScore, 'maxScore' => StrengthValidator::MAX_SCORE, 'strings' => [ - /* translators: 1: minimum password length, 2: minimum password strength score, 3: maximum password strength score */ 'hint' => sprintf( + /* translators: 1: minimum password length, 2: minimum password strength score, 3: maximum password strength score */ __('Use at least %1$d characters and a strength of %2$d/%3$d.', 'yard-warden'), $minLength, $minScore, StrengthValidator::MAX_SCORE ), 'tooShort' => __('Too short', 'yard-warden'), - /* translators: 1: minimum password strength score, 2: maximum password strength score */ 'requiredScore' => sprintf( + /* translators: 1: minimum password strength score, 2: maximum password strength score */ __('Minimum strength: %1$d/%2$d.', 'yard-warden'), $minScore, StrengthValidator::MAX_SCORE From c0da2f85906664f38f1e152ef4ccf45d1707b952 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Fri, 24 Jul 2026 17:39:47 +0200 Subject: [PATCH 05/13] fix: bump requires to 6.3 due to wp_is_password_reset_allowed_for_user() --- README.md | 2 +- readme.txt | 2 +- yard-warden.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c1c7874..127689c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Yard | Warden -Requires at least: 6.0 +Requires at least: 6.3 Tested up to: 7.0 Requires PHP: 7.4 Stable tag: 1.0.2 diff --git a/readme.txt b/readme.txt index 33682dd..9eac3cf 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === Yard | Warden === Contributors: yarddigitalagency Tags: security, password, login, brute force, multisite -Requires at least: 6.0 +Requires at least: 6.3 Tested up to: 7.0 Requires PHP: 7.4 Stable tag: 1.0.2 diff --git a/yard-warden.php b/yard-warden.php index f226804..44a9bac 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -15,7 +15,7 @@ * License: GPLv2 or later * Text Domain: yard-warden * Domain Path: /languages - * Requires at least: 6.0 + * Requires at least: 6.3 * Requires PHP: 7.4 */ @@ -31,7 +31,7 @@ require_once __DIR__ . '/vendor/autoload.php'; define('YARD_WARDEN_VERSION', '1.0.2'); -define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.0'); +define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.3'); define('YARD_WARDEN_PLUGIN_FILE', __FILE__); define('YARD_WARDEN_PLUGIN_URL', plugins_url('/', YARD_WARDEN_PLUGIN_FILE)); define('YARD_WARDEN_PLUGIN_DIR_PATH', plugin_dir_path(YARD_WARDEN_PLUGIN_FILE)); From 37e72d45b951cc58cb2e4e0e326d4d25269ff198 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 12:21:26 +0200 Subject: [PATCH 06/13] feat: add phpstan --- composer.json | 7 +- composer.lock | 342 +++++++++++++++++++++++++++++++++++++++++++++++++- phpstan.neon | 12 ++ 3 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 8341f93..1b6d186 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,9 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", - "yard/php-cs-fixer-rules": "^1.0" + "yard/php-cs-fixer-rules": "^1.0", + "szepeviktor/phpstan-wordpress": "^2.0", + "johnbillion/wp-compat": "^1.5" }, "autoload": { "psr-4": { @@ -34,7 +36,8 @@ "post-update-cmd": [ "composer dump-autoload" ], - "format": " ./vendor/bin/php-cs-fixer fix" + "format": " ./vendor/bin/php-cs-fixer fix", + "analyse": "vendor/bin/phpstan analyse --debug --memory-limit 1G" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 41f5428..415b31d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "98cc28d496ac6637d79b6a6b4d95c8bd", + "content-hash": "0ba1af869a5500d4cc188f07bebb7607", "packages": [ { "name": "bjeavons/zxcvbn-php", @@ -848,6 +848,200 @@ ], "time": "2026-07-15T09:51:47+00:00" }, + { + "name": "johnbillion/wp-compat", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/johnbillion/wp-compat.git", + "reference": "2ccebedbbbc6b6eec3fba4a568cff0a4ec05bf6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/johnbillion/wp-compat/zipball/2ccebedbbbc6b6eec3fba4a568cff0a4ec05bf6e", + "reference": "2ccebedbbbc6b6eec3fba4a568cff0a4ec05bf6e", + "shasum": "" + }, + "require": { + "php": ">= 7.4", + "phpstan/phpstan": "^2.0", + "wp-hooks/wordpress-core": "^1.12" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "johnbillion/plugin-infrastructure": "dev-trunk", + "nikic/php-parser": "^5.1", + "php-stubs/wordpress-stubs": "~7.0.0", + "phpstan/phpstan-deprecation-rules": "2.0.0", + "phpstan/phpstan-phpunit": "2.0.1", + "phpstan/phpstan-strict-rules": "2.0.0", + "phpunit/phpunit": "^9.0", + "roots/wordpress-core-installer": "1.100.0", + "roots/wordpress-full": "~7.0.0", + "wp-coding-standards/wpcs": "3.1.0" + }, + "suggest": { + "phpstan/phpstan-deprecation-rules": "PHPStan rules for detecting usage of deprecated symbols", + "swissspidy/phpstan-no-private": "PHPStan rules for detecting usage of pseudo-private functions, classes, and methods", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "wordpress-install-dir": "vendor/wordpress/wordpress" + }, + "autoload": { + "psr-4": { + "WPCompat\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Blackbourn", + "homepage": "https://johnblackbourn.com/" + } + ], + "description": "PHPStan extension to help verify that your PHP code is compatible with a given version of WordPress", + "keywords": [ + "PHPStan", + "wordpress" + ], + "support": { + "issues": "https://github.com/johnbillion/wp-compat/issues", + "source": "https://github.com/johnbillion/wp-compat" + }, + "funding": [ + { + "url": "https://github.com/sponsors/johnbillion", + "type": "github" + } + ], + "time": "2026-05-22T14:13:03+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.4", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "90a9412826b9944f93b10bf41d795b5fe68abcd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/90a9412826b9944f93b10bf41d795b5fe68abcd5", + "reference": "90a9412826b9944f93b10bf41d795b5fe68abcd5", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.6", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.4" + }, + "time": "2026-05-01T20:36:01+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.8", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e285254e60f33c21902efef4a926ca0987c06804", + "reference": "e285254e60f33c21902efef4a926ca0987c06804", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-08-04T22:21:45+00:00" + }, { "name": "psr/container", "version": "1.1.2", @@ -2938,6 +3132,69 @@ ], "time": "2024-11-10T20:33:58+00:00" }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" + }, + "time": "2025-09-14T02:58:22+00:00" + }, { "name": "webmozart/assert", "version": "1.12.1", @@ -2996,6 +3253,89 @@ }, "time": "2025-10-29T15:56:20+00:00" }, + { + "name": "wp-hooks/wordpress-core", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/wp-hooks/wordpress-core-hooks.git", + "reference": "0ba438bdd4c99b6613eb8459feb0a4f6d2d7082c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-hooks/wordpress-core-hooks/zipball/0ba438bdd4c99b6613eb8459feb0a4f6d2d7082c", + "reference": "0ba438bdd4c99b6613eb8459feb0a4f6d2d7082c", + "shasum": "" + }, + "replace": { + "johnbillion/wp-hooks": "*" + }, + "require-dev": { + "erusev/parsedown": "1.8.0-beta-7", + "oomphinc/composer-installers-extender": "^2", + "roots/wordpress-core-installer": "^1.0.0", + "roots/wordpress-full": "7.0", + "wp-hooks/generator": "1.0.0" + }, + "type": "library", + "extra": { + "wp-hooks": { + "ignore-files": [ + "wp-admin/includes/deprecated.php", + "wp-admin/includes/ms-deprecated.php", + "wp-content/", + "wp-includes/build/pages/", + "wp-includes/deprecated.php", + "wp-includes/ID3/", + "wp-includes/ms-deprecated.php", + "wp-includes/pomo/", + "wp-includes/random_compat/", + "wp-includes/Requests/", + "wp-includes/SimplePie/", + "wp-includes/sodium_compat/", + "wp-includes/Text/" + ], + "ignore-hooks": [ + "load-categories.php", + "load-edit-link-categories.php", + "load-edit-tags.php", + "load-page-new.php", + "load-page.php", + "option_enable_xmlrpc", + "edit_post_{$field}", + "pre_post_{$field}", + "post_{$field}", + "pre_option_enable_xmlrpc", + "$page_hook", + "$hook", + "$hook_name" + ] + }, + "wordpress-install-dir": "vendor/wordpress/wordpress" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "John Blackbourn", + "homepage": "https://johnblackbourn.com/" + } + ], + "description": "All the actions and filters from WordPress core in machine-readable JSON format.", + "support": { + "issues": "https://github.com/wp-hooks/wordpress-core-hooks/issues", + "source": "https://github.com/wp-hooks/wordpress-core-hooks/tree/1.12.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/johnbillion", + "type": "github" + } + ], + "time": "2026-05-22T10:28:41+00:00" + }, { "name": "yard/php-cs-fixer-rules", "version": "v1.0.5", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..aef3a8a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + - vendor/johnbillion/wp-compat/extension.neon + +parameters: + level: 8 + paths: + - src + scanFiles: + - yard-warden.php + WPCompat: + pluginFile: yard-warden.php From 5fb0e462aff511aec0ef792ee0e91f9f6ed52e39 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 12:21:36 +0200 Subject: [PATCH 07/13] fix: phpstan errors --- src/Login/LoginServiceProvider.php | 2 +- src/Password/PasswordServiceProvider.php | 6 +++--- src/Password/PasswordUIServiceProvider.php | 2 +- yard-warden.php | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Login/LoginServiceProvider.php b/src/Login/LoginServiceProvider.php index 40db50b..58d1acb 100644 --- a/src/Login/LoginServiceProvider.php +++ b/src/Login/LoginServiceProvider.php @@ -48,7 +48,7 @@ public function filterAuthenticateErrors($user, $username) return $user; } - if (! $this->isLeakyCode($user->get_error_code())) { + if (! $this->isLeakyCode((string) $user->get_error_code())) { return $user; } diff --git a/src/Password/PasswordServiceProvider.php b/src/Password/PasswordServiceProvider.php index 7146fb6..18da7d4 100644 --- a/src/Password/PasswordServiceProvider.php +++ b/src/Password/PasswordServiceProvider.php @@ -29,19 +29,19 @@ public function register(): void add_action('validate_password_reset', [$this, 'handlePasswordReset'], 10, 2); } - public function handleProfileUpdateErrors(WP_Error $errors, bool $update, $user): void + public function handleProfileUpdateErrors(WP_Error $errors, bool $update, object $user): void { unset($update); $this->maybeAddValidationError($errors, $user); } - public function handlePasswordReset(WP_Error $errors, $user): void + public function handlePasswordReset(WP_Error $errors, object $user): void { $this->maybeAddValidationError($errors, $user); } - private function maybeAddValidationError(WP_Error $errors, $user): void + private function maybeAddValidationError(WP_Error $errors, object $user): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- fires from user_profile_update_errors/validate_password_reset, only invoked by WP core after its own nonce check; sanitizing would mutate the value so the strength check would run against a different string than the password WP actually sets, and it's never stored or output as-is. $password = isset($_POST['pass1']) ? (string) wp_unslash($_POST['pass1']) : ''; diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index 1a318a0..feacba4 100644 --- a/src/Password/PasswordUIServiceProvider.php +++ b/src/Password/PasswordUIServiceProvider.php @@ -34,7 +34,7 @@ public function enqueue(): void wp_enqueue_script( self::SCRIPT_HANDLE, - YARD_WARDEN_PLUGIN_URL . 'resources/js/password-ui.js', + plugins_url('resources/js/password-ui.js', YARD_WARDEN_PLUGIN_FILE), ['zxcvbn-async', 'password-strength-meter'], YARD_WARDEN_VERSION, true diff --git a/yard-warden.php b/yard-warden.php index 44a9bac..ee39104 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -33,7 +33,6 @@ define('YARD_WARDEN_VERSION', '1.0.2'); define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.3'); define('YARD_WARDEN_PLUGIN_FILE', __FILE__); -define('YARD_WARDEN_PLUGIN_URL', plugins_url('/', YARD_WARDEN_PLUGIN_FILE)); define('YARD_WARDEN_PLUGIN_DIR_PATH', plugin_dir_path(YARD_WARDEN_PLUGIN_FILE)); require_once __DIR__ . '/src/Bootstrap.php'; From d775cbe1e194ca9392206a9e8d5c53fe745c5f74 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 12:21:49 +0200 Subject: [PATCH 08/13] chore: remove translation files per wordpress suggestion --- languages/yard_warden-nl_NL.mo | Bin 5706 -> 0 bytes languages/yard_warden-nl_NL.po | 233 -------------------------------- languages/yard_warden.pot | 234 --------------------------------- yard-warden.php | 1 - 4 files changed, 468 deletions(-) delete mode 100644 languages/yard_warden-nl_NL.mo delete mode 100644 languages/yard_warden-nl_NL.po delete mode 100644 languages/yard_warden.pot diff --git a/languages/yard_warden-nl_NL.mo b/languages/yard_warden-nl_NL.mo deleted file mode 100644 index 19d33852d2114cc536a5bff041442a4e094efcc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5706 zcmb7{ON?Yy8OIM#rPV~Si?dS5;yq!pL?sTryqmf)UE%0oX2u?N1>Xn12HtkNF^_`}fDeM_K#lJ~hBm(hPl9iN75GnZ3|{@5 zF?WN124(LDpyvM+JPaQBd_7+Tp5^|h;FrL^fZqf^03QZ#y2F?=;3ME;U;-Wje*r!U zz6;KQAAwA5?wK)$Da?bQ1!qC=au(FMeNgN95vcXQ4L$|_4P*#&=NIbr-v_q1|28Q8 zQg8wMHOSAr$Di7N2#SY~n&jyJ&>@MBPZ`VS~S-Fi>`{05lmQ#l` z{Q#$8Ogw2W*)=)ZTxcE8W*9OYm|&ng9FOyV3#Yh865+7?YfZ;ErDfT}^32Vg;(T&w zJ>pP@v?SgXEAsmjoQF7}Gw=?C<0$`Hrw*-KTZl0!j7M_;Y72bAy zRr#WAP9}*hT;**Nr;Ka+ZcQ(JX1j@}VHBFQM#G^mx~}w^)g@_OR@QBsUk_Z8w32*W zw>2l%@;I^)!}BJK>^dWsw(2`NV}mQpyc_d2vg5ew+x2|ZbSZq-XI0&P^U=`PO<|!P zuG@Eo>#_`%9%d^2_eVbK`pNzOFyARRj4PMK8?K5WD9U}A%~iJVlc8Pn#kx&L-M%>` zF08n^EUwN6uFR>L)Xb&L`pQw!+_1XqeRH}df#>;31-sY3J(Kkvc1LyyC&A64E6ICt zW=r246>+t0S8(Sw7Qa;B?6x`U;=-Ke*=B=+LG!FU1jBeKiL-&cwZ`hA(Am{aW|}Ha zF(gaY+vbH8%X8n0d8W0EO4QTGl|cka-W||BA5}IQc}cCxbDO#hb6lM8sy5Q0rgT^_ zH%_!&*T_eDxK3Ujk2>l}GwZtDe1w~W^MlQrTnt*4tBn@$?)S?qau+Q ztuj^JGK``nvTW?6mq$VQsMsWxd9#|0F2Y7&u}dKGLY&3vD79r(_^em;7wzofhokw~ zBM&3~i#wf51QKE?F}Nnb?{M|S+-4qT!Y|^xlZD3IqH;ET#ck#d3)gP+;`?|oRyV#xNODFxj6*Ai=3w- zjh;R$E^41V_QcU$th>_Bi^>lCdM9Uc^q|}aX*+#)r7E24 zv_(EH#UY*cXqQ9XGPV!O{UCwH&#|mw>+phY^6ey?cQm+Bi;#Bq8lWWgB;+FKbImMp zO599yM?KkOYMG^&y)NOj&GHSyxNVoDKLVZ%=z5Mcl#m?QFVwFTo#ra9on7(c+FOUL z3psdVc)y^3*|bU^3!iWIKppvMP6$<`&ZhOI_KCtR>JK}J+l*2#9pKDTZ zW?oiqndGp|jLC)+ufY{IEfx8y?^dlBq82lh#Ob2lGJsuZA8s$q5MR*Qx={AwsP){a zSGJb(Mcc2cVYxUzKOT?U>&(~Al9}^lgw}H5vJz+H#iHd&)*HE=Z!LS5F51(hs@tEr z@WO>N(+M@jsXSxPq3`<8FWSn#R?QC+oO(Ra3rQ!gE}v~ZJ#D8sR(#PqL*WpSBNpw` zow%A=y3`^>W38m64Q@YzaiIW1D~Kcl|m>x zbp!6>RZ_lZqmf%)%6-pfcv(dZBB{pS*B!X8a^JR>5M!qpkulYC`f?*)%~TjQbrE6; z8?Y)P9%eSY5Bo|eiZp&d&sU-*jmw@6LpaiZWjC}N-e#oH0kU2cVEN zUJcdKC}qFP?5cAGrAfkN6nmR8=qj6QkHA#j}Y^}8} zQ!Yp==2A*ks8)R+M`frY)W5Wm-)NFgnRo^-?SQBzC&{NhpRoT+G;E`x)bga42}_j3 z!EWx8d=T6k?jspdVHJX3p#qD!Y)BhQm_45zvI92HRd4$-=~Y@G0ZOf*up;NwWf0=6 zCztLf8B2N!R!{B7oJ$olbwSnfW#wE&&5~7XBpS`50qVEvuDdCfP06JKtZfuyS~MRF zBDd#zSf5R*EvNbgrw)`uwpCj@e4Y>Cr`zAqH_u-FzY^*!5z>uU$y{OYzAiXJ&kSnF z4cKL_ZA!qVIZsnnfdq(3t~xB(@>sO`wMi;@Q4W35QOU683-J*=K%6V4>&lB%24O0> zvrv3V{ZO<;{^8337uSB=A~^y>pFCSbchAK>uTX|&1kg-hmt}jAvs?RVh1T?UmM5@*RYkl?L(;rD?%Cmrv}d04ENoEiH>cxDF)TSZ^_!N`#0B3c zn^M`?asf4bX44IM{6_6Wcx4f~Ot?|QsVOoT2km5o8ch}IUCW3Y^eZ4{b8f(XzzgWjX1+Mn;osH2b$>$Ra-t0CFKUIBA0Lp`T(WGMy+~kZ9U|Wj6iLp8M6iQmMD|`s%>GmW=DGO~3 z6gF1ItRI9NMOpZY3=GrMmm6cM!dLW1CCJt^DGGJTX1}8-e37EK`K-l3?A0m<#bMk0 E7t;i$MF0Q* diff --git a/languages/yard_warden-nl_NL.po b/languages/yard_warden-nl_NL.po deleted file mode 100644 index 954b5d5..0000000 --- a/languages/yard_warden-nl_NL.po +++ /dev/null @@ -1,233 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: Yard Warden 0.1.0\n" -"Report-Msgid-Bugs-To: https://www.yard.nl\n" -"Last-Translator: \n" -"Language-Team: Dutch\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-04-15T00:00:00+00:00\n" -"PO-Revision-Date: 2026-04-15T00:00:00+00:00\n" -"Language: nl_NL\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Domain: yard_warden\n" - -#. Plugin Name of the plugin -#: yard-warden.php -msgid "Yard Warden" -msgstr "Yard Warden" - -#: src/Login/LoginServiceProvider.php:52 -msgid "Invalid credentials." -msgstr "Ongeldige inloggegevens." - -#. translators: %d: minimum password length -#: src/Password/StrengthValidator.php:46 -msgid "Password must be at least %d characters." -msgstr "Wachtwoord moet minimaal %d tekens lang zijn." - -#: src/Password/StrengthValidator.php:56 -msgid "This password is too easy to guess." -msgstr "Dit wachtwoord is te makkelijk te raden." - -#. translators: 1: minimum password length, 2: required password score, 3: maximum password score -#: src/Password/PasswordUIServiceProvider.php:50 -msgid "Use at least %1$d characters and a strength of %2$d/%3$d." -msgstr "Gebruik minimaal %1$d tekens en zorg voor een sterkte van %2$d/%3$d." - -#: src/Password/PasswordUIServiceProvider.php:53 -msgid "Too short" -msgstr "Te kort" - -#. translators: 1: required password score, 2: maximum password score -#: src/Password/PasswordUIServiceProvider.php:55 -msgid "Minimum strength: %1$d/%2$d." -msgstr "Minimale sterkte: %1$d/%2$d." - -#: src/Password/PasswordUIServiceProvider.php:60 -msgid "Very weak" -msgstr "Zeer zwak" - -#: src/Password/PasswordUIServiceProvider.php:61 -msgid "Weak" -msgstr "Zwak" - -#: src/Password/PasswordUIServiceProvider.php:62 -msgid "Fair" -msgstr "Redelijk" - -#: src/Password/PasswordUIServiceProvider.php:63 -msgid "Strong" -msgstr "Sterk" - -#: src/Password/PasswordUIServiceProvider.php:64 -msgid "Very strong" -msgstr "Zeer sterk" - -# zxcvbn warnings -#: src/Password/PasswordUIServiceProvider.php -msgid "Straight rows of keys are easy to guess" -msgstr "Opvolgende rijen toetsen zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Short keyboard patterns are easy to guess" -msgstr "Korte toetsenpatronen zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Use a longer keyboard pattern with more turns" -msgstr "Gebruik een langer toetsenpatroon met meer afwisseling" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Repeats like \"aaa\" are easy to guess" -msgstr "Herhalingen zoals \"aaa\" zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"" -msgstr "Herhalingen zoals \"abcabcabc\" zijn maar iets moeilijker te raden dan \"abc\"" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Sequences like abc or 6543 are easy to guess" -msgstr "Reeksen zoals abc of 6543 zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Recent years are easy to guess" -msgstr "Recente jaartallen zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Dates are often easy to guess" -msgstr "Datums zijn vaak makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a top-10 common password" -msgstr "Dit is een top-10 veelgebruikt wachtwoord" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a top-100 common password" -msgstr "Dit is een top-100 veelgebruikt wachtwoord" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a very common password" -msgstr "Dit is een zeer veelgebruikt wachtwoord" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is similar to a commonly used password" -msgstr "Dit lijkt op een veelgebruikt wachtwoord" - -#: src/Password/PasswordUIServiceProvider.php -msgid "A word by itself is easy to guess" -msgstr "Eén enkel woord is makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Names and surnames by themselves are easy to guess" -msgstr "Namen en achternamen op zichzelf zijn makkelijk te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Common names and surnames are easy to guess" -msgstr "Veelvoorkomende namen en achternamen zijn makkelijk te raden" - -# zxcvbn suggestions -#: src/Password/PasswordUIServiceProvider.php -msgid "Use a few words, avoid common phrases" -msgstr "Gebruik meerdere woorden, vermijd bekende zinnen" - -#: src/Password/PasswordUIServiceProvider.php -msgid "No need for symbols, digits, or uppercase letters" -msgstr "Symbolen, cijfers of hoofdletters zijn niet nodig" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Add another word or two. Uncommon words are better." -msgstr "Voeg nog een of twee woorden toe. Ongebruikelijke woorden zijn beter." - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid repeated words and characters" -msgstr "Vermijd herhaalde woorden en tekens" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid sequences" -msgstr "Vermijd reeksen" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid recent years" -msgstr "Vermijd recente jaartallen" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid years that are associated with you" -msgstr "Vermijd jaartallen die met jou geassocieerd worden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid dates and years that are associated with you" -msgstr "Vermijd datums en jaartallen die met jou geassocieerd worden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Capitalization doesn't help very much" -msgstr "Hoofdletters gebruiken helpt nauwelijks" - -#: src/Password/PasswordUIServiceProvider.php -msgid "All-uppercase is almost as easy to guess as all-lowercase" -msgstr "Alleen hoofdletters is bijna net zo makkelijk te raden als alleen kleine letters" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Reversed words aren't much harder to guess" -msgstr "Omgekeerde woorden zijn niet veel moeilijker te raden" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Predictable substitutions like '@' instead of 'a' don't help very much" -msgstr "Voorspelbare vervangingen zoals '@' in plaats van 'a' helpen nauwelijks" - -# Onboarding / welcome email -#. translators: %s: site name -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Welcome to %s!" -msgstr "Welkom bij %s!" - -#. translators: %s: user login -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Welcome %s," -msgstr "Welkom %s," - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Your account has been activated." -msgstr "Je account is geactiveerd." - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Use the link below to set a password and log in:" -msgstr "Gebruik onderstaande link om een wachtwoord in te stellen en in te loggen:" - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "For security reasons, this link is valid for a limited time only." -msgstr "Om veiligheidsredenen is deze link slechts korte tijd geldig." - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Thanks!" -msgstr "Bedankt!" - -#: src/Login/LostPasswordServiceProvider.php:72 -msgid "If an account with this username or email address exists, you will receive an email with a link to reset your password." -msgstr "Als een account met deze gebruikersnaam of email bestaat, ontvang je een email met een wachtwoord reset link." - -# Rate limiting -#: src/RateLimit/RateLimitServiceProvider.php -msgid "Too many failed login attempts. Please try again later." -msgstr "Te veel foutive login pogingen. Probeer het later nogmaals." - -#: src/RateLimit/RateLimitAdminPage.php -msgid "Rate Limiting" -msgstr "Limiteer loginbeveiliging" - -#: src/RateLimit/RateLimitAdminPage.php -msgid "If a legitimate user has been locked out due to too many failed login attempts, you can clear all rate limits below." -msgstr "Als legitieme gebruikers zijn geblokeerd zijn door teveel foutieve login pogingen, kan je hier alle limietcounters resetten." - -#: src/RateLimit/RateLimitAdminPage.php -msgid "Clear All Rate Limits" -msgstr "Reset alle login limieten" - -#: src/RateLimit/RateLimitAdminPage.php -msgid "All rate limits have been cleared." -msgstr "Alle login limieten zijn reset." - -#. Description of the plugin -#: yard-warden.php -msgid "Enhanced password and login security for WordPress." -msgstr "Verbeterde wachtwoord- en inlogbeveiliging voor WordPress." diff --git a/languages/yard_warden.pot b/languages/yard_warden.pot deleted file mode 100644 index 8a7d2bd..0000000 --- a/languages/yard_warden.pot +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright (C) 2026 Yard | Digital Agency -# This file is distributed under the GPLv2 or later license. -msgid "" -msgstr "" -"Project-Id-Version: Yard Warden 0.1.0\n" -"Report-Msgid-Bugs-To: https://www.yard.nl\n" -"POT-Creation-Date: 2026-04-16T00:00:00+00:00\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Language-Team: \n" -"Language: \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Domain: yard_warden\n" - -#. Plugin Name of the plugin -#: yard-warden.php -msgid "Yard Warden" -msgstr "" - -#. Description of the plugin -#: yard-warden.php -msgid "Enhanced password and login security for WordPress." -msgstr "" - -#: src/Login/LoginServiceProvider.php:52 -msgid "Invalid credentials." -msgstr "" - -#: src/Login/LostPasswordServiceProvider.php:72 -msgid "If an account with this username or email address exists, you will receive an email with a link to reset your password." -msgstr "" - -# Login limiting -#: src/LimitLogin/LimitLoginServiceProvider.php -msgid "Too many failed login attempts. Please try again later." -msgstr "" - -#: src/LimitLogin/LimitLoginAdminPage.php -msgid "Login Limiting" -msgstr "" - -#: src/LimitLogin/LimitLoginAdminPage.php -msgid "If a legitimate user has been locked out due to too many failed login attempts, you can clear all login limits below." -msgstr "" - -#: src/LimitLogin/LimitLoginAdminPage.php -msgid "Clear All Login Limits" -msgstr "" - -#: src/LimitLogin/LimitLoginAdminPage.php -msgid "All login limits have been cleared." -msgstr "" - -#. translators: %d: minimum password length -#: src/Password/StrengthValidator.php:46 -msgid "Password must be at least %d characters." -msgstr "" - -#: src/Password/StrengthValidator.php:56 -msgid "This password is too easy to guess." -msgstr "" - -#. translators: 1: minimum password length, 2: required password score, 3: maximum password score -#: src/Password/PasswordUIServiceProvider.php:50 -msgid "Use at least %1$d characters and a strength of %2$d/%3$d." -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:53 -msgid "Too short" -msgstr "" - -#. translators: 1: required password score, 2: maximum password score -#: src/Password/PasswordUIServiceProvider.php:55 -msgid "Minimum strength: %1$d/%2$d." -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:60 -msgid "Very weak" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:61 -msgid "Weak" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:62 -msgid "Fair" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:63 -msgid "Strong" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php:64 -msgid "Very strong" -msgstr "" - -# zxcvbn warnings -#: src/Password/PasswordUIServiceProvider.php -msgid "Straight rows of keys are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Short keyboard patterns are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Use a longer keyboard pattern with more turns" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Repeats like \"aaa\" are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Sequences like abc or 6543 are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Recent years are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Dates are often easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a top-10 common password" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a top-100 common password" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is a very common password" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "This is similar to a commonly used password" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "A word by itself is easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Names and surnames by themselves are easy to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Common names and surnames are easy to guess" -msgstr "" - -# zxcvbn suggestions -#: src/Password/PasswordUIServiceProvider.php -msgid "Use a few words, avoid common phrases" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "No need for symbols, digits, or uppercase letters" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Add another word or two. Uncommon words are better." -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid repeated words and characters" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid sequences" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid recent years" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid years that are associated with you" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Avoid dates and years that are associated with you" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Capitalization doesn't help very much" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "All-uppercase is almost as easy to guess as all-lowercase" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Reversed words aren't much harder to guess" -msgstr "" - -#: src/Password/PasswordUIServiceProvider.php -msgid "Predictable substitutions like '@' instead of 'a' don't help very much" -msgstr "" - -# Onboarding / welcome email -#. translators: %s: site name -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Welcome to %s!" -msgstr "" - -#. translators: %s: user login -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Welcome %s," -msgstr "" - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Your account has been activated." -msgstr "" - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Use the link below to set a password and log in:" -msgstr "" - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "For security reasons, this link is valid for a limited time only." -msgstr "" - -#: src/Onboarding/OnboardingServiceProvider.php -msgid "Thanks!" -msgstr "" diff --git a/yard-warden.php b/yard-warden.php index ee39104..2449321 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -14,7 +14,6 @@ * Author URI: https://www.yard.nl * License: GPLv2 or later * Text Domain: yard-warden - * Domain Path: /languages * Requires at least: 6.3 * Requires PHP: 7.4 */ From dc253c6c221d2adf3deb4a20d691cfc55ec1e938 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 13:26:38 +0200 Subject: [PATCH 09/13] fix: add non-permitted files to distignore --- .distignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.distignore b/.distignore index 5eedf39..1dd6d00 100644 --- a/.distignore +++ b/.distignore @@ -11,3 +11,7 @@ .DS_Store wp-content tests +data-scripts +composer.lock +*.neon +*.dist From 511adb02dc2c7942b95812c7fe7676316160ad2a Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 13:27:11 +0200 Subject: [PATCH 10/13] chore: add prefix to several generic names --- src/LimitLogin/LimitLoginServiceProvider.php | 4 ++-- src/LimitLogin/Limiter.php | 4 ++-- src/Login/LoginServiceProvider.php | 2 +- src/Onboarding/OnboardingServiceProvider.php | 2 +- src/Password/StrengthValidator.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/LimitLogin/LimitLoginServiceProvider.php b/src/LimitLogin/LimitLoginServiceProvider.php index 01196ca..ecf215a 100644 --- a/src/LimitLogin/LimitLoginServiceProvider.php +++ b/src/LimitLogin/LimitLoginServiceProvider.php @@ -26,7 +26,7 @@ class LimitLoginServiceProvider public const FILTER_ERROR_MESSAGE = 'yard::warden/limit-login/error-message'; public const FILTER_SKIP_ERROR_CODES = 'yard::warden/limit-login/skip-error-codes'; - public const ERROR_CODE = 'too_many_attempts'; + public const ERROR_CODE = 'yard_warden_too_many_attempts'; /** @var string[] */ private const DEFAULT_SKIP_CODES = ['expired_session']; @@ -44,7 +44,7 @@ public function register(): void // Priority 30: after core auth at 20, so a valid-credential WP_User gets // overwritten with our lockout error. Same priority as LoginServiceProvider // but registered after it — LoginServiceProvider only rewrites leaky codes, - // not our too_many_attempts code, so our response is the final value. + // not our yard_warden_too_many_attempts code, so our response is the final value. add_filter('authenticate', [$this, 'checkLockout'], 30, 2); add_action('wp_login_failed', [$this, 'recordFailedAttempt'], 10, 2); add_action('wp_login', [$this, 'onSuccessfulLogin'], 10, 1); diff --git a/src/LimitLogin/Limiter.php b/src/LimitLogin/Limiter.php index c32cb9e..812d028 100644 --- a/src/LimitLogin/Limiter.php +++ b/src/LimitLogin/Limiter.php @@ -22,8 +22,8 @@ */ class Limiter { - protected const KEY_PREFIX = 'yw_ll:'; - protected const LOCK_PREFIX = 'yw_ll:lock:'; + protected const KEY_PREFIX = 'yard_warden_ll_'; + protected const LOCK_PREFIX = 'yard_warden_ll_lock_'; public const DEFAULT_THRESHOLD_IP_USER = 5; public const DEFAULT_THRESHOLD_IP = 50; diff --git a/src/Login/LoginServiceProvider.php b/src/Login/LoginServiceProvider.php index 58d1acb..0032547 100644 --- a/src/Login/LoginServiceProvider.php +++ b/src/Login/LoginServiceProvider.php @@ -52,7 +52,7 @@ public function filterAuthenticateErrors($user, $username) return $user; } - return new WP_Error('invalid_credentials', $this->genericErrorMessage()); + return new WP_Error('yard_warden_invalid_credentials', $this->genericErrorMessage()); } private function isLeakyCode(string $code): bool diff --git a/src/Onboarding/OnboardingServiceProvider.php b/src/Onboarding/OnboardingServiceProvider.php index 87e2c52..cc67bac 100644 --- a/src/Onboarding/OnboardingServiceProvider.php +++ b/src/Onboarding/OnboardingServiceProvider.php @@ -29,7 +29,7 @@ class OnboardingServiceProvider { public const FILTER_WELCOME_SUBJECT = 'yard::warden/onboarding/welcome-subject'; public const FILTER_WELCOME_BODY = 'yard::warden/onboarding/welcome-body'; - public const DISABLE_WELCOME_REQUEST_KEY = 'disable_welcome_email'; + public const DISABLE_WELCOME_REQUEST_KEY = 'yard_warden_disable_welcome_email'; public function register(): void { diff --git a/src/Password/StrengthValidator.php b/src/Password/StrengthValidator.php index 013efa0..54caa0e 100644 --- a/src/Password/StrengthValidator.php +++ b/src/Password/StrengthValidator.php @@ -41,7 +41,7 @@ public function validate(string $password, ?WP_User $user = null): ?WP_Error if (strlen($password) < $minLength) { return new WP_Error( - 'weak_password_length', + 'yard_warden_weak_password_length', sprintf( /* translators: %d: minimum password length */ __('Password must be at least %d characters.', 'yard-warden'), @@ -56,7 +56,7 @@ public function validate(string $password, ?WP_User $user = null): ?WP_Error if ((int) ($result['score'] ?? 0) < $minScore) { $feedback = $result['feedback']['warning'] ?? __('This password is too easy to guess.', 'yard-warden'); - return new WP_Error('weak_password_score', (string) $feedback); + return new WP_Error('yard_warden_weak_password_score', (string) $feedback); } return null; From 1dc351e0d735c3df3fa62190ae5ad3e7506d6641 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 15:02:11 +0200 Subject: [PATCH 11/13] fix: correct paths in distignore --- .distignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.distignore b/.distignore index 1dd6d00..bd7ab05 100644 --- a/.distignore +++ b/.distignore @@ -10,8 +10,8 @@ .phpunit.result.cache .DS_Store wp-content -tests -data-scripts +tests/* +data-scripts/* composer.lock *.neon *.dist From b9de44853fdd9d71fa7b86a9e29a4f5dd6766dd6 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Tue, 11 Aug 2026 15:02:18 +0200 Subject: [PATCH 12/13] release: 1.0.3 --- README.md | 2 +- readme.txt | 12 +++++++++++- yard-warden.php | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 127689c..1c125cc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Requires at least: 6.3 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 1.0.2 +Stable tag: 1.0.3 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/readme.txt b/readme.txt index 9eac3cf..a284355 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: security, password, login, brute force, multisite Requires at least: 6.3 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 1.0.2 +Stable tag: 1.0.3 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -62,6 +62,13 @@ Yes, via the `yard::warden/limit-login/enabled` filter. == Changelog == += 1.0.3 = +* Minimum WordPress version raised to 6.3. +* Bundled Dutch translations removed; translations are now served through translate.wordpress.org. +* WP_Error codes prefixed with yard_warden_ to avoid collisions with other plugins. +* Login-limit transient keys prefixed with yard_warden_ll_. +* Welcome-email opt-out query flag renamed to yard_warden_disable_welcome_email. + = 1.0.2 = * Text domain adjusted to yard-warden. @@ -70,5 +77,8 @@ Yes, via the `yard::warden/limit-login/enabled` filter. == Upgrade Notice == += 1.0.3 = +Login attempt counters and active lockouts are reset on upgrade. If you use the disable_welcome_email query flag, rename it to yard_warden_disable_welcome_email. + = 1.0.2 = Text domain change only, no action required. diff --git a/yard-warden.php b/yard-warden.php index 2449321..a66ac72 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -9,7 +9,7 @@ * * Plugin Name: Yard | Warden * Description: Enhanced password and login security for WordPress. - * Version: 1.0.2 + * Version: 1.0.3 * Author: Yard | Digital Agency * Author URI: https://www.yard.nl * License: GPLv2 or later @@ -29,7 +29,7 @@ require_once __DIR__ . '/vendor/autoload.php'; -define('YARD_WARDEN_VERSION', '1.0.2'); +define('YARD_WARDEN_VERSION', '1.0.3'); define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.3'); define('YARD_WARDEN_PLUGIN_FILE', __FILE__); define('YARD_WARDEN_PLUGIN_DIR_PATH', plugin_dir_path(YARD_WARDEN_PLUGIN_FILE)); From 72d81ca53d7beea1fe7dddaa667697437d598881 Mon Sep 17 00:00:00 2001 From: Roric van Schalkwijk Date: Thu, 20 Aug 2026 20:29:47 +0200 Subject: [PATCH 13/13] feat: setup svn workflow --- .github/workflows/deploy.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..da8f4b9 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,41 @@ +name: Deploy to WordPress.org +on: + push: + tags: + - "*" + workflow_dispatch: + +jobs: + tag: + name: New tag + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v5 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: composer + + - name: Install runtime dependencies + run: composer install --no-dev --optimize-autoloader + + - name: WordPress deploy plugin + id: deploy + uses: 10up/action-wordpress-plugin-deploy@stable + with: + generate-zip: true + env: + SLUG: yard-warden + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.deploy.outputs.zip-path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}