diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..bd7ab05 --- /dev/null +++ b/.distignore @@ -0,0 +1,17 @@ +.git +.github +.gitignore +.gitattributes +.distignore +.editorconfig +.markdownlint.yml +.php-cs-fixer.php +.php-cs-fixer.cache +.phpunit.result.cache +.DS_Store +wp-content +tests/* +data-scripts/* +composer.lock +*.neon +*.dist 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 }} diff --git a/README.md b/README.md index f108de3..1c125cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# Yard Warden +# Yard | Warden + +Requires at least: 6.3 +Tested up to: 7.0 +Requires PHP: 7.4 +Stable tag: 1.0.3 +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/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/languages/yard_warden-nl_NL.mo b/languages/yard_warden-nl_NL.mo deleted file mode 100644 index 19d3385..0000000 Binary files a/languages/yard_warden-nl_NL.mo and /dev/null differ 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/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 diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..a284355 --- /dev/null +++ b/readme.txt @@ -0,0 +1,84 @@ +=== Yard | Warden === +Contributors: yarddigitalagency +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.3 +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.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. + += 1.0.0 = +* Initial release. + +== 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/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/Bootstrap.php b/src/Bootstrap.php index 5d34bbd..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_TEXT_DOMAIN, - 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 c40b5d7..b3c974e 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. @@ -32,8 +39,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 +69,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' ); @@ -78,22 +85,23 @@ 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); } ?>
-

+

-

-

+

+

['default' => '']]) + ? filter_var(wp_unslash($_SERVER['REMOTE_ADDR']), FILTER_VALIDATE_IP, ['options' => ['default' => '']]) : ''; return (string) apply_filters(self::FILTER_CLIENT_IP, $ip); @@ -207,7 +214,7 @@ private function errorMessage(): string { return (string) apply_filters( self::FILTER_ERROR_MESSAGE, - __('Too many failed login attempts. Please try again later.', YARD_WARDEN_TEXT_DOMAIN) + __('Too many failed login attempts. Please try again later.', 'yard-warden') ); } diff --git a/src/LimitLogin/Limiter.php b/src/LimitLogin/Limiter.php index dfc6bbc..812d028 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. @@ -15,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 2990262..0032547 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. * @@ -41,11 +48,11 @@ public function filterAuthenticateErrors($user, $username) return $user; } - if (! $this->isLeakyCode($user->get_error_code())) { + if (! $this->isLeakyCode((string) $user->get_error_code())) { 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 @@ -59,7 +66,7 @@ private function genericErrorMessage(): string { return (string) apply_filters( self::GENERIC_ERROR_FILTER, - __('Invalid credentials.', YARD_WARDEN_TEXT_DOMAIN) + __('Invalid credentials.', 'yard-warden') ); } } diff --git a/src/Login/LostPasswordServiceProvider.php b/src/Login/LostPasswordServiceProvider.php index be5177a..99de821 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,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. if (! isset($_GET['checkemail']) || 'confirm' !== $_GET['checkemail']) { return $errors; } @@ -68,7 +76,7 @@ public function ambiguousConfirmMessage(WP_Error $errors): WP_Error $errors->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..cc67bac 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. * @@ -22,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 { @@ -93,7 +100,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 ); @@ -150,6 +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. return isset($_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]) && '' !== $_REQUEST[self::DISABLE_WELCOME_REQUEST_KEY]; } @@ -175,18 +183,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/PasswordServiceProvider.php b/src/Password/PasswordServiceProvider.php index 0848f30..18da7d4 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; @@ -22,21 +29,22 @@ 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 { - $password = (string) ($_POST['pass1'] ?? ''); + // 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) { return; diff --git a/src/Password/PasswordUIServiceProvider.php b/src/Password/PasswordUIServiceProvider.php index 3ebe7e5..feacba4 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 @@ -27,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 @@ -55,25 +62,28 @@ public function enqueue(): void wp_localize_script(self::SCRIPT_HANDLE, 'YardWarden', [ 'minLength' => $minLength, 'minScore' => $minScore, + 'maxScore' => StrengthValidator::MAX_SCORE, 'strings' => [ 'hint' => sprintf( - __('Use at least %1$d characters and a strength of %2$d/%3$d.', YARD_WARDEN_TEXT_DOMAIN), + /* 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, - 4 + StrengthValidator::MAX_SCORE ), - 'tooShort' => __('Too short', YARD_WARDEN_TEXT_DOMAIN), + 'tooShort' => __('Too short', 'yard-warden'), 'requiredScore' => sprintf( - __('Minimum strength: %1$d/%2$d.', YARD_WARDEN_TEXT_DOMAIN), + /* translators: 1: minimum password strength score, 2: maximum password strength score */ + __('Minimum strength: %1$d/%2$d.', 'yard-warden'), $minScore, - 4 + StrengthValidator::MAX_SCORE ), '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 +101,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..54caa0e 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. */ @@ -15,6 +22,7 @@ class StrengthValidator { public const DEFAULT_MIN_LENGTH = 16; public const DEFAULT_MIN_SCORE = 4; + public const MAX_SCORE = 4; protected Zxcvbn $zxcvbn; @@ -33,10 +41,10 @@ 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_TEXT_DOMAIN), + __('Password must be at least %d characters.', 'yard-warden'), $minLength ) ); @@ -46,9 +54,9 @@ 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); + return new WP_Error('yard_warden_weak_password_score', (string) $feedback); } return null; diff --git a/yard-warden.php b/yard-warden.php index 54973cc..a66ac72 100644 --- a/yard-warden.php +++ b/yard-warden.php @@ -9,13 +9,12 @@ * * 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 - * Text Domain: yard_warden - * Domain Path: /languages - * Requires at least: 6.0 + * Text Domain: yard-warden + * Requires at least: 6.3 * Requires PHP: 7.4 */ @@ -28,13 +27,12 @@ exit; } -define('YARD_WARDEN_VERSION', '1.0.2'); -define('YARD_WARDEN_REQUIRED_WP_VERSION', '6.0'); -define('YARD_WARDEN_PLUGIN_NAME', basename(__DIR__)); +require_once __DIR__ . '/vendor/autoload.php'; + +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_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';