From 3f56ee70659d1bf8af8b9d259826c2a3483c521f Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 10:10:26 -0800 Subject: [PATCH 001/120] Add Account Protection toggle to Jetpack security settings --- pnpm-lock.yaml | 2 + .../client/components/settings-card/index.jsx | 19 +++++++ .../_inc/client/lib/plans/constants.js | 2 + .../client/security/account-protection.jsx | 50 +++++++++++++++++++ .../jetpack/_inc/client/security/index.jsx | 4 ++ .../jetpack/modules/account-protection.php | 14 ++++++ 6 files changed, 91 insertions(+) create mode 100644 projects/plugins/jetpack/_inc/client/security/account-protection.jsx create mode 100644 projects/plugins/jetpack/modules/account-protection.php diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 381294a9066fb..2dc54f62fb891 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1765,6 +1765,8 @@ importers: specifier: 4.9.1 version: 4.9.1(webpack@5.94.0) + projects/packages/account-protection: {} + projects/packages/admin-ui: {} projects/packages/assets: diff --git a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx index 8cb3d077921bd..6dd2932eb7285 100644 --- a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx @@ -24,6 +24,7 @@ import { getJetpackProductUpsellByFeature, FEATURE_JETPACK_BLAZE, FEATURE_JETPACK_EARN, + FEATURE_JETPACK_ACCOUNT_PROTECTION, } from 'lib/plans/constants'; import ProStatus from 'pro-status'; import { @@ -455,6 +456,24 @@ export const SettingsCard = inprops => { rna /> ); + case FEATURE_JETPACK_ACCOUNT_PROTECTION: + if ( props.hasConnectedOwner ) { + return ''; + } + + return ( + + ); default: return ''; } diff --git a/projects/plugins/jetpack/_inc/client/lib/plans/constants.js b/projects/plugins/jetpack/_inc/client/lib/plans/constants.js index 0a486259173e5..12a0743eb48cc 100644 --- a/projects/plugins/jetpack/_inc/client/lib/plans/constants.js +++ b/projects/plugins/jetpack/_inc/client/lib/plans/constants.js @@ -417,6 +417,7 @@ export const FEATURE_POST_BY_EMAIL = 'post-by-email-jetpack'; export const FEATURE_JETPACK_SOCIAL = 'social-jetpack'; export const FEATURE_JETPACK_BLAZE = 'blaze-jetpack'; export const FEATURE_JETPACK_EARN = 'earn-jetpack'; +export const FEATURE_JETPACK_ACCOUNT_PROTECTION = 'account-protection-jetpack'; // Upsells export const JETPACK_FEATURE_PRODUCT_UPSELL_MAP = { @@ -439,6 +440,7 @@ export const JETPACK_FEATURE_PRODUCT_UPSELL_MAP = { [ FEATURE_VIDEOPRESS ]: PLAN_JETPACK_VIDEOPRESS, [ FEATURE_NEWSLETTER_JETPACK ]: PLAN_JETPACK_CREATOR_YEARLY, [ FEATURE_WORDADS_JETPACK ]: PLAN_JETPACK_SECURITY_T1_YEARLY, + [ FEATURE_JETPACK_ACCOUNT_PROTECTION ]: PLAN_JETPACK_FREE, }; /** diff --git a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx new file mode 100644 index 0000000000000..39b074365c852 --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx @@ -0,0 +1,50 @@ +import { getRedirectUrl } from '@automattic/jetpack-components'; +import { __, _x } from '@wordpress/i18n'; +import React, { Component } from 'react'; +import { withModuleSettingsFormHelpers } from 'components/module-settings/with-module-settings-form-helpers'; +import { ModuleToggle } from 'components/module-toggle'; +import SettingsCard from 'components/settings-card'; +import SettingsGroup from 'components/settings-group'; +import { FEATURE_JETPACK_ACCOUNT_PROTECTION } from '../lib/plans/constants'; + +const AccountProtectionComponent = class extends Component { + render() { + const isAccountProtectionActive = this.props.getOptionValue( 'account-protection' ), + unavailableInOfflineMode = this.props.isUnavailableInOfflineMode( 'account-protection' ); + return ( + + + + + { __( 'Require strong passwords', 'jetpack' ) } + + + + + ); + } +}; + +export const AccountProtection = withModuleSettingsFormHelpers( AccountProtectionComponent ); diff --git a/projects/plugins/jetpack/_inc/client/security/index.jsx b/projects/plugins/jetpack/_inc/client/security/index.jsx index f6e2c9369fc53..ff1ec0efad4f2 100644 --- a/projects/plugins/jetpack/_inc/client/security/index.jsx +++ b/projects/plugins/jetpack/_inc/client/security/index.jsx @@ -12,6 +12,7 @@ import { isModuleFound } from 'state/search'; import { getSettings } from 'state/settings'; import { siteHasFeature } from 'state/site'; import { isPluginActive, isPluginInstalled } from 'state/site/plugins'; +import { AccountProtection } from './account-protection'; import AllowList from './allowList'; import Antispam from './antispam'; import BackupsScan from './backups-scan'; @@ -91,6 +92,8 @@ export class Security extends Component { ); + const foundAccountProtection = this.props.isModuleFound( 'account-protection' ); + return (
@@ -112,6 +115,7 @@ export class Security extends Component { ) } + { foundAccountProtection && } { foundWaf && } { foundProtect && } { ( foundWaf || foundProtect ) && } diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php new file mode 100644 index 0000000000000..87bd3c9925d1d --- /dev/null +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -0,0 +1,14 @@ + Date: Thu, 9 Jan 2025 11:43:59 -0800 Subject: [PATCH 002/120] Import package and run activation/deactivation on module toggle --- .../src/class-account-protection.php | 66 ++++++++- projects/plugins/jetpack/composer.json | 1 + projects/plugins/jetpack/composer.lock | 139 +++++++++++++----- .../jetpack/modules/account-protection.php | 5 + 4 files changed, 174 insertions(+), 37 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index c895dc25a0216..29623cfd361f1 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -1,16 +1,76 @@ is_active( 'account-protection' ); + } + + /** + * Enables the account protection module. + * + * @return bool + */ + public static function enable() { + // Return true if already enabled. + if ( self::is_enabled() ) { + return true; + } + return ( new Modules() )->activate( 'account-protection', false, false ); + } + + /** + * Disables the account protection module. + * + * @return bool + */ + public static function disable() { + // Return true if already disabled. + if ( ! self::is_enabled() ) { + return true; + } + return ( new Modules() )->deactivate( 'account-protection' ); + } } diff --git a/projects/plugins/jetpack/composer.json b/projects/plugins/jetpack/composer.json index bd613184927c5..ee311a946c40e 100644 --- a/projects/plugins/jetpack/composer.json +++ b/projects/plugins/jetpack/composer.json @@ -12,6 +12,7 @@ "ext-json": "*", "ext-openssl": "*", "automattic/jetpack-a8c-mc-stats": "@dev", + "automattic/jetpack-account-protection": "@dev", "automattic/jetpack-admin-ui": "@dev", "automattic/jetpack-assets": "@dev", "automattic/jetpack-autoloader": "@dev", diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index f823169790931..2c890a56ac012 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/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": "7b6c53f88fcb9c7098d80137fd6d13c1", + "content-hash": "cbb88a4e4e1b0088ff12393af82e5cdc", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -59,6 +59,76 @@ "relative": true } }, + { + "name": "automattic/jetpack-account-protection", + "version": "dev-trunk", + "dist": { + "type": "path", + "url": "../../packages/account-protection", + "reference": "c22829e6a80ff9f5cd10e4b4eece3d405f69e8f9" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-changelogger": "@dev", + "automattic/wordbless": "dev-master", + "yoast/phpunit-polyfills": "^1.1.1" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-account-protection/compare/v${old}...v${new}" + }, + "mirror-repo": "Automattic/jetpack-account-protection", + "textdomain": "jetpack-account-protection", + "version-constants": { + "::PACKAGE_VERSION": "src/class-account-protection.php" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "build-development": [ + "echo 'Add your build step to composer.json, please!'" + ], + "build-production": [ + "echo 'Add your build step to composer.json, please!'" + ], + "phpunit": [ + "./vendor/phpunit/phpunit/phpunit --colors=always" + ], + "post-install-cmd": [ + "WorDBless\\Composer\\InstallDropin::copy" + ], + "post-update-cmd": [ + "WorDBless\\Composer\\InstallDropin::copy" + ], + "test-coverage": [ + "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\"" + ], + "test-php": [ + "@composer phpunit" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Account protection", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-admin-ui", "version": "dev-trunk", @@ -3320,16 +3390,16 @@ "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.2.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "b07d4fb37c3c723c8755122160c089e077d5de65" + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/b07d4fb37c3c723c8755122160c089e077d5de65", - "reference": "b07d4fb37c3c723c8755122160c089e077d5de65", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/1bf183a3e1bd094f231a2128b9ecc5363c269245", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245", "shasum": "" }, "require": { @@ -3362,9 +3432,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.2.0" + "source": "https://github.com/antecedent/patchwork/tree/2.2.1" }, - "time": "2024-09-27T16:59:55+00:00" + "time": "2024-12-11T10:19:54+00:00" }, { "name": "automattic/jetpack-changelogger", @@ -3686,16 +3756,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -3738,9 +3808,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -5300,16 +5370,16 @@ }, { "name": "symfony/console", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { @@ -5373,7 +5443,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -5389,7 +5459,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5410,12 +5480,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -5722,8 +5792,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -5861,12 +5931,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -6059,16 +6129,16 @@ }, { "name": "yoast/phpunit-polyfills", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "e9c8413de4c8ae03d2923a44f17d0d7dad1b96be" + "reference": "0b31ce834facf03b8b44b6587e65b3cf1d7cfb94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e9c8413de4c8ae03d2923a44f17d0d7dad1b96be", - "reference": "e9c8413de4c8ae03d2923a44f17d0d7dad1b96be", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/0b31ce834facf03b8b44b6587e65b3cf1d7cfb94", + "reference": "0b31ce834facf03b8b44b6587e65b3cf1d7cfb94", "shasum": "" }, "require": { @@ -6118,13 +6188,14 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2024-09-06T22:03:10+00:00" + "time": "2025-01-08T16:58:34+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { "automattic/jetpack-a8c-mc-stats": 20, + "automattic/jetpack-account-protection": 20, "automattic/jetpack-admin-ui": 20, "automattic/jetpack-assets": 20, "automattic/jetpack-autoloader": 20, diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index 87bd3c9925d1d..76849893538ea 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -12,3 +12,8 @@ * * @package automattic/jetpack */ + +use Automattic\Jetpack\Account_Protection\Account_Protection; + +$account_protection = new Account_Protection(); +$account_protection->init(); From c83c60454e2ae906f03265ad927bceb036f47c38 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 11:44:55 -0800 Subject: [PATCH 003/120] changelog --- .../add-jetpack-account-protection-security-settings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings diff --git a/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings new file mode 100644 index 0000000000000..04ebecc39c6e1 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Adds a Account Protection module toggle From ab4f99aa37d7f7d4fe937f7e5c8091fcbd7f44c7 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 13:44:11 -0800 Subject: [PATCH 004/120] Add Protect Settings page and hook up Account Protection toggle --- projects/plugins/protect/composer.json | 3 +- projects/plugins/protect/composer.lock | 127 ++++++++++++++---- .../protect/src/class-jetpack-protect.php | 2 + .../protect/src/class-rest-controller.php | 65 +++++++++ projects/plugins/protect/src/js/api.ts | 12 ++ .../src/js/components/admin-page/index.jsx | 4 + projects/plugins/protect/src/js/constants.js | 1 + .../use-account-protection-query.ts | 17 +++ .../use-toggle-account-protection-mutation.ts | 40 ++++++ projects/plugins/protect/src/js/index.tsx | 2 + .../protect/src/js/routes/settings/index.jsx | 99 ++++++++++++++ .../src/js/routes/settings/styles.module.scss | 53 ++++++++ .../plugins/protect/src/js/types/global.d.ts | 1 + 13 files changed, 397 insertions(+), 29 deletions(-) create mode 100644 projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts create mode 100644 projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts create mode 100644 projects/plugins/protect/src/js/routes/settings/index.jsx create mode 100644 projects/plugins/protect/src/js/routes/settings/styles.module.scss diff --git a/projects/plugins/protect/composer.json b/projects/plugins/protect/composer.json index 6819583bc0ea3..ab10678d3898e 100644 --- a/projects/plugins/protect/composer.json +++ b/projects/plugins/protect/composer.json @@ -17,7 +17,8 @@ "automattic/jetpack-plans": "@dev", "automattic/jetpack-waf": "@dev", "automattic/jetpack-status": "@dev", - "automattic/jetpack-protect-status": "@dev" + "automattic/jetpack-protect-status": "@dev", + "automattic/jetpack-account-protection": "@dev" }, "require-dev": { "yoast/phpunit-polyfills": "^1.1.1", diff --git a/projects/plugins/protect/composer.lock b/projects/plugins/protect/composer.lock index 920a59e6563a4..74d116ee76a0c 100644 --- a/projects/plugins/protect/composer.lock +++ b/projects/plugins/protect/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": "3645f8b938b9d5c8f1dc3b736ea3abaa", + "content-hash": "0cdb2fec4c2556c8d7b6d23371401a0a", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -59,6 +59,76 @@ "relative": true } }, + { + "name": "automattic/jetpack-account-protection", + "version": "dev-trunk", + "dist": { + "type": "path", + "url": "../../packages/account-protection", + "reference": "c22829e6a80ff9f5cd10e4b4eece3d405f69e8f9" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-changelogger": "@dev", + "automattic/wordbless": "dev-master", + "yoast/phpunit-polyfills": "^1.1.1" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-account-protection/compare/v${old}...v${new}" + }, + "mirror-repo": "Automattic/jetpack-account-protection", + "textdomain": "jetpack-account-protection", + "version-constants": { + "::PACKAGE_VERSION": "src/class-account-protection.php" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "build-development": [ + "echo 'Add your build step to composer.json, please!'" + ], + "build-production": [ + "echo 'Add your build step to composer.json, please!'" + ], + "phpunit": [ + "./vendor/phpunit/phpunit/phpunit --colors=always" + ], + "post-install-cmd": [ + "WorDBless\\Composer\\InstallDropin::copy" + ], + "post-update-cmd": [ + "WorDBless\\Composer\\InstallDropin::copy" + ], + "test-coverage": [ + "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\"" + ], + "test-php": [ + "@composer phpunit" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Account protection", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-admin-ui", "version": "dev-trunk", @@ -2260,16 +2330,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -2312,9 +2382,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -4058,16 +4128,16 @@ }, { "name": "symfony/console", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { @@ -4131,7 +4201,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -4147,7 +4217,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4168,12 +4238,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4480,8 +4550,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4619,12 +4689,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4817,16 +4887,16 @@ }, { "name": "yoast/phpunit-polyfills", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "e9c8413de4c8ae03d2923a44f17d0d7dad1b96be" + "reference": "0b31ce834facf03b8b44b6587e65b3cf1d7cfb94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/e9c8413de4c8ae03d2923a44f17d0d7dad1b96be", - "reference": "e9c8413de4c8ae03d2923a44f17d0d7dad1b96be", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/0b31ce834facf03b8b44b6587e65b3cf1d7cfb94", + "reference": "0b31ce834facf03b8b44b6587e65b3cf1d7cfb94", "shasum": "" }, "require": { @@ -4876,12 +4946,13 @@ "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2024-09-06T22:03:10+00:00" + "time": "2025-01-08T16:58:34+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { + "automattic/jetpack-account-protection": 20, "automattic/jetpack-admin-ui": 20, "automattic/jetpack-assets": 20, "automattic/jetpack-autoloader": 20, diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 293ccdaeb3ce7..b2cbf268a2a2b 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -9,6 +9,7 @@ exit; } +use Automattic\Jetpack\Account_Protection\Account_Protection; use Automattic\Jetpack\Admin_UI\Admin_Menu; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State; @@ -226,6 +227,7 @@ public function initial_state() { 'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ), 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), + 'accountProtection' => Account_Protection::is_enabled(), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), 'currentIp' => IP_Utils::get_ip(), diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index 0aa752ddfd6d1..32d85f5e8ad97 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -9,6 +9,7 @@ namespace Automattic\Jetpack\Protect; +use Automattic\Jetpack\Account_Protection\Account_Protection; use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication; use Automattic\Jetpack\IP\Utils as IP_Utils; use Automattic\Jetpack\Protect_Status\REST_Controller as Protect_Status_REST_Controller; @@ -117,6 +118,30 @@ public static function register_rest_endpoints() { ) ); + register_rest_route( + 'jetpack-protect/v1', + 'toggle-account-protection', + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::api_toggle_account_protection', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ) + ); + + register_rest_route( + 'jetpack-protect/v1', + 'account-protection', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::api_get_account_protection', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ) + ); + register_rest_route( 'jetpack-protect/v1', 'toggle-waf', @@ -340,6 +365,46 @@ public static function api_scan() { return new WP_REST_Response( 'Scan enqueued.' ); } + /** + * Toggles the Account Protection module on or off for the API endpoint + * + * @return WP_REST_Response|WP_Error + */ + public static function api_toggle_account_protection() { + if ( Account_Protection::is_enabled() ) { + $disabled = Account_Protection::disable(); + if ( ! $disabled ) { + return new WP_Error( + 'account_protection_disable_failed', + __( 'An error occurred disabling account protection.', 'jetpack-protect' ), + array( 'status' => 500 ) + ); + } + + return rest_ensure_response( true ); + } + + $enabled = Account_Protection::enable(); + if ( ! $enabled ) { + return new WP_Error( + 'account_protection_enable_failed', + __( 'An error occurred enabling account protection.', 'jetpack-protect' ), + array( 'status' => 500 ) + ); + } + + return rest_ensure_response( true ); + } + + /** + * Get Account Protection data for the API endpoint + * + * @return WP_Rest_Response + */ + public static function api_get_account_protection() { + return new WP_REST_Response( Account_Protection::is_enabled() ); + } + /** * Toggles the WAF module on or off for the API endpoint * diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts index 2b98a6164bf8b..b2570e892fc61 100644 --- a/projects/plugins/protect/src/js/api.ts +++ b/projects/plugins/protect/src/js/api.ts @@ -3,6 +3,18 @@ import apiFetch from '@wordpress/api-fetch'; import camelize from 'camelize'; const API = { + toggleAccountProtection: () => + apiFetch( { + method: 'POST', + path: 'jetpack-protect/v1/toggle-account-protection', + } ), + + getAccountProtection: () => + apiFetch( { + path: 'jetpack-protect/v1/account-protection', + method: 'GET', + } ), + getWaf: (): Promise< WafStatus > => apiFetch( { path: 'jetpack-protect/v1/waf', diff --git a/projects/plugins/protect/src/js/components/admin-page/index.jsx b/projects/plugins/protect/src/js/components/admin-page/index.jsx index 4579831b5f0a5..95e34eb79daa8 100644 --- a/projects/plugins/protect/src/js/components/admin-page/index.jsx +++ b/projects/plugins/protect/src/js/components/admin-page/index.jsx @@ -69,6 +69,10 @@ const AdminPage = ( { children } ) => { } /> + { __( 'Settings', 'jetpack-protect' ) } } + /> { children } diff --git a/projects/plugins/protect/src/js/constants.js b/projects/plugins/protect/src/js/constants.js index 5ec94bdccafc9..f643493fd37bd 100644 --- a/projects/plugins/protect/src/js/constants.js +++ b/projects/plugins/protect/src/js/constants.js @@ -31,3 +31,4 @@ export const QUERY_ONBOARDING_PROGRESS_KEY = 'onboarding progress'; export const QUERY_PRODUCT_DATA_KEY = 'product data'; export const QUERY_SCAN_STATUS_KEY = 'scan status'; export const QUERY_WAF_KEY = 'waf'; +export const QUERY_ACCOUNT_PROTECTION_KEY = 'account protection'; diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts new file mode 100644 index 0000000000000..cc9ac9cbf202c --- /dev/null +++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts @@ -0,0 +1,17 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import camelize from 'camelize'; +import API from '../../api'; +import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; + +/** + * WAF Query Hook + * + * @return {UseQueryResult} useQuery result. + */ +export default function useAccountProtectionQuery(): UseQueryResult< boolean > { + return useQuery( { + queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ], + queryFn: API.getAccountProtection, + initialData: camelize( window?.jetpackProtectInitialState?.accountProtection ), + } ); +} diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts new file mode 100644 index 0000000000000..10fa874cb1ebb --- /dev/null +++ b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts @@ -0,0 +1,40 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; + +/** + * Toggle Account Protection Mutatation + * + * @return {UseMutationResult} useMutation result. + */ +export default function useToggleAccountProtectMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: API.toggleAccountProtection, + onMutate: () => { + showSavingNotice(); + // Get the current account protection settings. + const initialValue = queryClient.getQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ] ); + + // Optimistically update settings. + queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], ( status: boolean ) => ! status ); + + return { initialValue }; + }, + onSuccess: () => { + showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); + }, + onError: () => { + showErrorNotice( + __( 'An error occurred toggling the account protection module.', 'jetpack-protect' ) + ); + }, + onSettled: () => { + queryClient.invalidateQueries( { queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ] } ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx index b8983d65bb836..3ffe20e853986 100644 --- a/projects/plugins/protect/src/js/index.tsx +++ b/projects/plugins/protect/src/js/index.tsx @@ -13,6 +13,7 @@ import { CheckoutProvider } from './hooks/use-plan'; import FirewallRoute from './routes/firewall'; import ScanRoute from './routes/scan'; import ScanHistoryRoute from './routes/scan/history'; +import SettingsRoute from './routes/settings'; import SetupRoute from './routes/setup'; import './styles.module.scss'; @@ -56,6 +57,7 @@ function render() { + } /> } /> } /> { + const { hasPlan } = usePlan(); + const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); + const { data: isAccountProtectionEnabled } = useAccountProtectionQuery(); + + // Track view for Protect Account Protection page. + useAnalyticsTracks( { + pageViewEventName: 'protect_account_protection', + pageViewEventProperties: { + has_plan: hasPlan, + }, + } ); + + /** + * Toggle Account Protection Module + * + * Flips the switch on the Account Protection module, and then refreshes the data. + */ + const toggleAccountProtection = useCallback( async () => { + toggleAccountProtectionMutation.mutate(); + }, [ toggleAccountProtectionMutation ] ); + + const accountProtectionSettings = ( +
+
+ +
+
+ + { __( 'Require strong passwords', 'jetpack-protect' ) } + + + { createInterpolateElement( + __( + 'When enabled, users can only set passwords that meet strong security standards, helping protect their accounts and your site.', + 'jetpack-protect' + ), + { + link: , // TODO: Update this redirect URL + } + ) } + + { isAccountProtectionEnabled && ( + + + { createInterpolateElement( + __( + 'Jetpack recommends activating this setting. Please be mindful of the risks.', + 'jetpack-protect' + ), + { + link: , // TODO: Update this redirect URL + } + ) } + + ) } +
+
+ ); + + /** + * Render + */ + return ( + + + + +
{ accountProtectionSettings }
+ +
+
+
+ ); +}; + +export default SettingsPage; diff --git a/projects/plugins/protect/src/js/routes/settings/styles.module.scss b/projects/plugins/protect/src/js/routes/settings/styles.module.scss new file mode 100644 index 0000000000000..3b43e636ffd96 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/settings/styles.module.scss @@ -0,0 +1,53 @@ +.container { + width: 100%; + max-width: calc( 744px + ( var( --spacing-base ) * 6 ) ); // 744px + 48px (desired inner width + horizontal padding) +} + +.toggle-section { + display: flex; + + &:not(:first-child) { + margin-top: calc( var( --spacing-base ) * 7 ); // 56px + } + + &__control { + padding-top: calc( var( --spacing-base ) / 2 ); // 4px + margin-right: calc( var( --spacing-base ) * 2 ); // 16px + + @media ( min-width: 600px ) { + margin-right: calc( var( --spacing-base ) * 5 ); // 48px + } + } + + &__content { + width: 100%; + } + + &__description { + a { + color: inherit; + + &:hover { + color: var( --jp-black ); + } + } + } + + &__warning { + color: var( --jp-red-50 ); + + a { + color: var( --jp-red-50 ); + + &:hover { + color: var( --jp-red-70 ) + } + } + + svg { + fill: var( --jp-red-50 ); + margin-bottom: calc( -1 * var( --spacing-base ) * 3/4 ); // -6px + margin-right: calc( var( --spacing-base ) / 4 ); // 2px + } + } +} \ No newline at end of file diff --git a/projects/plugins/protect/src/js/types/global.d.ts b/projects/plugins/protect/src/js/types/global.d.ts index 826b133869a7a..2f01fad2c7a54 100644 --- a/projects/plugins/protect/src/js/types/global.d.ts +++ b/projects/plugins/protect/src/js/types/global.d.ts @@ -29,6 +29,7 @@ declare global { jetpackScan: ProductData; hasPlan: boolean; onboardingProgress: string[]; + accountProtection: boolean; waf: WafStatus; }; } From c2e8f1eb13c2f33f67a34ef55c905befd7b31666 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 13:45:25 -0800 Subject: [PATCH 005/120] changelog --- .../protect/changelog/add-protect-account-protection-settings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/protect/changelog/add-protect-account-protection-settings diff --git a/projects/plugins/protect/changelog/add-protect-account-protection-settings b/projects/plugins/protect/changelog/add-protect-account-protection-settings new file mode 100644 index 0000000000000..0383c19735e86 --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-account-protection-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds the Account Protection module toggle From b64fdaf43b2318dc121e47577a01ad21afbd147d Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 13:48:06 -0800 Subject: [PATCH 006/120] Update changelog --- .../changelog/add-jetpack-account-protection-security-settings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings index 04ebecc39c6e1..4c36bca9e49ec 100644 --- a/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings +++ b/projects/plugins/jetpack/changelog/add-jetpack-account-protection-security-settings @@ -1,4 +1,4 @@ Significance: minor Type: enhancement -Adds a Account Protection module toggle +Adds the Account Protection module toggle From 3f685364189e17df934ba1420e093d4c7cfb9587 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 14:58:26 -0800 Subject: [PATCH 007/120] Register modules on plugin activation --- projects/plugins/protect/src/class-jetpack-protect.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index b2cbf268a2a2b..9d84cc27525d3 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -59,6 +59,7 @@ class Jetpack_Protect { ); const JETPACK_WAF_MODULE_SLUG = 'waf'; const JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG = 'protect'; + const JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG = 'account-protection'; const JETPACK_PROTECT_ACTIVATION_OPTION = JETPACK_PROTECT_SLUG . '_activated'; /** @@ -277,12 +278,13 @@ public static function do_plugin_activation_activities() { } /** - * Activates the waf and brute force protection modules and disables the activation option + * Activates the waf, brute force protection and account protection modules and disables the activation option */ public static function activate_modules() { delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ); ( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false ); ( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false ); + ( new Modules() )->activate( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, false, false ); } /** @@ -340,7 +342,7 @@ public function admin_bar( $wp_admin_bar ) { * @return array */ public function protect_filter_available_modules( $modules ) { - return array_merge( array( self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG ), $modules ); + return array_merge( array( self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG ), $modules ); } /** From 3628b02dce3c81b2801244dfa6d03e1783ec7bae Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 15:08:00 -0800 Subject: [PATCH 008/120] Ensure package is initialized on plugin activation --- projects/plugins/protect/src/class-jetpack-protect.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 9d84cc27525d3..e2a1cfd74d38b 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -114,6 +114,9 @@ function () { // Web application firewall package. $config->ensure( 'waf' ); + + // Account protection package. + $config->ensure( 'account_protection' ); }, 1 ); @@ -135,6 +138,7 @@ public function init() { REST_Controller::init(); My_Jetpack_Initializer::init(); Site_Health::init(); + Account_Protection::init(); // Sets up JITMS. JITM::configure(); From 3f90fe40747ccdcaccc8294fbac5163ebd3ec768 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 9 Jan 2025 15:09:17 -0800 Subject: [PATCH 009/120] Make account protection class init static --- .../account-protection/src/class-account-protection.php | 2 +- projects/plugins/jetpack/modules/account-protection.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 29623cfd361f1..c623faa14781a 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -19,7 +19,7 @@ class Account_Protection { /** * Initializes the configurations needed for the account protection module. */ - public function init() { + public static function init() { // Account protection activation/deactivation hooks add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index 76849893538ea..891d023f7fe37 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -15,5 +15,4 @@ use Automattic\Jetpack\Account_Protection\Account_Protection; -$account_protection = new Account_Protection(); -$account_protection->init(); +Account_Protection::init(); From 3bfbcb3de075174da0771e746c1d03537e46bbed Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 13 Jan 2025 14:55:00 -0800 Subject: [PATCH 010/120] Add auth hooks, redirect and a custom login action template --- .../src/assets/jetpack-logo.php | 21 ++ .../src/class-account-protection.php | 203 +++++++++++++++++- .../src/css/password-detection.css | 50 +++++ .../templates/password-detection-template.php | 22 ++ 4 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 projects/packages/account-protection/src/assets/jetpack-logo.php create mode 100644 projects/packages/account-protection/src/css/password-detection.css create mode 100644 projects/packages/account-protection/src/templates/password-detection-template.php diff --git a/projects/packages/account-protection/src/assets/jetpack-logo.php b/projects/packages/account-protection/src/assets/jetpack-logo.php new file mode 100644 index 0000000000000..b91e3c5c216f5 --- /dev/null +++ b/projects/packages/account-protection/src/assets/jetpack-logo.php @@ -0,0 +1,21 @@ + + "Jetpack Logo" + + + + + + + + + diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index c623faa14781a..8a4b9ce49c20b 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -23,20 +23,217 @@ public static function init() { // Account protection activation/deactivation hooks add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); + + if ( self::is_enabled() ) { + // Validate password post successful login + add_action( 'wp_authenticate_user', __CLASS__ . '::custom_post_login_password_check', 10, 2 ); + + // Add custom validation flow for users with unsafe passwords + add_action( + 'login_form_password-detection', + function () { + // Restrict access for logged-out users + if ( ! is_user_logged_in() ) { + wp_redirect( wp_login_url() ); + exit; + } + + $current_user = wp_get_current_user(); + // TODO: Are we confident we always have a user here? How to handle it otherwise... + $password_status = get_user_meta( $current_user->ID, 'jetpack_account_protection_password_status', true ); + + // Restrict access for logged in users with secure or unevaluated passwords to the admin + if ( ! $password_status || 'safe' === $password_status ) { + wp_redirect( admin_url() ); + exit; + } + + if ( isset( $_POST['reset'] ) ) { + $site_url = home_url(); + $parsed_url = parse_url( $site_url ); + $domain_name = $parsed_url['host']; + $username = $current_user->user_login; + $email = $current_user->user_email; + $masked_email = self::mask_email_address( $email ); + + $key = get_password_reset_key( $current_user ); + $locale = get_user_locale( $current_user ); + $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $current_user->user_login ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; + + // Send reset email to user - only initially and on resend, not refresh + self::send_custom_reset_email( $domain_name, $username, $email, $password_reset_link ); + + $header_title = 'Secure Your Account'; + $page_title = "Let's secure your account"; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . $masked_email . '. After that, we\'ll guide you through updating your password.

+

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; + + } elseif ( isset( $_POST['proceed'] ) ) { + wp_redirect( admin_url() ); + exit; + } else { + $header_title = 'Stay Secure'; + $page_title = 'Take action to stay secure'; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

It is highly recommended that you update your password.

+
+
+ +
+
+ +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

'; + } + + // Include the template + include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; + exit; + } + ); + + // Ensure jetpack_account_protection_password_status user meta is removed on password change + // TODO: Is there potentially another hook for storing old password after reset? + // Or we can store old passwords by hash and use wp_check_password to validate + add_action( + 'after_password_reset', + function ( $user, $new_pass ) { + delete_user_meta( $user->ID, 'jetpack_account_protection_password_status' ); + + // Add old password hash to user meta + // $password_hash_history = get_user_meta( $user->ID, 'jetpack_account_protection_password_hash_history', true ) ?: []; + // $password_hash_history[] = $user->user_pass; + // update_user_meta( $user->ID, 'jetpack_account_protection_password_hash_history', $password_hash_history ); + }, + 10, + 2 + ); + + // Ensure jetpack_account_protection_password_status user meta is removed when user updates password via profile updates + add_action( + 'profile_update', + function ( $user_id, $old_user_data ) { + // Profile updates should include validation, but we should reset user meta to be safe + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + // TODO: Only if the password is actually updated + delete_user_meta( $user_id, 'jetpack_account_protection_password_status' ); + + // // Add old password hash to user meta + // $password_hash_history = get_user_meta( $user_id, 'jetpack_account_protection_password_hash_history', true ) ?: []; + // // Add the current password hash to the array + // $password_hash_history[] = $old_user_data->user_pass; + // update_user_meta( $user_id, 'jetpack_account_protection_password_hash_history', $password_hash_history ); + } + }, + 10, + 2 + ); + + // TODO: Action/cron for clearing out old password hashes? + + } + } + + /** + * Custom login validation. + */ + public static function custom_post_login_password_check( $user, $password ) { + if ( ! self::custom_password_check( $password ) ) { + error_log( 'Password check failed' ); + // TODO: Are there any potential issues with using this, eg if somehow the pass is updated before this is corrected? + update_user_meta( $user->ID, 'jetpack_account_protection_password_status', 'unsafe' ); + + // Log the user in but customize the redirect + add_filter( 'login_redirect', __CLASS__ . '::custom_login_redirect', 10, 3 ); + } else { + update_user_meta( $user->ID, 'user', 'safe' ); + } + + return $user; + } + + /** + * Custom post login password check. + */ + public static function custom_password_check( $password ) { + // TESTING BGN + // TODO: This belongs in the set/update validation + // current test wont work here because we wouldn't reach this part because core validation would error out the login process... + // This would work somewhere... + // error_log( var_export( wp_check_password( 'wordpress', $old_hash ), true ) ); + + // $user = wp_get_current_user(); + // $user_id = $user->ID; + + // // Retrieve old hashes + // $old_hashes = get_user_meta( $user_id, 'old_password_hashes', true ) ?: []; + + // // Check against each old hash + // foreach ( $old_hashes as $old_hash ) { + // if ( wp_check_password( $password, $old_hash ) ) { + // error_log( var_export( 'Password found in old hashes ' . $password, true ) ); + // } + // } + // Once we have the reset_link we can test this... + // TESTING STOP + + // TODO: The validation here is less extension then when setting a password, for example, no need to include a historic check + return $password ? false : true; + } + + public static function custom_login_redirect() { + return home_url( '/wp-login.php?action=password-detection' ); + } + + public static function send_custom_reset_email( $domain_name, $username, $email, $password_reset_link ) { + error_log( 'Site: ' . $domain_name ); + error_log( 'Username: ' . $username ); + error_log( 'Sending custom reset email to ' . $email ); + error_log( 'Password reset link: ' . $password_reset_link ); + } + + /** + * Mask an email address like d*****@g*****.com. + * + * @param string $email The email address to mask. + * @return string The masked email address. + */ + public static function mask_email_address( $email ) { + $parts = explode( '@', $email ); + $name = $parts[0]; + $domain = $parts[1]; + + // Mask the name part (first letter + asterisks) + $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); + + // Mask the domain part (first letter + asterisks + domain extension) + $domain_parts = explode( '.', $domain ); + $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; + + return $masked_name . '@' . $masked_domain; } /** * Activate the account protection on module activation. */ public static function on_account_protection_activation() { - // Account protection activated } /** - * Deactivate the account protection on module activation. + * Deactivate the account protection on module deactivation. */ public static function on_account_protection_deactivation() { - // Account protection deactivated + // Remove user meta on deactivation + $users = get_users(); + foreach ( $users as $user ) { + delete_user_meta( $user->ID, 'jetpack_account_protection_password_status' ); + // TODO: Do we want to clear password hash history only on deactivation? + // TODO: Ensure this happens on plugin deactivation as well? + } } /** diff --git a/projects/packages/account-protection/src/css/password-detection.css b/projects/packages/account-protection/src/css/password-detection.css new file mode 100644 index 0000000000000..00d9203ea32c8 --- /dev/null +++ b/projects/packages/account-protection/src/css/password-detection.css @@ -0,0 +1,50 @@ +body { + background: #f0f0f1; + min-width: 0; + color: #3c434a; + font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 13px; + line-height: 1.4; +} + +.custom { + background: #fff; + width: 420px; + margin: 124px auto; + padding: 26px 24px; + font-weight: 400; + overflow: hidden; + background: #fff; + border: 1px solid #c3c4c7; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.custom-title { + font-size: 24px; + font-weight: 500; +} + +.actions { + display: flex; + flex-direction: column; + gap: 8px; +} + +.action { + height: 36px; + cursor: pointer; + width: 100%; +} + +.action-reset { + margin-top: 10px; + background-color: #0000EE; + border: 1px solid #0000EE; + color: #fff; + } + +.action-proceed { + background-color: #fff; + border: 1px solid #0000EE; + color: #0000EE; +} \ No newline at end of file diff --git a/projects/packages/account-protection/src/templates/password-detection-template.php b/projects/packages/account-protection/src/templates/password-detection-template.php new file mode 100644 index 0000000000000..748663cb8460c --- /dev/null +++ b/projects/packages/account-protection/src/templates/password-detection-template.php @@ -0,0 +1,22 @@ + + + + + + + + <?php echo 'Jetpack - ' . esc_html( $header_title ); ?> + + + +
+ +

+ +
+ +'; \ No newline at end of file From 289dbdbdbd5d95e5a5dff44b9f246558b47bcf08 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 14 Jan 2025 09:01:52 -0800 Subject: [PATCH 011/120] Reorg, add Password_Detection class --- .../src/class-account-protection.php | 334 +++++++++--------- .../src/class-password-detection.php | 15 + 2 files changed, 183 insertions(+), 166 deletions(-) create mode 100644 projects/packages/account-protection/src/class-password-detection.php diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 8a4b9ce49c20b..1bfb3b36b019a 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -25,175 +25,198 @@ public static function init() { add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); if ( self::is_enabled() ) { - // Validate password post successful login - add_action( 'wp_authenticate_user', __CLASS__ . '::custom_post_login_password_check', 10, 2 ); + // Validate password after successful login + add_action( 'wp_authenticate_user', __CLASS__ . '::login_form_password_detection', 10, 2 ); - // Add custom validation flow for users with unsafe passwords + // Add password detection flow for users with unsafe passwords add_action( 'login_form_password-detection', - function () { - // Restrict access for logged-out users - if ( ! is_user_logged_in() ) { - wp_redirect( wp_login_url() ); - exit; - } - - $current_user = wp_get_current_user(); - // TODO: Are we confident we always have a user here? How to handle it otherwise... - $password_status = get_user_meta( $current_user->ID, 'jetpack_account_protection_password_status', true ); - - // Restrict access for logged in users with secure or unevaluated passwords to the admin - if ( ! $password_status || 'safe' === $password_status ) { - wp_redirect( admin_url() ); - exit; - } - - if ( isset( $_POST['reset'] ) ) { - $site_url = home_url(); - $parsed_url = parse_url( $site_url ); - $domain_name = $parsed_url['host']; - $username = $current_user->user_login; - $email = $current_user->user_email; - $masked_email = self::mask_email_address( $email ); - - $key = get_password_reset_key( $current_user ); - $locale = get_user_locale( $current_user ); - $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $current_user->user_login ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; - - // Send reset email to user - only initially and on resend, not refresh - self::send_custom_reset_email( $domain_name, $username, $email, $password_reset_link ); - - $header_title = 'Secure Your Account'; - $page_title = "Let's secure your account"; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . $masked_email . '. After that, we\'ll guide you through updating your password.

-

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; - - } elseif ( isset( $_POST['proceed'] ) ) { - wp_redirect( admin_url() ); - exit; - } else { - $header_title = 'Stay Secure'; - $page_title = 'Take action to stay secure'; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

It is highly recommended that you update your password.

-
-
- -
-
- -
-
-

Learn more about the risks of using weak passwords and how to protect your account.

'; - } - - // Include the template - include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; - exit; - } + __CLASS__ . '::render_password_detection_page', + 10, + 2 ); - // Ensure jetpack_account_protection_password_status user meta is removed on password change - // TODO: Is there potentially another hook for storing old password after reset? - // Or we can store old passwords by hash and use wp_check_password to validate + // Ensure jetpack_account_protection_password_status usermeta is removed on password change add_action( 'after_password_reset', function ( $user, $new_pass ) { delete_user_meta( $user->ID, 'jetpack_account_protection_password_status' ); - - // Add old password hash to user meta - // $password_hash_history = get_user_meta( $user->ID, 'jetpack_account_protection_password_hash_history', true ) ?: []; - // $password_hash_history[] = $user->user_pass; - // update_user_meta( $user->ID, 'jetpack_account_protection_password_hash_history', $password_hash_history ); }, 10, 2 ); - // Ensure jetpack_account_protection_password_status user meta is removed when user updates password via profile updates + // Ensure jetpack_account_protection_password_status usermeta is removed when user updates password via profile updates add_action( 'profile_update', function ( $user_id, $old_user_data ) { // Profile updates should include validation, but we should reset user meta to be safe if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { // TODO: Only if the password is actually updated - delete_user_meta( $user_id, 'jetpack_account_protection_password_status' ); - - // // Add old password hash to user meta - // $password_hash_history = get_user_meta( $user_id, 'jetpack_account_protection_password_hash_history', true ) ?: []; - // // Add the current password hash to the array - // $password_hash_history[] = $old_user_data->user_pass; - // update_user_meta( $user_id, 'jetpack_account_protection_password_hash_history', $password_hash_history ); + self::remove_password_detection_usermeta( $user_id ); } }, 10, 2 ); + } + } - // TODO: Action/cron for clearing out old password hashes? + /** + * Activate the account protection on module activation. + */ + public static function on_account_protection_activation() { + } + /** + * Deactivate the account protection on module deactivation. + */ + public static function on_account_protection_deactivation() { + // Remove user meta on deactivation + $users = get_users(); + foreach ( $users as $user ) { + self::remove_password_detection_usermeta( $user->ID ); + // TODO: Remove usermeta on plugin deactivation as well } } /** - * Custom login validation. + * Determines if the account protection module is enabled on the site. + * + * @return bool + */ + public static function is_enabled() { + return ( new Modules() )->is_active( 'account-protection' ); + } + + /** + * Enables the account protection module. + * + * @return bool + */ + public static function enable() { + // Return true if already enabled. + if ( self::is_enabled() ) { + return true; + } + return ( new Modules() )->activate( 'account-protection', false, false ); + } + + /** + * Disables the account protection module. + * + * @return bool + */ + public static function disable() { + // Return true if already disabled. + if ( ! self::is_enabled() ) { + return true; + } + return ( new Modules() )->deactivate( 'account-protection' ); + } + + /** + * Check if the password is safe after login. + * + * @param WP_User $user The user object. + * @param string $password The password. + * @return WP_User The user object. */ - public static function custom_post_login_password_check( $user, $password ) { - if ( ! self::custom_password_check( $password ) ) { - error_log( 'Password check failed' ); - // TODO: Are there any potential issues with using this, eg if somehow the pass is updated before this is corrected? + public static function login_form_password_detection( $user, $password ) { + if ( ! self::validate_password( $password ) ) { + // TODO: Ensure this usermeta is always up to date update_user_meta( $user->ID, 'jetpack_account_protection_password_status', 'unsafe' ); - // Log the user in but customize the redirect - add_filter( 'login_redirect', __CLASS__ . '::custom_login_redirect', 10, 3 ); + // Redirect to the password detection page + add_filter( 'login_redirect', __CLASS__ . '::password_detection_redirect', 10, 3 ); } else { - update_user_meta( $user->ID, 'user', 'safe' ); + update_user_meta( $user->ID, 'jetpack_account_protection_password_status', 'safe' ); } return $user; } /** - * Custom post login password check. + * Render password detection page. + * + * This page is shown to users with unsafe passwords after login. + * + * @return void */ - public static function custom_password_check( $password ) { - // TESTING BGN - // TODO: This belongs in the set/update validation - // current test wont work here because we wouldn't reach this part because core validation would error out the login process... - // This would work somewhere... - // error_log( var_export( wp_check_password( 'wordpress', $old_hash ), true ) ); - - // $user = wp_get_current_user(); - // $user_id = $user->ID; - - // // Retrieve old hashes - // $old_hashes = get_user_meta( $user_id, 'old_password_hashes', true ) ?: []; - - // // Check against each old hash - // foreach ( $old_hashes as $old_hash ) { - // if ( wp_check_password( $password, $old_hash ) ) { - // error_log( var_export( 'Password found in old hashes ' . $password, true ) ); - // } - // } - // Once we have the reset_link we can test this... - // TESTING STOP - - // TODO: The validation here is less extension then when setting a password, for example, no need to include a historic check - return $password ? false : true; + public static function render_password_detection_page() { + // Restrict direct access to logged in users + if ( ! is_user_logged_in() ) { + wp_redirect( wp_login_url() ); + exit; + } + + $current_user = wp_get_current_user(); + $user_password_status = get_user_meta( $current_user->ID, 'jetpack_account_protection_password_status', true ); + + // Restrict direct access to users with unsafe passwords + if ( ! $user_password_status || 'safe' === $user_password_status ) { + wp_redirect( admin_url() ); + exit; + } + + if ( isset( $_POST['reset'] ) ) { + $email = $current_user->user_email; + + // Send reset email to the user - only initially and on resend, not refresh + $email_sent = self::send_password_reset_email( $current_user, $email ); + if ( ! $email_sent ) { + // TODO: Handle email sending errors + } + + $header_title = 'Secure Your Account'; + $page_title = "Let's secure your account"; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . self::mask_email_address( $email ) . '. After that, we\'ll guide you through updating your password.

+

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; + + } elseif ( isset( $_POST['proceed'] ) ) { + wp_redirect( admin_url() ); + exit; + } else { + $header_title = 'Stay Secure'; + $page_title = 'Take action to stay secure'; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

It is highly recommended that you update your password.

+
+
+ +
+
+ +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

'; + } + + include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; + exit; } - public static function custom_login_redirect() { - return home_url( '/wp-login.php?action=password-detection' ); + /** + * Password validation. + * + * @param string $password The password to validate. + * @return bool True if the password is valid, false otherwise. + */ + public static function validate_password( $password ) { + // TODO: Update to use custom password validation method when available. + return $password ? false : true; } - public static function send_custom_reset_email( $domain_name, $username, $email, $password_reset_link ) { - error_log( 'Site: ' . $domain_name ); - error_log( 'Username: ' . $username ); - error_log( 'Sending custom reset email to ' . $email ); - error_log( 'Password reset link: ' . $password_reset_link ); + /** + * Redirect to the password detection page. + * + * @return string The URL to redirect to. + */ + public static function password_detection_redirect() { + return home_url( '/wp-login.php?action=password-detection' ); } /** @@ -218,56 +241,35 @@ public static function mask_email_address( $email ) { } /** - * Activate the account protection on module activation. - */ - public static function on_account_protection_activation() { - } - - /** - * Deactivate the account protection on module deactivation. - */ - public static function on_account_protection_deactivation() { - // Remove user meta on deactivation - $users = get_users(); - foreach ( $users as $user ) { - delete_user_meta( $user->ID, 'jetpack_account_protection_password_status' ); - // TODO: Do we want to clear password hash history only on deactivation? - // TODO: Ensure this happens on plugin deactivation as well? - } - } - - /** - * Determines if the account protection module is enabled on the site. + * Send password reset email. * - * @return bool + * @param WP_User $user The user object. + * @param string $email The user email. + * @return bool True if the email was sent successfully, false otherwise. */ - public static function is_enabled() { - return ( new Modules() )->is_active( 'account-protection' ); + public static function send_password_reset_email( $user, $email ) { + $site_url = home_url(); + $parsed_url = parse_url( $site_url ); + $domain_name = $parsed_url['host']; + $username = $user->user_login; + + $key = get_password_reset_key( $user ); + $locale = get_user_locale( $user ); + $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; + + // TODO: Update to use custom email method when available, passing $domain_name, $email, $username, and $password_reset_link + return true; } /** - * Enables the account protection module. + * Remove the password detection usermeta. * - * @return bool + * @param int $user_id The user ID. */ - public static function enable() { - // Return true if already enabled. - if ( self::is_enabled() ) { - return true; - } - return ( new Modules() )->activate( 'account-protection', false, false ); + public static function remove_password_detection_usermeta( $user_id ) { + delete_user_meta( $user_id, 'jetpack_account_protection_password_status' ); } - /** - * Disables the account protection module. - * - * @return bool - */ - public static function disable() { - // Return true if already disabled. - if ( ! self::is_enabled() ) { - return true; - } - return ( new Modules() )->deactivate( 'account-protection' ); - } + // TODO: Move password detection methods to a dedicated class + // TODO: Add killswitch define and is support env checks here } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php new file mode 100644 index 0000000000000..93050157d831e --- /dev/null +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -0,0 +1,15 @@ + Date: Tue, 14 Jan 2025 12:50:43 -0800 Subject: [PATCH 012/120] Remove user cxn req and banner --- .../client/components/settings-card/index.jsx | 19 ------------------- .../jetpack/modules/account-protection.php | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx index 6dd2932eb7285..8cb3d077921bd 100644 --- a/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/settings-card/index.jsx @@ -24,7 +24,6 @@ import { getJetpackProductUpsellByFeature, FEATURE_JETPACK_BLAZE, FEATURE_JETPACK_EARN, - FEATURE_JETPACK_ACCOUNT_PROTECTION, } from 'lib/plans/constants'; import ProStatus from 'pro-status'; import { @@ -456,24 +455,6 @@ export const SettingsCard = inprops => { rna /> ); - case FEATURE_JETPACK_ACCOUNT_PROTECTION: - if ( props.hasConnectedOwner ) { - return ''; - } - - return ( - - ); default: return ''; } diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index 891d023f7fe37..4814ac0eca89b 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -5,7 +5,7 @@ * Sort Order: 4 * First Introduced: 14.3 * Requires Connection: Yes - * Requires User Connection: Yes + * Requires User Connection: No * Auto Activate: Yes * Module Tags: Account Protection * Feature: Security From 5a1af0b530159883ee67c31c8bfe6cc9cbfb5d8c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 14 Jan 2025 12:59:34 -0800 Subject: [PATCH 013/120] Do not enabled module by default --- projects/plugins/jetpack/modules/account-protection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index 4814ac0eca89b..b84d338782098 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -6,7 +6,7 @@ * First Introduced: 14.3 * Requires Connection: Yes * Requires User Connection: No - * Auto Activate: Yes + * Auto Activate: No * Module Tags: Account Protection * Feature: Security * From 3b35efe33d914f06f2596a5c4bfd26e339380a50 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 15 Jan 2025 06:23:55 -0800 Subject: [PATCH 014/120] Add strict mode option and settings toggle --- projects/js-packages/api/index.jsx | 10 ++ .../src/class-account-protection.php | 7 +- .../src/class-rest-controller.php | 100 ++++++++++++ .../index.jsx | 44 +++++ .../client/security/account-protection.jsx | 151 +++++++++++++++++- .../_inc/client/security/allowList.jsx | 2 +- .../jetpack/_inc/client/security/index.jsx | 2 +- .../jetpack/_inc/client/security/style.scss | 24 ++- .../state/account-protection/actions.js | 66 ++++++++ .../client/state/account-protection/index.js | 2 + .../state/account-protection/reducer.js | 87 ++++++++++ .../jetpack/_inc/client/state/action-types.js | 9 ++ .../jetpack/_inc/client/state/reducer.js | 2 + .../lib/class.core-rest-api-endpoints.php | 9 +- ....wpcom-json-api-site-settings-endpoint.php | 2 + projects/plugins/jetpack/modules/waf.php | 2 +- 16 files changed, 508 insertions(+), 11 deletions(-) create mode 100644 projects/packages/account-protection/src/class-rest-controller.php create mode 100644 projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx create mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/actions.js create mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/index.js create mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js diff --git a/projects/js-packages/api/index.jsx b/projects/js-packages/api/index.jsx index 8233d0ba8a616..6f6cdffe0b325 100644 --- a/projects/js-packages/api/index.jsx +++ b/projects/js-packages/api/index.jsx @@ -510,6 +510,16 @@ function JetpackRestApiClient( root, nonce ) { getRequest( `${ wpcomOriginApiUrl }jetpack/v4/search/stats`, getParams ) .then( checkStatus ) .then( parseJsonResponse ), + fetchAccountProtectionSettings: () => + getRequest( `${ apiRoot }jetpack/v4/account-protection`, getParams ) + .then( checkStatus ) + .then( parseJsonResponse ), + updateAccountProtectionSettings: newSettings => + postRequest( `${ apiRoot }jetpack/v4/account-protection`, postParams, { + body: JSON.stringify( newSettings ), + } ) + .then( checkStatus ) + .then( parseJsonResponse ), fetchWafSettings: () => getRequest( `${ apiRoot }jetpack/v4/waf`, getParams ) .then( checkStatus ) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index c623faa14781a..f809bbe567657 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -14,7 +14,9 @@ */ class Account_Protection { - const PACKAGE_VERSION = '1.0.0-alpha'; + const PACKAGE_VERSION = '1.0.0-alpha'; + const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; + const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; /** * Initializes the configurations needed for the account protection module. @@ -23,6 +25,9 @@ public static function init() { // Account protection activation/deactivation hooks add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); + + // Register REST routes + add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); } /** diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php new file mode 100644 index 0000000000000..60e6f71f6cc74 --- /dev/null +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -0,0 +1,100 @@ + WP_REST_Server::READABLE, + 'callback' => __CLASS__ . '::get_settings', + 'permission_callback' => __CLASS__ . '::permissions_callback', + ) + ); + + register_rest_route( + 'jetpack/v4', + '/account-protection', + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => __CLASS__ . '::update_settings', + 'permission_callback' => __CLASS__ . '::permissions_callback', + ) + ); + + $routes_registered = true; + } + + /** + * Account Protection Settings Endpoint + * + * @return WP_REST_Response + */ + public static function get_settings() { + return rest_ensure_response( + array( + Account_Protection::STRICT_MODE_OPTION_NAME => get_option( Account_Protection::STRICT_MODE_OPTION_NAME ), + ) + ); + } + + /** + * Update Account Protection Settings Endpoint + * + * @param WP_REST_Request $request The API request. + * + * @return WP_REST_Response|WP_Error + */ + public static function update_settings( $request ) { + // Strict Mode + if ( isset( $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ) ) { + update_option( Account_Protection::STRICT_MODE_OPTION_NAME, $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ? '1' : '' ); + } + + return self::get_settings(); + } + + /** + * Account Protection Endpoint Permissions Callback + * + * @return bool|WP_Error True if user can view the Jetpack admin page. + */ + public static function permissions_callback() { + if ( current_user_can( 'manage_options' ) ) { + return true; + } + + return new WP_Error( + 'invalid_user_permission_manage_options', + REST_Connector::get_user_permissions_error_msg(), + array( 'status' => rest_authorization_required_code() ) + ); + } +} diff --git a/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx b/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx new file mode 100644 index 0000000000000..d86ec79e0917b --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { + fetchAccountProtectionSettings, + isFetchingAccountProtectionSettings, +} from 'state/account-protection'; +import { isOfflineMode } from 'state/connection'; + +class QueryAccountProtectionSettings extends Component { + static propTypes = { + isFetchingAccountProtectionSettings: PropTypes.bool, + isOfflineMode: PropTypes.bool, + }; + + static defaultProps = { + isFetchingAccountProtectionSettings: false, + isOfflineMode: false, + }; + + componentDidMount() { + if ( ! this.props.isFetchingAccountProtectionSettings && ! this.props.isOfflineMode ) { + this.props.fetchAccountProtectionSettings(); + } + } + + render() { + return null; + } +} + +export default connect( + state => { + return { + isFetchingAccountProtectionSettings: isFetchingAccountProtectionSettings( state ), + isOfflineMode: isOfflineMode( state ), + }; + }, + dispatch => { + return { + fetchAccountProtectionSettings: () => dispatch( fetchAccountProtectionSettings() ), + }; + } +)( QueryAccountProtectionSettings ); diff --git a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx index 39b074365c852..6b5e9126b7493 100644 --- a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx +++ b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx @@ -1,16 +1,99 @@ -import { getRedirectUrl } from '@automattic/jetpack-components'; +import { ToggleControl } from '@automattic/jetpack-components'; +import { ExternalLink } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { FormFieldset } from 'components/forms'; +import { createNotice, removeNotice } from 'components/global-notices/state/notices/actions'; import { withModuleSettingsFormHelpers } from 'components/module-settings/with-module-settings-form-helpers'; import { ModuleToggle } from 'components/module-toggle'; import SettingsCard from 'components/settings-card'; import SettingsGroup from 'components/settings-group'; +import QueryAccountProtectionSettings from '../components/data/query-account-protection-settings'; +import InfoPopover from '../components/info-popover'; import { FEATURE_JETPACK_ACCOUNT_PROTECTION } from '../lib/plans/constants'; +import { updateAccountProtectionSettings } from '../state/account-protection/actions'; +import { + getAccountProtectionSettings, + isFetchingAccountProtectionSettings, + isUpdatingAccountProtectionSettings, +} from '../state/account-protection/reducer'; + +const AccountProtection = class extends Component { + /** + * Get options for initial state. + * + * @return {object} + */ + state = { + strictMode: this.props.settings?.strictMode, + }; + + /** + * Keep the form values in sync with updates to the settings prop. + * + * @param {object} prevProps - Next render props. + */ + componentDidUpdate = prevProps => { + // Sync the form values with the settings prop. + if ( this.props.settings !== prevProps.settings ) { + this.setState( { + ...this.state, + strictMode: this.props.settings?.strictMode, + } ); + } + }; + + /** + * Handle settings updates. + * + * @return {void} + */ + onSubmit = () => { + this.props.removeNotice( 'module-setting-update' ); + this.props.removeNotice( 'module-setting-update-success' ); + + this.props.createNotice( 'is-info', __( 'Updating settings…', 'jetpack' ), { + id: 'module-setting-update', + } ); + this.props + .updateAccountProtectionSettings( this.state ) + .then( () => { + this.props.removeNotice( 'module-setting-update' ); + this.props.createNotice( 'is-success', __( 'Updated Settings.', 'jetpack' ), { + id: 'module-setting-update-success', + } ); + } ) + .catch( () => { + this.props.removeNotice( 'module-setting-update' ); + this.props.createNotice( 'is-error', __( 'Error updating settings.', 'jetpack' ), { + id: 'module-setting-update', + } ); + } ); + }; + + /** + * Toggle strict mode. + */ + toggleStrictMode = () => { + const state = { + ...this.state, + strictMode: ! this.state.strictMode, + }; + + this.setState( state, this.onSubmit ); + }; -const AccountProtectionComponent = class extends Component { render() { const isAccountProtectionActive = this.props.getOptionValue( 'account-protection' ), unavailableInOfflineMode = this.props.isUnavailableInOfflineMode( 'account-protection' ); + const baseInputDisabledCase = + ! isAccountProtectionActive || + unavailableInOfflineMode || + this.props.isFetchingAccountProtectionSettings || + this.props.isSavingAnyOption( [ 'account-protection' ] ); + return ( + { isAccountProtectionActive && } - { __( 'Require strong passwords', 'jetpack' ) } + { __( + 'Protect your site with enhanced password detection and profile management security.', + 'jetpack' + ) } + { isAccountProtectionActive && ( + +
+ + + { __( 'Reqiure strong passwords', 'jetpack' ) } + + + { createInterpolateElement( + __( + 'Allow Jetpack to enforce strict password rules. Learn more
Privacy Information', + 'jetpack' + ), + { + ExternalLink: , // TODO: Update this redirect URL + hr:
, + } + ) } +
+
+ } + /> +
+ + ) } ); } }; -export const AccountProtection = withModuleSettingsFormHelpers( AccountProtectionComponent ); +export default connect( + state => { + return { + isFetchingSettings: isFetchingAccountProtectionSettings( state ), + isUpdatingAccountProtectionSettings: isUpdatingAccountProtectionSettings( state ), + settings: getAccountProtectionSettings( state ), + }; + }, + dispatch => { + return { + updateAccountProtectionSettings: newSettings => + dispatch( updateAccountProtectionSettings( newSettings ) ), + createNotice: ( type, message, props ) => dispatch( createNotice( type, message, props ) ), + removeNotice: notice => dispatch( removeNotice( notice ) ), + }; + } +)( withModuleSettingsFormHelpers( AccountProtection ) ); diff --git a/projects/plugins/jetpack/_inc/client/security/allowList.jsx b/projects/plugins/jetpack/_inc/client/security/allowList.jsx index e102a89cd8918..8f9d8621477ab 100644 --- a/projects/plugins/jetpack/_inc/client/security/allowList.jsx +++ b/projects/plugins/jetpack/_inc/client/security/allowList.jsx @@ -155,7 +155,7 @@ const AllowList = class extends Component { label={ { __( - "Prevent Jetpack's security features from blocking specific IP addresses", + "Prevent Jetpack's security features from blocking specific IP addresses.", 'jetpack' ) } diff --git a/projects/plugins/jetpack/_inc/client/security/index.jsx b/projects/plugins/jetpack/_inc/client/security/index.jsx index ff1ec0efad4f2..d4677461de9ea 100644 --- a/projects/plugins/jetpack/_inc/client/security/index.jsx +++ b/projects/plugins/jetpack/_inc/client/security/index.jsx @@ -12,7 +12,7 @@ import { isModuleFound } from 'state/search'; import { getSettings } from 'state/settings'; import { siteHasFeature } from 'state/site'; import { isPluginActive, isPluginInstalled } from 'state/site/plugins'; -import { AccountProtection } from './account-protection'; +import AccountProtection from './account-protection'; import AllowList from './allowList'; import Antispam from './antispam'; import BackupsScan from './backups-scan'; diff --git a/projects/plugins/jetpack/_inc/client/security/style.scss b/projects/plugins/jetpack/_inc/client/security/style.scss index 9a4608ee3bf57..385e7feaa710f 100644 --- a/projects/plugins/jetpack/_inc/client/security/style.scss +++ b/projects/plugins/jetpack/_inc/client/security/style.scss @@ -56,7 +56,9 @@ } &__share-data-popover { - margin-left: 8px; + display: flex; + align-items: center; + margin-left: 4px; } &__upgrade-popover { @@ -189,4 +191,24 @@ .jp-form-settings-group p { margin-bottom: 0.5rem; +} + +.account-protection__settings { + &__toggle-setting { + flex-wrap: wrap; + display: flex; + margin-bottom: 24px; + + &__label { + display: flex; + align-items: center; + } + } + + &__strict-mode-popover { + display: flex; + align-items: center; + margin-left: 4px; + } + } \ No newline at end of file diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js new file mode 100644 index 0000000000000..feee531d78a38 --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js @@ -0,0 +1,66 @@ +import restApi from '@automattic/jetpack-api'; +import { + ACCOUNT_PROTECTION_SETTINGS_FETCH, + ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, + ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, + ACCOUNT_PROTECTION_SETTINGS_UPDATE, + ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, + ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, +} from 'state/action-types'; + +export const fetchAccountProtectionSettings = () => { + return dispatch => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_FETCH, + } ); + return restApi + .fetchAccountProtectionSettings() + .then( settings => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, + settings, + } ); + return settings; + } ) + .catch( error => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, + error: error, + } ); + } ); + }; +}; + +/** + * Update Account Protection Settings + * + * @param {object} newSettings - The new settings to be saved. + * @param {boolean} newSettings.strictMode - Whether strict mode is enabled. + * @return {Function} - The action. + */ +export const updateAccountProtectionSettings = newSettings => { + return dispatch => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_UPDATE, + } ); + return restApi + .updateAccountProtectionSettings( { + jetpack_account_protection_strict_mode: newSettings.strictMode, + } ) + .then( settings => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, + settings, + } ); + return settings; + } ) + .catch( error => { + dispatch( { + type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, + error: error, + } ); + + throw error; + } ); + }; +}; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js new file mode 100644 index 0000000000000..5e3164b4c9f72 --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js @@ -0,0 +1,2 @@ +export * from './reducer'; +export * from './actions'; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js new file mode 100644 index 0000000000000..cb42d7bccc486 --- /dev/null +++ b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js @@ -0,0 +1,87 @@ +import { assign, get } from 'lodash'; +import { combineReducers } from 'redux'; +import { + ACCOUNT_PROTECTION_SETTINGS_FETCH, + ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, + ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, + ACCOUNT_PROTECTION_SETTINGS_UPDATE, + ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, + ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, +} from 'state/action-types'; + +export const data = ( state = {}, action ) => { + switch ( action.type ) { + case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: + case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: + return assign( {}, state, { + strictMode: Boolean( action.settings?.jetpack_account_protection_strict_mode ), + } ); + default: + return state; + } +}; + +export const initialRequestsState = { + isFetchingAccountProtectionSettings: false, + isUpdatingAccountProtectionSettings: false, +}; + +export const requests = ( state = initialRequestsState, action ) => { + switch ( action.type ) { + case ACCOUNT_PROTECTION_SETTINGS_FETCH: + return assign( {}, state, { + isFetchingAccountProtectionSettings: true, + } ); + case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: + case ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL: + return assign( {}, state, { + isFetchingAccountProtectionSettings: false, + } ); + case ACCOUNT_PROTECTION_SETTINGS_UPDATE: + return assign( {}, state, { + isUpdatingAccountProtectionSettings: true, + } ); + case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: + case ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL: + return assign( {}, state, { + isUpdatingAccountProtectionSettings: false, + } ); + default: + return state; + } +}; + +export const reducer = combineReducers( { + data, + requests, +} ); + +/** + * Returns true if currently requesting the account protection settings. Otherwise false. + * + * @param {object} state - Global state tree + * @return {boolean} Whether the account protection settings are being requested + */ +export function isFetchingAccountProtectionSettings( state ) { + return !! state.jetpack.accountProtection.requests.isFetchingAccountProtectionSettings; +} + +/** + * Returns true if currently updating the account protection settings. Otherwise false. + * + * @param {object} state - Global state tree + * @return {boolean} Whether the account protection settings are being requested + */ +export function isUpdatingAccountProtectionSettings( state ) { + return !! state.jetpack.accountProtection.requests.isUpdatingAccountProtectionSettings; +} + +/** + * Returns the account protection's settings. + * + * @param {object} state - Global state tree + * @return {string} File path to bootstrap.php + */ +export function getAccountProtectionSettings( state ) { + return get( state.jetpack.accountProtection, [ 'data' ], {} ); +} diff --git a/projects/plugins/jetpack/_inc/client/state/action-types.js b/projects/plugins/jetpack/_inc/client/state/action-types.js index c4785d4a2ced5..7da1fbb07cf1d 100644 --- a/projects/plugins/jetpack/_inc/client/state/action-types.js +++ b/projects/plugins/jetpack/_inc/client/state/action-types.js @@ -245,6 +245,15 @@ export const JETPACK_LICENSING_GET_USER_LICENSES_FAILURE = export const JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL = 'JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL'; +export const ACCOUNT_PROTECTION_SETTINGS_FETCH = 'ACCOUNT_PROTECTION_SETTINGS_FETCH'; +export const ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE = + 'ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE'; +export const ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL'; +export const ACCOUNT_PROTECTION_SETTINGS_UPDATE = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE'; +export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS = + 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS'; +export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL'; + export const WAF_SETTINGS_FETCH = 'WAF_SETTINGS_FETCH'; export const WAF_SETTINGS_FETCH_RECEIVE = 'WAF_SETTINGS_FETCH_RECEIVE'; export const WAF_SETTINGS_FETCH_FAIL = 'WAF_SETTINGS_FETCH_FAIL'; diff --git a/projects/plugins/jetpack/_inc/client/state/reducer.js b/projects/plugins/jetpack/_inc/client/state/reducer.js index 5ff156b807a49..14e85f0fb5289 100644 --- a/projects/plugins/jetpack/_inc/client/state/reducer.js +++ b/projects/plugins/jetpack/_inc/client/state/reducer.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux'; import { globalNotices } from 'components/global-notices/state/notices/reducer'; +import { reducer as accountProtection } from 'state/account-protection/reducer'; import { dashboard } from 'state/at-a-glance/reducer'; import { reducer as connection } from 'state/connection/reducer'; import { reducer as devCard } from 'state/dev-version/reducer'; @@ -46,6 +47,7 @@ const jetpackReducer = combineReducers( { disconnectSurvey, trackingSettings, licensing, + accountProtection, waf, introOffers, } ); diff --git a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php index e56d427c0c5ea..9697a1d79d423 100644 --- a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php +++ b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php @@ -2358,7 +2358,14 @@ public static function get_updateable_data_list( $selector = '' ) { 'validate_callback' => __CLASS__ . '::validate_posint', 'jp_group' => 'custom-content-types', ), - + // Account Protection. + 'jetpack_account_protection_strict_mode' => array( + 'description' => esc_html__( 'Strict mode - Require strong passwords.', 'jetpack' ), + 'type' => 'boolean', + 'default' => 0, + 'validate_callback' => __CLASS__ . '::validate_boolean', + 'jp_group' => 'account-protection', + ), // WAF. 'jetpack_waf_automatic_rules' => array( 'description' => esc_html__( 'Enable automatic rules - Protect your site against untrusted traffic sources with automatic security rules.', 'jetpack' ), diff --git a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php index 9852478a7c53d..4b931dadb330f 100644 --- a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php +++ b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php @@ -127,6 +127,7 @@ 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled', 'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled', 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt', + 'jetpack_account_protection_strict_mode' => '(bool) Whether to enforce strict password requirements', 'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules', 'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow', 'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled', @@ -490,6 +491,7 @@ function ( $newsletter_category ) { 'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ), 'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ), 'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ), + 'jetpack_account_protection_strict_mode' => (bool) get_option( 'jetpack_account_protection_strict_mode' ), 'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ), 'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ), 'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ), diff --git a/projects/plugins/jetpack/modules/waf.php b/projects/plugins/jetpack/modules/waf.php index 1d5a5984f4bab..0df3856fb1948 100644 --- a/projects/plugins/jetpack/modules/waf.php +++ b/projects/plugins/jetpack/modules/waf.php @@ -1,7 +1,7 @@ Date: Wed, 15 Jan 2025 06:29:22 -0800 Subject: [PATCH 015/120] changelog --- .../add-jetpack-account-protection-security-settings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings diff --git a/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings b/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings new file mode 100644 index 0000000000000..778ccde6854ed --- /dev/null +++ b/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds Account Protection requests From b62811b31700435ab2a4f47d0ea30e4d943d1c73 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 15 Jan 2025 08:50:04 -0800 Subject: [PATCH 016/120] Add strict mode toggle --- .../protect/src/js/routes/settings/index.jsx | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/projects/plugins/protect/src/js/routes/settings/index.jsx b/projects/plugins/protect/src/js/routes/settings/index.jsx index d8172573fb0ea..edbed69dc3bda 100644 --- a/projects/plugins/protect/src/js/routes/settings/index.jsx +++ b/projects/plugins/protect/src/js/routes/settings/index.jsx @@ -49,7 +49,7 @@ const SettingsPage = () => {
- { __( 'Require strong passwords', 'jetpack-protect' ) } + { __( 'Account protection', 'jetpack-protect' ) } { createInterpolateElement( @@ -62,20 +62,46 @@ const SettingsPage = () => { } ) } - { isAccountProtectionEnabled && ( - - - { createInterpolateElement( - __( - 'Jetpack recommends activating this setting. Please be mindful of the risks.', - 'jetpack-protect' - ), - { - link: , // TODO: Update this redirect URL - } - ) } - - ) } +
+ + ); + + const strictModeSettings = ( +
+
+ +
+
); @@ -88,7 +114,10 @@ const SettingsPage = () => { -
{ accountProtectionSettings }
+
+ { accountProtectionSettings } + { isAccountProtectionEnabled && strictModeSettings } +
From 28f5820cfbbc299d473991d41209566821b98bd9 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 15 Jan 2025 11:10:11 -0800 Subject: [PATCH 017/120] Add strict mode toggle and endpoints --- .../src/class-account-protection.php | 13 ++++ .../src/class-rest-controller.php | 8 +-- .../components/breve/features/events.ts | 4 +- .../protect/src/class-jetpack-protect.php | 5 +- projects/plugins/protect/src/js/api.ts | 19 ++++-- .../use-account-protection-mutation.ts | 57 ++++++++++++++++++ .../use-account-protection-query.ts | 5 +- ...ggle-account-protection-module-mutation.ts | 45 ++++++++++++++ .../use-toggle-account-protection-mutation.ts | 40 ------------- .../use-account-protection-data/index.jsx | 59 +++++++++++++++++++ .../protect/src/js/routes/settings/index.jsx | 31 ++++------ .../src/js/types/account-protection.ts | 12 ++++ 12 files changed, 225 insertions(+), 73 deletions(-) create mode 100644 projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts delete mode 100644 projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx create mode 100644 projects/plugins/protect/src/js/types/account-protection.ts diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index f809bbe567657..6ef5c215d4fdd 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -78,4 +78,17 @@ public static function disable() { } return ( new Modules() )->deactivate( 'account-protection' ); } + + /** + * Get the account protection settings. + * + * @return array + */ + public static function get_settings() { + $settings = array( + self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), + ); + + return $settings; + } } diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php index 60e6f71f6cc74..d8af3451bc861 100644 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -58,11 +58,9 @@ public static function register_rest_routes() { * @return WP_REST_Response */ public static function get_settings() { - return rest_ensure_response( - array( - Account_Protection::STRICT_MODE_OPTION_NAME => get_option( Account_Protection::STRICT_MODE_OPTION_NAME ), - ) - ); + $settings = Account_Protection::get_settings(); + + return rest_ensure_response( $settings ); } /** diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts index 58075d8857569..eb5eb0442d2e4 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts @@ -97,7 +97,7 @@ async function handleMouseEnter( e: MouseEvent ) { target: el, virtual: virtual, } as Anchor ); - }, 500 ); + }, 500 ) as unknown as number; } function handleMouseLeave() { @@ -106,7 +106,7 @@ function handleMouseLeave() { highlightTimeout = setTimeout( () => { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ); + }, 100 ) as unknown as number; } export default function registerEvents( clientId: string ) { diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index e2a1cfd74d38b..8c0ec4f1b5b99 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -232,7 +232,10 @@ public function initial_state() { 'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ), 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), - 'accountProtection' => Account_Protection::is_enabled(), + 'accountProtection' => array( + 'isEnabled' => Account_Protection::is_enabled(), + 'settings' => Account_Protection::get_settings(), + ), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), 'currentIp' => IP_Utils::get_ip(), diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts index b2570e892fc61..186ac89c2c513 100644 --- a/projects/plugins/protect/src/js/api.ts +++ b/projects/plugins/protect/src/js/api.ts @@ -1,19 +1,28 @@ -import { type FixersStatus, type ScanStatus, type WafStatus } from '@automattic/jetpack-scan'; +import { type FixersStatus, type ScanStatus } from '@automattic/jetpack-scan'; import apiFetch from '@wordpress/api-fetch'; import camelize from 'camelize'; +import { AccountProtectionStatus } from './types/account-protection'; +import { WafStatus } from './types/waf'; const API = { + getAccountProtection: (): Promise< AccountProtectionStatus > => + apiFetch( { + path: 'jetpack-protect/v1/account-protection', + method: 'GET', + } ), + toggleAccountProtection: () => apiFetch( { method: 'POST', path: 'jetpack-protect/v1/toggle-account-protection', } ), - getAccountProtection: () => + updateAccountProtection: data => apiFetch( { - path: 'jetpack-protect/v1/account-protection', - method: 'GET', - } ), + method: 'POST', + path: 'jetpack/v4/account-protection', + data, + } ).then( camelize ), getWaf: (): Promise< WafStatus > => apiFetch( { diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts new file mode 100644 index 0000000000000..abd7749a797cd --- /dev/null +++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts @@ -0,0 +1,57 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import camelize from 'camelize'; +import API from '../../api'; +import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; +import { AccountProtectionStatus } from '../../types/account-protection'; + +/** + * Account Protection Mutatation Hook + * + * @return {UseMutationResult} useMutation result. + */ +export default function useAccountProtectionMutation(): UseMutationResult< + unknown, + { [ key: string ]: unknown }, + unknown, + { initialValue: AccountProtectionStatus } +> { + const queryClient = useQueryClient(); + const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: API.updateAccountProtection, + onMutate: settings => { + showSavingNotice(); + + // Get the current Account Protection settings. + const initialValue = queryClient.getQueryData( [ + QUERY_ACCOUNT_PROTECTION_KEY, + ] ) as AccountProtectionStatus; + + // Optimistically update the Account Protection settings. + queryClient.setQueryData( + [ QUERY_ACCOUNT_PROTECTION_KEY ], + ( accountProtectionStatus: AccountProtectionStatus ) => ( { + ...accountProtectionStatus, + settings: { + ...accountProtectionStatus.settings, + ...camelize( settings ), + }, + } ) + ); + + return { initialValue }; + }, + onSuccess: () => { + showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); + }, + onError: ( error, variables, context ) => { + // Reset the WAF config to its previous state. + queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], context.initialValue ); + + showErrorNotice( __( 'Error saving changes.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts index cc9ac9cbf202c..01dd3354432a9 100644 --- a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts +++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts @@ -2,13 +2,14 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import camelize from 'camelize'; import API from '../../api'; import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; +import { AccountProtectionStatus } from '../../types/account-protection'; /** - * WAF Query Hook + * Account Protection Query Hook * * @return {UseQueryResult} useQuery result. */ -export default function useAccountProtectionQuery(): UseQueryResult< boolean > { +export default function useAccountProtectionQuery(): UseQueryResult< AccountProtectionStatus > { return useQuery( { queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ], queryFn: API.getAccountProtection, diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts new file mode 100644 index 0000000000000..2f8ca342902ea --- /dev/null +++ b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts @@ -0,0 +1,45 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; +import { AccountProtectionStatus } from '../../types/account-protection'; + +/** + * Toggle Account Protection Mutatation + * + * @return {UseMutationResult} useMutation result. + */ +export default function useToggleAccountProtectionMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSavingNotice, showSuccessNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: API.toggleAccountProtection, + onMutate: () => { + showSavingNotice(); + + // Get the current Account Protection settings. + const initialValue = queryClient.getQueryData( [ + QUERY_ACCOUNT_PROTECTION_KEY, + ] ) as AccountProtectionStatus; + + // Optimistically update the Account Protection settings. + queryClient.setQueryData( + [ QUERY_ACCOUNT_PROTECTION_KEY ], + ( accountProtectionStatus: AccountProtectionStatus ) => ( { + ...accountProtectionStatus, + isEnabled: ! initialValue.isEnabled, + } ) + ); + + return { initialValue }; + }, + onSuccess: () => { + showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); + }, + onError: () => { + showErrorNotice( __( 'Error savings changes.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts deleted file mode 100644 index 10fa874cb1ebb..0000000000000 --- a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-mutation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; -import { __ } from '@wordpress/i18n'; -import API from '../../api'; -import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; -import useNotices from '../../hooks/use-notices'; - -/** - * Toggle Account Protection Mutatation - * - * @return {UseMutationResult} useMutation result. - */ -export default function useToggleAccountProtectMutation(): UseMutationResult { - const queryClient = useQueryClient(); - const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); - - return useMutation( { - mutationFn: API.toggleAccountProtection, - onMutate: () => { - showSavingNotice(); - // Get the current account protection settings. - const initialValue = queryClient.getQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ] ); - - // Optimistically update settings. - queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], ( status: boolean ) => ! status ); - - return { initialValue }; - }, - onSuccess: () => { - showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); - }, - onError: () => { - showErrorNotice( - __( 'An error occurred toggling the account protection module.', 'jetpack-protect' ) - ); - }, - onSettled: () => { - queryClient.invalidateQueries( { queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ] } ); - }, - } ); -} diff --git a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx new file mode 100644 index 0000000000000..90e473c270bc6 --- /dev/null +++ b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx @@ -0,0 +1,59 @@ +import { useCallback } from 'react'; +import useAccountProtectionMutation from '../../data/account-protection/use-account-protection-mutation'; +import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; +import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-module-mutation'; +import useAnalyticsTracks from '../use-analytics-tracks'; + +/** + * Use Account Protection Data Hook + * + * @return {object} Account Protection data and methods for interacting with it. + */ +const useAccountProtectionData = () => { + const { recordEvent } = useAnalyticsTracks(); + const { data: accountProtection } = useAccountProtectionQuery(); + const accountProtectionMutation = useAccountProtectionMutation(); + const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); + + /** + * Toggle Account Protection Module + * + * Flips the switch on the Account Protection module, and then refreshes the data. + */ + const toggleAccountProtection = useCallback( async () => { + toggleAccountProtectionMutation.mutate(); + }, [ toggleAccountProtectionMutation ] ); + + /** + * Toggle Strict Mode + * + * Flips the switch on the strict mode option, and then refreshes the data. + */ + const toggleStrictMode = useCallback( async () => { + const value = ! accountProtection.settings.jetpackAccountProtectionStrictMode; + const mutationObj = { jetpack_account_protection_strict_mode: value }; + if ( ! value ) { + mutationObj.jetpack_account_protection_strict_mode = false; + } + await accountProtectionMutation.mutateAsync( mutationObj ); + recordEvent( + mutationObj + ? 'jetpack_account_protection_strict_mode_enabled' + : 'jetpack_account_protection_strict_mode_disabled' + ); + }, [ + recordEvent, + accountProtection.settings.jetpackAccountProtectionStrictMode, + accountProtectionMutation, + ] ); + + return { + ...accountProtection, + isUpdating: accountProtectionMutation.isPending, + isToggling: toggleAccountProtectionMutation.isPending, + toggleAccountProtection, + toggleStrictMode, + }; +}; + +export default useAccountProtectionData; diff --git a/projects/plugins/protect/src/js/routes/settings/index.jsx b/projects/plugins/protect/src/js/routes/settings/index.jsx index edbed69dc3bda..3fe6b735f7d3e 100644 --- a/projects/plugins/protect/src/js/routes/settings/index.jsx +++ b/projects/plugins/protect/src/js/routes/settings/index.jsx @@ -8,18 +8,22 @@ import { import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon, warning } from '@wordpress/icons'; -import { useCallback } from 'react'; import AdminPage from '../../components/admin-page'; -import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; -import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-mutation'; +import useAccountProtectionData from '../../hooks/use-account-protection-data'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import usePlan from '../../hooks/use-plan'; import styles from './styles.module.scss'; const SettingsPage = () => { const { hasPlan } = usePlan(); - const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); - const { data: isAccountProtectionEnabled } = useAccountProtectionQuery(); + const { + settings: { jetpackAccountProtectionStrictMode: strictMode }, + isEnabled: isAccountProtectionEnabled, + toggleAccountProtection, + toggleStrictMode, + isToggling, + isUpdating, + } = useAccountProtectionData(); // Track view for Protect Account Protection page. useAnalyticsTracks( { @@ -29,22 +33,13 @@ const SettingsPage = () => { }, } ); - /** - * Toggle Account Protection Module - * - * Flips the switch on the Account Protection module, and then refreshes the data. - */ - const toggleAccountProtection = useCallback( async () => { - toggleAccountProtectionMutation.mutate(); - }, [ toggleAccountProtectionMutation ] ); - const accountProtectionSettings = (
@@ -70,9 +65,9 @@ const SettingsPage = () => {
diff --git a/projects/plugins/protect/src/js/types/account-protection.ts b/projects/plugins/protect/src/js/types/account-protection.ts new file mode 100644 index 0000000000000..37d557638982b --- /dev/null +++ b/projects/plugins/protect/src/js/types/account-protection.ts @@ -0,0 +1,12 @@ +export type AccountProtectionStatus = { + /** Whether the "account-protection" module is enabled. */ + isEnabled: boolean; + + /** The current Account Protetion settings. */ + settings: AccountProtectionSettings; +}; + +export type AccountProtectionSettings = { + /** Whether the user has enabled strict mode. */ + jetpackAccountProtectionStrictMode: boolean; +}; From b72e93a95209873de638ff21084ceb778b47f7f3 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 15 Jan 2025 12:36:24 -0800 Subject: [PATCH 018/120] Reorg and add kill switch and is supported check --- .../src/class-account-protection.php | 200 +++++------------- .../src/class-password-detection.php | 132 ++++++++++++ .../src/class-password-reset-email.php | 57 +++++ 3 files changed, 238 insertions(+), 151 deletions(-) create mode 100644 projects/packages/account-protection/src/class-password-reset-email.php diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 63187c7724381..863c5c5e0aff5 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -26,39 +26,43 @@ public static function init() { add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); + // Do not run in unsupported environments + add_action( 'jetpack_get_available_modules', __CLASS__ . '::remove_module_on_unsupported_environments' ); + add_action( 'jetpack_get_available_standalone_modules', __CLASS__ . '::remove_standalone_module_on_unsupported_environments' ); + // Register REST routes add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); if ( self::is_enabled() ) { // Validate password after successful login - add_action( 'wp_authenticate_user', __CLASS__ . '::login_form_password_detection', 10, 2 ); + add_action( 'wp_authenticate_user', array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'login_form_password_detection' ), 10, 2 ); // Add password detection flow for users with unsafe passwords add_action( 'login_form_password-detection', - __CLASS__ . '::render_password_detection_page', + array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'render_password_detection_page' ), 10, 2 ); - // Ensure jetpack_account_protection_password_status usermeta is removed on password change + // Remove password detection usermeta on password reset add_action( 'after_password_reset', function ( $user, $new_pass ) { - delete_user_meta( $user->ID, 'jetpack_account_protection_password_status' ); + Password_Detection::remove_password_detection_usermeta( $user->ID ); }, 10, 2 ); - // Ensure jetpack_account_protection_password_status usermeta is removed when user updates password via profile updates + // Remove password detection usermeta on profile password update add_action( 'profile_update', function ( $user_id, $old_user_data ) { // Profile updates should include validation, but we should reset user meta to be safe if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - // TODO: Only if the password is actually updated - self::remove_password_detection_usermeta( $user_id ); + // TODO: Ensure this only happens if the password is actually updated + Password_Detection::remove_password_detection_usermeta( $user_id ); } }, 10, @@ -80,8 +84,8 @@ public static function on_account_protection_deactivation() { // Remove user meta on deactivation $users = get_users(); foreach ( $users as $user ) { - self::remove_password_detection_usermeta( $user->ID ); - // TODO: Remove usermeta on plugin deactivation as well + Password_Detection::remove_password_detection_usermeta( $user->ID ); + // TODO: Remove usermeta on plugin deactivation } } @@ -121,173 +125,67 @@ public static function disable() { } /** - * Get the account protection settings. - * - * @return array - */ - public static function get_settings() { - $settings = array( - self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), - ); - - return $settings; - } - - /** - * Check if the password is safe after login. + * Determines if Account Protection is supported in the current environment. * - * @param WP_User $user The user object. - * @param string $password The password. - * @return WP_User The user object. + * @return bool */ - public static function login_form_password_detection( $user, $password ) { - if ( ! self::validate_password( $password ) ) { - // TODO: Ensure this usermeta is always up to date - update_user_meta( $user->ID, 'jetpack_account_protection_password_status', 'unsafe' ); - - // Redirect to the password detection page - add_filter( 'login_redirect', __CLASS__ . '::password_detection_redirect', 10, 3 ); - } else { - update_user_meta( $user->ID, 'jetpack_account_protection_password_status', 'safe' ); + public static function is_supported_environment() { + // Do not run when killswitch is enabled + if ( defined( 'DISABLE_JETPACK_ACCOUNT_PROTECTION' ) && DISABLE_JETPACK_ACCOUNT_PROTECTION ) { + return false; } - return $user; + return true; } /** - * Render password detection page. + * Disables the Account Protection module when on an unsupported platform in Jetpack. * - * This page is shown to users with unsafe passwords after login. + * @param array $modules Filterable value for `jetpack_get_available_modules`. * - * @return void + * @return array Array of module slugs. */ - public static function render_password_detection_page() { - // Restrict direct access to logged in users - if ( ! is_user_logged_in() ) { - wp_redirect( wp_login_url() ); - exit; - } - - $current_user = wp_get_current_user(); - $user_password_status = get_user_meta( $current_user->ID, 'jetpack_account_protection_password_status', true ); - - // Restrict direct access to users with unsafe passwords - if ( ! $user_password_status || 'safe' === $user_password_status ) { - wp_redirect( admin_url() ); - exit; + public static function remove_module_on_unsupported_environments( $modules ) { + if ( ! self::is_supported_environment() ) { + // Account protection should never be available on unsupported platforms. + unset( $modules['account-protection'] ); } - if ( isset( $_POST['reset'] ) ) { - $email = $current_user->user_email; - - // Send reset email to the user - only initially and on resend, not refresh - $email_sent = self::send_password_reset_email( $current_user, $email ); - if ( ! $email_sent ) { - // TODO: Handle email sending errors - } - - $header_title = 'Secure Your Account'; - $page_title = "Let's secure your account"; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . self::mask_email_address( $email ) . '. After that, we\'ll guide you through updating your password.

-

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; - - } elseif ( isset( $_POST['proceed'] ) ) { - wp_redirect( admin_url() ); - exit; - } else { - $header_title = 'Stay Secure'; - $page_title = 'Take action to stay secure'; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

It is highly recommended that you update your password.

-
-
- -
-
- -
-
-

Learn more about the risks of using weak passwords and how to protect your account.

'; - } - - include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; - exit; - } - - /** - * Password validation. - * - * @param string $password The password to validate. - * @return bool True if the password is valid, false otherwise. - */ - public static function validate_password( $password ) { - // TODO: Update to use custom password validation method when available. - return $password ? false : true; + return $modules; } /** - * Redirect to the password detection page. + * Disables the Account Protection module when on an unsupported platform in a standalone plugin. * - * @return string The URL to redirect to. - */ - public static function password_detection_redirect() { - return home_url( '/wp-login.php?action=password-detection' ); - } - - /** - * Mask an email address like d*****@g*****.com. + * @param array $modules Filterable value for `jetpack_get_available_standalone_modules`. * - * @param string $email The email address to mask. - * @return string The masked email address. + * @return array Array of module slugs. */ - public static function mask_email_address( $email ) { - $parts = explode( '@', $email ); - $name = $parts[0]; - $domain = $parts[1]; - - // Mask the name part (first letter + asterisks) - $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); + public static function remove_standalone_module_on_unsupported_environments( $modules ) { + if ( ! self::is_supported_environment() ) { + // Account Protection should never be available on unsupported platforms. + $modules = array_filter( + $modules, + function ( $module ) { + return $module !== 'account-protection'; + } + ); - // Mask the domain part (first letter + asterisks + domain extension) - $domain_parts = explode( '.', $domain ); - $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; + } - return $masked_name . '@' . $masked_domain; + return $modules; } /** - * Send password reset email. + * Get the account protection settings. * - * @param WP_User $user The user object. - * @param string $email The user email. - * @return bool True if the email was sent successfully, false otherwise. + * @return array */ - public static function send_password_reset_email( $user, $email ) { - $site_url = home_url(); - $parsed_url = parse_url( $site_url ); - $domain_name = $parsed_url['host']; - $username = $user->user_login; - - $key = get_password_reset_key( $user ); - $locale = get_user_locale( $user ); - $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; - - // TODO: Update to use custom email method when available, passing $domain_name, $email, $username, and $password_reset_link - return true; - } + public static function get_settings() { + $settings = array( + self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), + ); - /** - * Remove the password detection usermeta. - * - * @param int $user_id The user ID. - */ - public static function remove_password_detection_usermeta( $user_id ) { - delete_user_meta( $user_id, 'jetpack_account_protection_password_status' ); + return $settings; } - - // TODO: Move password detection methods to a dedicated class - // TODO: Add killswitch define and is support env checks here } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 93050157d831e..dda8a0551a7e4 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -12,4 +12,136 @@ * Class Password_Detection */ class Password_Detection { + + const PASSWORD_DETECTION_USER_META_KEY = 'jetpack_account_protection_password_status'; + + /** + * Redirect to the password detection page. + * + * @return string The URL to redirect to. + */ + public static function password_detection_redirect() { + return home_url( '/wp-login.php?action=password-detection' ); + } + + /** + * Check if the password is safe after login. + * + * @param WP_User $user The user object. + * @param string $password The password. + * @return WP_User The user object. + */ + public static function login_form_password_detection( $user, $password ) { + if ( ! self::validate_password( $password ) ) { + // TODO: Ensure this usermeta is always up to date + self::add_password_detection_usermeta( $user->ID, 'unsafe' ); + + // Redirect to the password detection page + add_filter( 'login_redirect', __CLASS__ . '::password_detection_redirect', 10, 3 ); + } else { + self::add_password_detection_usermeta( $user->ID, 'safe' ); + + } + + return $user; + } + + /** + * Render password detection page. + * + * This page is shown to users with unsafe passwords after login. + * + * @return void + */ + public static function render_password_detection_page() { + // Restrict direct access to logged in users + if ( ! is_user_logged_in() ) { + wp_redirect( wp_login_url() ); + exit; + } + + $current_user = wp_get_current_user(); + $user_password_status = self::get_password_detection_usermeta( $current_user->ID ); + + // Restrict direct access to users with unsafe passwords + if ( ! $user_password_status || 'safe' === $user_password_status ) { + wp_redirect( admin_url() ); + exit; + } + + if ( isset( $_POST['reset'] ) ) { + $email = $current_user->user_email; + + // Send reset email to the user - only initially and on resend, not refresh + $email_sent = Password_Reset_Email::send( $current_user, $email ); + if ( ! $email_sent ) { + // TODO: Handle email sending errors + } + + $header_title = 'Secure Your Account'; + $page_title = "Let's secure your account"; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . Password_Reset_Email::mask_email_address( $email ) . '. After that, we\'ll guide you through updating your password.

+

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; + + } elseif ( isset( $_POST['proceed'] ) ) { + wp_redirect( admin_url() ); + exit; + } else { + $header_title = 'Stay Secure'; + $page_title = 'Take action to stay secure'; + $content = ' +

Your current password was found in a public leak, which means your account might be at risk.

+

It is highly recommended that you update your password.

+
+
+ +
+
+ +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

'; + } + + include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; + exit; + } + + /** + * Password validation. + * + * @param string $password The password to validate. + * @return bool True if the password is valid, false otherwise. + */ + public static function validate_password( $password ) { + // TODO: Update to use custom password validation method(s) when available. + return $password ? false : true; + } + + /** + * Add the password detection usermeta. + * + * @param int $user_id The user ID. + */ + public static function add_password_detection_usermeta( $user_id, $setting ) { + update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); + } + + /** + * Remove the password detection usermeta. + * + * @param int $user_id The user ID. + */ + public static function remove_password_detection_usermeta( $user_id ) { + delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); + } + + /** + * Get the password detection usermeta. + */ + public static function get_password_detection_usermeta( $user_id ) { + return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); + } } diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-password-reset-email.php new file mode 100644 index 0000000000000..e395394a82da3 --- /dev/null +++ b/projects/packages/account-protection/src/class-password-reset-email.php @@ -0,0 +1,57 @@ +user_login; + + $key = get_password_reset_key( $user ); + $locale = get_user_locale( $user ); + $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; + + // TODO: Update to use custom email method when available, passing $domain_name, $email, $username, and $password_reset_link + return true; + } +} From 7fad7f98f727bdee66fa4d7f1aaee227bb368196 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 15 Jan 2025 13:59:31 -0800 Subject: [PATCH 019/120] Add testing infrastructure --- .../account-protection/tests/.phpcs.dir.xml | 4 ---- .../tests/action-test-coverage.sh | 9 ++++++++ .../tests/php/integration/bootstrap.php | 16 ++++++++++++++ .../tests/php/integration/phpunit.xml.dist | 21 +++++++++++++++++++ .../tests/php/{ => unit}/bootstrap.php | 2 +- .../tests/php/unit/phpunit.xml.dist | 21 +++++++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) delete mode 100644 projects/packages/account-protection/tests/.phpcs.dir.xml create mode 100755 projects/packages/account-protection/tests/action-test-coverage.sh create mode 100644 projects/packages/account-protection/tests/php/integration/bootstrap.php create mode 100644 projects/packages/account-protection/tests/php/integration/phpunit.xml.dist rename projects/packages/account-protection/tests/php/{ => unit}/bootstrap.php (64%) create mode 100644 projects/packages/account-protection/tests/php/unit/phpunit.xml.dist diff --git a/projects/packages/account-protection/tests/.phpcs.dir.xml b/projects/packages/account-protection/tests/.phpcs.dir.xml deleted file mode 100644 index 46951fe77b37e..0000000000000 --- a/projects/packages/account-protection/tests/.phpcs.dir.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/projects/packages/account-protection/tests/action-test-coverage.sh b/projects/packages/account-protection/tests/action-test-coverage.sh new file mode 100755 index 0000000000000..8a7a1e9de6565 --- /dev/null +++ b/projects/packages/account-protection/tests/action-test-coverage.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -veo pipefail + +EXIT=0 +php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php "$COVERAGE_DIR/integration/php.cov" --configuration tests/php/integration/phpunit.xml.dist || EXIT=1 +php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php "$COVERAGE_DIR/unit/php.cov" --configuration tests/php/unit/phpunit.xml.dist || EXIT=1 + +exit $EXIT diff --git a/projects/packages/account-protection/tests/php/integration/bootstrap.php b/projects/packages/account-protection/tests/php/integration/bootstrap.php new file mode 100644 index 0000000000000..4c1205f352a1d --- /dev/null +++ b/projects/packages/account-protection/tests/php/integration/bootstrap.php @@ -0,0 +1,16 @@ + + + + + + + ../../../src + + + + + . + + + diff --git a/projects/packages/account-protection/tests/php/bootstrap.php b/projects/packages/account-protection/tests/php/unit/bootstrap.php similarity index 64% rename from projects/packages/account-protection/tests/php/bootstrap.php rename to projects/packages/account-protection/tests/php/unit/bootstrap.php index 46763b04a2cdb..e16bad0ecf0bf 100644 --- a/projects/packages/account-protection/tests/php/bootstrap.php +++ b/projects/packages/account-protection/tests/php/unit/bootstrap.php @@ -8,4 +8,4 @@ /** * Include the composer autoloader. */ -require_once __DIR__ . '/../../vendor/autoload.php'; +require_once __DIR__ . '/../../../vendor/autoload.php'; diff --git a/projects/packages/account-protection/tests/php/unit/phpunit.xml.dist b/projects/packages/account-protection/tests/php/unit/phpunit.xml.dist new file mode 100644 index 0000000000000..e901dd1acf030 --- /dev/null +++ b/projects/packages/account-protection/tests/php/unit/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + + + ../../../src + + + + + . + + + From 39a28d5998299ec063e45e57cdfabcd7141a3410 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 16 Jan 2025 12:54:51 -0800 Subject: [PATCH 020/120] Add email handlings, resend AJAX action, and attempt limitations --- .../src/class-account-protection.php | 22 +++- .../src/class-password-detection.php | 116 ++++++++++++------ .../src/css/password-detection.css | 9 +- .../src/js/resend-password-reset.js | 71 +++++++++++ .../templates/password-detection-template.php | 43 +++++-- 5 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 projects/packages/account-protection/src/js/resend-password-reset.js diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 863c5c5e0aff5..a20cd51388aa5 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -45,10 +45,13 @@ public static function init() { 2 ); + // Register AJAX resend password reset email action + add_action( 'wp_ajax_resend_password_reset', array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'ajax_resend_password_reset_email' ) ); + // Remove password detection usermeta on password reset add_action( 'after_password_reset', - function ( $user, $new_pass ) { + function ( $user ) { Password_Detection::remove_password_detection_usermeta( $user->ID ); }, 10, @@ -58,16 +61,23 @@ function ( $user, $new_pass ) { // Remove password detection usermeta on profile password update add_action( 'profile_update', - function ( $user_id, $old_user_data ) { - // Profile updates should include validation, but we should reset user meta to be safe - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - // TODO: Ensure this only happens if the password is actually updated - Password_Detection::remove_password_detection_usermeta( $user_id ); + function ( $user_id ) { + // TODO: Ensure nonce verfication works as expected + if ( + ! empty( $_POST['_wpnonce'] ) && + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) + ) { + // Profile updates should include validation, but we should reset user meta to be safe + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + // TODO: Ensure this only happens if the password is actually updated + Password_Detection::remove_password_detection_usermeta( $user_id ); + } } }, 10, 2 ); + } } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index dda8a0551a7e4..9b1a55968ce38 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -1,5 +1,4 @@ ID ) { + wp_safe_redirect( wp_login_url() ); exit; } - $current_user = wp_get_current_user(); - $user_password_status = self::get_password_detection_usermeta( $current_user->ID ); - // Restrict direct access to users with unsafe passwords + $user_password_status = self::get_password_detection_usermeta( $current_user->ID ); if ( ! $user_password_status || 'safe' === $user_password_status ) { - wp_redirect( admin_url() ); + wp_safe_redirect( admin_url() ); exit; } + add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_password_detection_styles' ); + + // Use a transient to track email sent status + $transient_key = 'password_reset_email_sent_' . $current_user->ID; + $email_sent_flag = get_transient( $transient_key ); + + // TODO: Add nonce verification if ( isset( $_POST['reset'] ) ) { + $reset = true; $email = $current_user->user_email; - // Send reset email to the user - only initially and on resend, not refresh - $email_sent = Password_Reset_Email::send( $current_user, $email ); - if ( ! $email_sent ) { - // TODO: Handle email sending errors + // Send reset email + if ( ! $email_sent_flag ) { + $email_sent = Password_Reset_Email::send( $current_user, $email ); + if ( $email_sent ) { + // Set transient to mark the email as sent + set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); + } + // TODO: Handle email sending failure } - $header_title = 'Secure Your Account'; - $page_title = "Let's secure your account"; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

Don\'t worry - To keep your account safe, we\'ve sent a verification email to a ' . Password_Reset_Email::mask_email_address( $email ) . '. After that, we\'ll guide you through updating your password.

-

Please check your inbox and click the link to verify it\'s you. Didn\'t get the email? Resend email

'; - + add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_resend_password_reset_scripts' ); + // TODO: Add nonce verification } elseif ( isset( $_POST['proceed'] ) ) { - wp_redirect( admin_url() ); + wp_safe_redirect( admin_url() ); exit; - } else { - $header_title = 'Stay Secure'; - $page_title = 'Take action to stay secure'; - $content = ' -

Your current password was found in a public leak, which means your account might be at risk.

-

It is highly recommended that you update your password.

-
-
- -
-
- -
-
-

Learn more about the risks of using weak passwords and how to protect your account.

'; } include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; exit; } + /** + * Enqueue the resend password reset email scripts. + */ + public static function enqueue_resend_password_reset_scripts() { + wp_enqueue_script( 'resend-password-reset', plugin_dir_url( __FILE__ ) . 'js/resend-password-reset.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); + + // Pass AJAX URL and nonce to the script + wp_localize_script( + 'resend-password-reset', + 'ajaxObject', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'resend_password_reset_nonce' ), + ) + ); + } + + /** + * Enqueue the password detection page styles. + */ + public static function enqueue_password_detection_styles() { + wp_enqueue_style( + 'password-detection-styles', + plugin_dir_url( __FILE__ ) . 'css/password-detection.css', + array(), + Account_Protection::PACKAGE_VERSION + ); + } + + /** + * Run AJAX request to resend password reset email. + */ + public static function ajax_resend_password_reset_email() { + // Verify the nonce for security + check_ajax_referer( 'resend_password_reset_nonce', 'security' ); + + // Check if the user is logged in + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => 'User not authenticated' ) ); + } + + $current_user = wp_get_current_user(); + $email = $current_user->user_email; + + // Resend the email + $email_sent = Password_Reset_Email::send( $current_user, $email ); + if ( $email_sent ) { + wp_send_json_success( array( 'message' => 'Resend successful.' ) ); + } else { + wp_send_json_error( array( 'message' => 'Resend failed. ' ) ); + } + } + /** * Password validation. * @@ -123,7 +166,8 @@ public static function validate_password( $password ) { /** * Add the password detection usermeta. * - * @param int $user_id The user ID. + * @param int $user_id The user ID. + * @param string $setting The password detection setting. */ public static function add_password_detection_usermeta( $user_id, $setting ) { update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); @@ -140,6 +184,8 @@ public static function remove_password_detection_usermeta( $user_id ) { /** * Get the password detection usermeta. + * + * @param int $user_id The user ID. */ public static function get_password_detection_usermeta( $user_id ) { return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); diff --git a/projects/packages/account-protection/src/css/password-detection.css b/projects/packages/account-protection/src/css/password-detection.css index 00d9203ea32c8..d1ec425dd5da3 100644 --- a/projects/packages/account-protection/src/css/password-detection.css +++ b/projects/packages/account-protection/src/css/password-detection.css @@ -1,5 +1,5 @@ -body { - background: #f0f0f1; +.password-detection-wrapper { + background-color: #f0f0f1; min-width: 0; color: #3c434a; font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; @@ -7,19 +7,18 @@ body { line-height: 1.4; } -.custom { +.password-detection { background: #fff; width: 420px; margin: 124px auto; padding: 26px 24px; font-weight: 400; overflow: hidden; - background: #fff; border: 1px solid #c3c4c7; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); } -.custom-title { +.password-detection-title { font-size: 24px; font-weight: 500; } diff --git a/projects/packages/account-protection/src/js/resend-password-reset.js b/projects/packages/account-protection/src/js/resend-password-reset.js new file mode 100644 index 0000000000000..2e0cdf8a4ab0a --- /dev/null +++ b/projects/packages/account-protection/src/js/resend-password-reset.js @@ -0,0 +1,71 @@ +/* global jQuery, ajaxObject */ +( function ( $ ) { + $( document ).ready( function () { + const attemptLimit = 3; + let attempts = 0; + + $( '#resend-password-reset' ).on( 'click', function ( e ) { + e.preventDefault(); // Prevent the default action + + const message = $( '#resend-password-reset-message' ); + const button = $( this ); + + // Store the original text of the message + const originalMessageText = message.text(); + + // Update message and hide button while resending + message.text( 'Resending email...' ); + button.hide(); + + attempts++; + + // Perform the AJAX request + $.ajax( { + url: ajaxObject.ajax_url, + type: 'POST', + data: { + action: 'resend_password_reset', + security: ajaxObject.nonce, + }, + success: function ( response ) { + if ( response.success ) { + // Show success message + message.text( response.data.message ).show(); + + // Hide the status message and show the button after 5 seconds + setTimeout( function () { + let messageText = originalMessageText; + if ( attempts < attemptLimit ) { + button.show(); + } else { + messageText += 'Please try again later.'; + } + message.text( messageText ).show(); + }, 5000 ); + } else { + // Show error message + let messageText = 'An error occurred. '; + if ( attempts < attemptLimit ) { + button.text( 'Please try again' ).show(); + } else { + messageText += 'Please contact support.'; // TODO: Add support redirect + } + + message.text( messageText ).show(); + } + }, + error: function () { + // Show error message + let messageText = 'An error occurred. '; + if ( attempts < attemptLimit ) { + button.text( 'Please try again' ).show(); + } else { + messageText += 'Please contact support.'; // TODO: Add support redirect + } + + message.text( messageText ).show(); + }, + } ); + } ); + } ); +} )( jQuery ); diff --git a/projects/packages/account-protection/src/templates/password-detection-template.php b/projects/packages/account-protection/src/templates/password-detection-template.php index 748663cb8460c..ec3651384bf67 100644 --- a/projects/packages/account-protection/src/templates/password-detection-template.php +++ b/projects/packages/account-protection/src/templates/password-detection-template.php @@ -1,7 +1,16 @@ @@ -9,14 +18,32 @@ - <?php echo 'Jetpack - ' . esc_html( $header_title ); ?> - + <?php echo 'Jetpack - ' . $reset ? 'Stay Secure' : 'Secure Your Account'; ?> + - -
+ +
-

- +

+ +

+

Don't worry - To keep your account safe, we've sent a verification email to a user_email ) ); ?>. After that, we'll guide you through updating your password.

+

Please check your inbox and click the link to verify it's you.

+

Didn't get the email? Resend email

+ +

+

It is highly recommended that you update your password.

+
+
+ +
+
+ +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

+
+ -'; \ No newline at end of file + From da85a185dda50d59b77cb1b0f906a8a54a7c0a85 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 16 Jan 2025 19:13:22 -0800 Subject: [PATCH 021/120] Add nonces, checks and template error handling --- .../src/class-account-protection.php | 2 - .../src/class-password-detection.php | 118 +++++++++++++++--- .../src/class-password-reset-email.php | 21 ++-- .../templates/password-detection-template.php | 26 +++- 4 files changed, 130 insertions(+), 37 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index a20cd51388aa5..213c17cd0839a 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -62,14 +62,12 @@ function ( $user ) { add_action( 'profile_update', function ( $user_id ) { - // TODO: Ensure nonce verfication works as expected if ( ! empty( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) ) { // Profile updates should include validation, but we should reset user meta to be safe if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - // TODO: Ensure this only happens if the password is actually updated Password_Detection::remove_password_detection_usermeta( $user_id ); } } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 9b1a55968ce38..acadc69749f1f 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -67,35 +67,55 @@ public static function render_password_detection_page() { exit; } - add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_password_detection_styles' ); - // Use a transient to track email sent status $transient_key = 'password_reset_email_sent_' . $current_user->ID; $email_sent_flag = get_transient( $transient_key ); - // TODO: Add nonce verification - if ( isset( $_POST['reset'] ) ) { + // Initialize template variables + $reset = false; + $context = 'Your current password was found in a public leak, which means your account might be at risk.'; + $error = ''; + + add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_password_detection_styles' ); + + // Handle reset_password_action form submission + if ( isset( $_POST['reset-password'] ) ) { $reset = true; - $email = $current_user->user_email; - - // Send reset email - if ( ! $email_sent_flag ) { - $email_sent = Password_Reset_Email::send( $current_user, $email ); - if ( $email_sent ) { - // Set transient to mark the email as sent - set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); + + // Verify nonce + if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) { + // Send password reset email + if ( ! $email_sent_flag ) { + $email_sent = Password_Reset_Email::send( $current_user ); + if ( $email_sent ) { + // Set transient to mark the email as sent + set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); + } else { + $error = 'email_send_error'; + } } - // TODO: Handle email sending failure + + add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_resend_password_reset_scripts' ); + } else { + $error = 'reset_passowrd_nonce_verification_error'; } - add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_resend_password_reset_scripts' ); - // TODO: Add nonce verification + // Handle proceed_action form submission } elseif ( isset( $_POST['proceed'] ) ) { - wp_safe_redirect( admin_url() ); - exit; + $reset = true; + + // Verify nonce + if ( isset( $_POST['_wpnonce_proceed'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_proceed'] ) ), 'proceed_action' ) ) { + wp_safe_redirect( admin_url() ); + exit; + } else { + $error = 'proceed_nonce_verification_error'; + } } - include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; + // TODO: Remove this once we decide which template approach is best + // include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; + self::render_password_detection_template( $reset, $context, $error, Password_Reset_Email::mask_email_address( $current_user->user_email ) ); exit; } @@ -190,4 +210,66 @@ public static function remove_password_detection_usermeta( $user_id ) { public static function get_password_detection_usermeta( $user_id ) { return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); } + + /** + * Render template for password detection page. + * + * @param bool $reset Whether the user is resetting their password. + * @param string $context The context for the password detection page. + * @param string $error The error message to display. + * @param string $masked_email The masked email address. + */ + public static function render_password_detection_template( $reset, $context, $error, $masked_email ) { + ?> + + + + + + <?php echo 'Jetpack - ' . $reset ? 'Stay Secure' : 'Secure Your Account'; ?> + + + +
+ +

+ +

+ + +

We've encountered an issue verifying your request to proceed without updating your password.

+ +

While attempting to send a verification email to , an error occurred.

+ + +

Don't worry - To keep your account safe, we've sent a verification email to . After that, we'll guide you through updating your password.

+ + +

Click here to be redirected to your admin dashboard.

+ +

Please check your inbox and click the link to verify it's you. Alternatively, you can update your password from your account profile.

+

Didn't get the email? Resend email

+ + +

+

It is highly recommended that you update your password.

+
+
+ + +
+
+ + +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

+ +
+ + + + user_login; + public static function send( $user ) { + // $site_url = home_url(); + // $parsed_url = wp_parse_url( $site_url ); + // $domain_name = $parsed_url['host']; + // $username = $user->user_login; + // $email = $user->user_email; - $key = get_password_reset_key( $user ); - $locale = get_user_locale( $user ); - $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; + // $key = get_password_reset_key( $user ); + // $locale = get_user_locale( $user ); + // $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; // TODO: Update to use custom email method when available, passing $domain_name, $email, $username, and $password_reset_link - return true; + return $user ? true : false; } } diff --git a/projects/packages/account-protection/src/templates/password-detection-template.php b/projects/packages/account-protection/src/templates/password-detection-template.php index ec3651384bf67..a853c08623b7f 100644 --- a/projects/packages/account-protection/src/templates/password-detection-template.php +++ b/projects/packages/account-protection/src/templates/password-detection-template.php @@ -9,8 +9,8 @@ use Automattic\Jetpack\Account_Protection\Password_Reset_Email; -$reset = isset( $reset ) ? $reset : false; -$context = isset( $context ) ? $context : 'Your current password was found in a public leak, which means your account might be at risk.'; +$masked_email = Password_Reset_Email::mask_email_address( $email ); + ?> @@ -27,17 +27,31 @@

-

Don't worry - To keep your account safe, we've sent a verification email to a user_email ) ); ?>. After that, we'll guide you through updating your password.

-

Please check your inbox and click the link to verify it's you.

-

Didn't get the email? Resend email

+ + +

We've encountered an issue verifying your request to proceed without updating your password.

+ +

While attempting to send a verification email to , an error occurred.

+ + +

Don't worry - To keep your account safe, we've sent a verification email to . After that, we'll guide you through updating your password.

+ + +

Click here to be redirected to your admin dashboard.

+ +

Please check your inbox and click the link to verify it's you. Alternatively, you can update your password from your account profile.

+

Didn't get the email? Resend email

+

It is highly recommended that you update your password.

- + +
+
From 6a43ec0e23ae78c09a537f997fc3803ab86b230a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 16 Jan 2025 19:31:09 -0800 Subject: [PATCH 022/120] Use method over template to avoid lint errors --- .../src/class-password-detection.php | 112 ++++++++++-------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index acadc69749f1f..bcc01faacb4b4 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -113,7 +113,7 @@ public static function render_password_detection_page() { } } - // TODO: Remove this once we decide which template approach is best + // TODO: Remove this once we decide which template approach is best - ensure the functionality is 1:1 // include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; self::render_password_detection_template( $reset, $context, $error, Password_Reset_Email::mask_email_address( $current_user->user_email ) ); exit; @@ -220,56 +220,64 @@ public static function get_password_detection_usermeta( $user_id ) { * @param string $masked_email The masked email address. */ public static function render_password_detection_template( $reset, $context, $error, $masked_email ) { - ?> - - - - - - <?php echo 'Jetpack - ' . $reset ? 'Stay Secure' : 'Secure Your Account'; ?> - - - -
- -

- -

- - -

We've encountered an issue verifying your request to proceed without updating your password.

- -

While attempting to send a verification email to , an error occurred.

- - -

Don't worry - To keep your account safe, we've sent a verification email to . After that, we'll guide you through updating your password.

- - -

Click here to be redirected to your admin dashboard.

- -

Please check your inbox and click the link to verify it's you. Alternatively, you can update your password from your account profile.

-

Didn't get the email? Resend email

- - -

-

It is highly recommended that you update your password.

-
-
- - -
-
- - -
-
-

Learn more about the risks of using weak passwords and how to protect your account.

- -
- - - - '; + echo ''; + echo ''; + echo ''; + echo ''; + echo '' . esc_html( $reset ? 'Jetpack - Stay Secure' : 'Jetpack - Secure Your Account' ) . ''; + wp_head(); + echo ''; + echo ''; + echo '
'; + + // Include Jetpack logo + require plugin_dir_path( __FILE__ ) . '/assets/jetpack-logo.php'; + + echo '

' . esc_html( $reset ? 'Take action to stay secure' : "Let's secure your account" ) . '

'; + + if ( $reset ) { + echo '

' . esc_html( $context ) . '

'; + if ( $error ) { + if ( 'proceed_nonce_verification_error' === $error ) { + echo '

We\'ve encountered an issue verifying your request to proceed without updating your password.

'; + } else { + echo '

'; + echo 'reset_password_nonce_verification_error' === $error + ? "We've encountered an issue verifying your request to create a new password. " + : ''; + echo 'While attempting to send a verification email to ' . esc_html( $masked_email ) . ', an error occurred.'; + echo '

'; + } + } else { + echo '

Don\'t worry - To keep your account safe, we\'ve sent a verification email to ' . esc_html( $masked_email ) . '. After that, we\'ll guide you through updating your password.

'; + } + echo '

Please check your inbox and click the link to verify it\'s you. Alternatively, you can update your password from your account profile.

'; + echo '

'; + echo 'Didn\'t get the email? '; + echo 'Resend email'; + echo '

'; + } else { + echo '

' . esc_html( $context ) . '

'; + echo '

It is highly recommended that you update your password.

'; + echo '
'; + echo '
'; + wp_nonce_field( 'reset_password_action', '_wpnonce_reset_password' ); + echo ''; + echo '
'; + echo '
'; + wp_nonce_field( 'proceed_action', '_wpnonce_proceed' ); + echo ''; + echo '
'; + echo '
'; + echo '

Learn more about the risks of using weak passwords and how to protect your account.

'; + } + + echo '
'; + wp_footer(); + echo ''; + echo ''; } } From 755705697e083ae33ec5e6342b4b6fa561c51bdd Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 09:04:06 -0800 Subject: [PATCH 023/120] Improve render_password_detection_template, update SVG file ext --- .../{jetpack-logo.php => jetpack-logo.svg} | 0 .../src/class-password-detection.php | 114 +++++++++--------- .../templates/password-detection-template.php | 2 +- 3 files changed, 57 insertions(+), 59 deletions(-) rename projects/packages/account-protection/src/assets/{jetpack-logo.php => jetpack-logo.svg} (100%) diff --git a/projects/packages/account-protection/src/assets/jetpack-logo.php b/projects/packages/account-protection/src/assets/jetpack-logo.svg similarity index 100% rename from projects/packages/account-protection/src/assets/jetpack-logo.php rename to projects/packages/account-protection/src/assets/jetpack-logo.svg diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index bcc01faacb4b4..91db2d23ae9d0 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -221,63 +221,61 @@ public static function get_password_detection_usermeta( $user_id ) { */ public static function render_password_detection_template( $reset, $context, $error, $masked_email ) { defined( 'ABSPATH' ) || exit; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo '' . esc_html( $reset ? 'Jetpack - Stay Secure' : 'Jetpack - Secure Your Account' ) . ''; - wp_head(); - echo ''; - echo ''; - echo '
'; - - // Include Jetpack logo - require plugin_dir_path( __FILE__ ) . '/assets/jetpack-logo.php'; - - echo '

' . esc_html( $reset ? 'Take action to stay secure' : "Let's secure your account" ) . '

'; - - if ( $reset ) { - echo '

' . esc_html( $context ) . '

'; - if ( $error ) { - if ( 'proceed_nonce_verification_error' === $error ) { - echo '

We\'ve encountered an issue verifying your request to proceed without updating your password.

'; - } else { - echo '

'; - echo 'reset_password_nonce_verification_error' === $error - ? "We've encountered an issue verifying your request to create a new password. " - : ''; - echo 'While attempting to send a verification email to ' . esc_html( $masked_email ) . ', an error occurred.'; - echo '

'; - } - } else { - echo '

Don\'t worry - To keep your account safe, we\'ve sent a verification email to ' . esc_html( $masked_email ) . '. After that, we\'ll guide you through updating your password.

'; - } - echo '

Please check your inbox and click the link to verify it\'s you. Alternatively, you can update your password from your account profile.

'; - echo '

'; - echo 'Didn\'t get the email? '; - echo 'Resend email'; - echo '

'; - } else { - echo '

' . esc_html( $context ) . '

'; - echo '

It is highly recommended that you update your password.

'; - echo '
'; - echo '
'; - wp_nonce_field( 'reset_password_action', '_wpnonce_reset_password' ); - echo ''; - echo '
'; - echo '
'; - wp_nonce_field( 'proceed_action', '_wpnonce_proceed' ); - echo ''; - echo '
'; - echo '
'; - echo '

Learn more about the risks of using weak passwords and how to protect your account.

'; - } - - echo '
'; - wp_footer(); - echo ''; - echo ''; + ?> + + + + + + <?php echo esc_html( $reset ? 'Jetpack - Stay Secure' : 'Jetpack - Secure Your Account' ); ?> + + + +
+ +

+ +

+ + +

We've encountered an issue verifying your request to proceed without updating your password.

+ +

+ + While attempting to send a verification email to , an error occurred. +

+ + +

Don't worry - To keep your account safe, we've sent a verification email to . After that, we'll guide you through updating your password.

+ +

Please check your inbox and click the link to verify it's you. Alternatively, you can update your password from your account profile.

+

+ Didn't get the email? + Resend email +

+ +

+

It is highly recommended that you update your password.

+
+
+ + +
+
+ + +
+
+

Learn more about the risks of using weak passwords and how to protect your account.

+ +
+ + + +
- +

From 16845a7239e3cd50493f70598a64535b8e1f17d6 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 09:11:45 -0800 Subject: [PATCH 024/120] Remove template file and include --- .../src/class-password-detection.php | 2 - .../templates/password-detection-template.php | 63 ------------------- 2 files changed, 65 deletions(-) delete mode 100644 projects/packages/account-protection/src/templates/password-detection-template.php diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 91db2d23ae9d0..9cc0f7c39136f 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -113,8 +113,6 @@ public static function render_password_detection_page() { } } - // TODO: Remove this once we decide which template approach is best - ensure the functionality is 1:1 - // include plugin_dir_path( __FILE__ ) . 'templates/password-detection-template.php'; self::render_password_detection_template( $reset, $context, $error, Password_Reset_Email::mask_email_address( $current_user->user_email ) ); exit; } diff --git a/projects/packages/account-protection/src/templates/password-detection-template.php b/projects/packages/account-protection/src/templates/password-detection-template.php deleted file mode 100644 index d8ffb833391ec..0000000000000 --- a/projects/packages/account-protection/src/templates/password-detection-template.php +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - <?php echo 'Jetpack - ' . $reset ? 'Stay Secure' : 'Secure Your Account'; ?> - - - -
- -

- -

- - -

We've encountered an issue verifying your request to proceed without updating your password.

- -

While attempting to send a verification email to , an error occurred.

- - -

Don't worry - To keep your account safe, we've sent a verification email to . After that, we'll guide you through updating your password.

- - -

Click here to be redirected to your admin dashboard.

- -

Please check your inbox and click the link to verify it's you. Alternatively, you can update your password from your account profile.

-

Didn't get the email? Resend email

- - -

-

It is highly recommended that you update your password.

-
-
- - -
-
- - -
-
-

Learn more about the risks of using weak passwords and how to protect your account.

- -
- - - From 5f959f1ebb0287cce2af4b2cd1e9b13c7256e99c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 11:46:17 -0800 Subject: [PATCH 025/120] Prep for validation endpoints --- .../src/class-password-detection.php | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 9cc0f7c39136f..61dac3b16959f 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -7,6 +7,9 @@ namespace Automattic\Jetpack\Account_Protection; +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Connection\Manager as Connection_Manager; + /** * Class Password_Detection */ @@ -31,6 +34,16 @@ public static function password_detection_redirect() { * @return WP_User The user object. */ public static function login_form_password_detection( $user, $password ) { + // Check if the user is already a WP_Error object + if ( is_wp_error( $user ) ) { + return $user; + } + + // Ensure the password is correct for this user + if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { + return $user; + } + if ( ! self::validate_password( $password ) ) { // TODO: Ensure this usermeta is always up to date self::add_password_detection_usermeta( $user->ID, 'unsafe' ); @@ -177,10 +190,57 @@ public static function ajax_resend_password_reset_email() { * @return bool True if the password is valid, false otherwise. */ public static function validate_password( $password ) { - // TODO: Update to use custom password validation method(s) when available. + // TODO: Uncomment out once endpoint is live + // Check compromised and common passwords + // $weak_password = self::check_weak_passwords( $password ); + return $password ? false : true; } + /** + * Check if the password is in the list of common/compromised passwords. + * + * @param string $password The password to check. + * @return bool|WP_Error True if the password is in the list of common/compromised passwords, false otherwise. + */ + public static function check_weak_passwords( $password ) { + $api_url = '/jetpack-protect-weak-password'; + + $is_connected = ( new Connection_Manager() )->is_connected(); + + if ( ! $is_connected ) { + return new \WP_Error( 'site_not_connected' ); + } + + // Hash pass with sha1, and pass first 5 characters to the API + $hashed_password = sha1( $password ); + $password_prefix = substr( $hashed_password, 0, 5 ); + + $response = Client::wpcom_json_api_request_as_blog( + $api_url . '/' . $password_prefix, + '2', + array( 'method' => 'GET' ), + null, + 'wpcom' + ); + + $response_code = wp_remote_retrieve_response_code( $response ); + + if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { + return new \WP_Error( 'failed_fetching_weak_passwords', 'Failed to fetch weak passwords from the server', array( 'status' => $response_code ) ); + } + + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + + // Check if the password is in the list of common/compromised passwords + $password_suffix = substr( $hashed_password, 5 ); + if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { + return true; + } + + return false; + } + /** * Add the password detection usermeta. * From a4ba9593d71fe7dc1784e5a4d051b220cb547665 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 14:16:33 -0800 Subject: [PATCH 026/120] Update classes to be dynamic --- .../src/class-account-protection.php | 50 +++++++++---------- .../src/class-password-detection.php | 44 ++++++++-------- .../src/class-password-reset-email.php | 4 +- .../src/class-rest-controller.php | 22 ++++---- .../jetpack/modules/account-protection.php | 2 +- .../protect/src/class-jetpack-protect.php | 7 +-- .../protect/src/class-rest-controller.php | 9 ++-- 7 files changed, 70 insertions(+), 68 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 213c17cd0839a..42145fa3ede97 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -21,38 +21,38 @@ class Account_Protection { /** * Initializes the configurations needed for the account protection module. */ - public static function init() { + public function init() { // Account protection activation/deactivation hooks - add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); - add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); + add_action( 'jetpack_activate_module_account-protection', array( $this, 'on_account_protection_activation' ) ); + add_action( 'jetpack_deactivate_module_account-protection', array( $this, 'on_account_protection_deactivation' ) ); // Do not run in unsupported environments - add_action( 'jetpack_get_available_modules', __CLASS__ . '::remove_module_on_unsupported_environments' ); - add_action( 'jetpack_get_available_standalone_modules', __CLASS__ . '::remove_standalone_module_on_unsupported_environments' ); + add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) ); + add_action( 'jetpack_get_available_standalone_modules', array( $this, 'remove_standalone_module_on_unsupported_environments' ) ); // Register REST routes add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); - if ( self::is_enabled() ) { + if ( $this->is_enabled() ) { // Validate password after successful login - add_action( 'wp_authenticate_user', array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'login_form_password_detection' ), 10, 2 ); + add_action( 'wp_authenticate_user', array( new Password_Detection(), 'login_form_password_detection' ), 10, 2 ); // Add password detection flow for users with unsafe passwords add_action( 'login_form_password-detection', - array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'render_password_detection_page' ), + array( new Password_Detection(), 'render_password_detection_page' ), 10, 2 ); // Register AJAX resend password reset email action - add_action( 'wp_ajax_resend_password_reset', array( 'Automattic\Jetpack\Account_Protection\Password_Detection', 'ajax_resend_password_reset_email' ) ); + add_action( 'wp_ajax_resend_password_reset', array( new Password_Detection(), 'ajax_resend_password_reset_email' ) ); // Remove password detection usermeta on password reset add_action( 'after_password_reset', function ( $user ) { - Password_Detection::remove_password_detection_usermeta( $user->ID ); + ( new Password_Detection() )->remove_password_detection_usermeta( $user->ID ); }, 10, 2 @@ -68,7 +68,7 @@ function ( $user_id ) { ) { // Profile updates should include validation, but we should reset user meta to be safe if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - Password_Detection::remove_password_detection_usermeta( $user_id ); + ( new Password_Detection() )->remove_password_detection_usermeta( $user_id ); } } }, @@ -82,17 +82,17 @@ function ( $user_id ) { /** * Activate the account protection on module activation. */ - public static function on_account_protection_activation() { + public function on_account_protection_activation() { } /** * Deactivate the account protection on module deactivation. */ - public static function on_account_protection_deactivation() { + public function on_account_protection_deactivation() { // Remove user meta on deactivation $users = get_users(); foreach ( $users as $user ) { - Password_Detection::remove_password_detection_usermeta( $user->ID ); + ( new Password_Detection() )->remove_password_detection_usermeta( $user->ID ); // TODO: Remove usermeta on plugin deactivation } } @@ -102,7 +102,7 @@ public static function on_account_protection_deactivation() { * * @return bool */ - public static function is_enabled() { + public function is_enabled() { return ( new Modules() )->is_active( 'account-protection' ); } @@ -111,9 +111,9 @@ public static function is_enabled() { * * @return bool */ - public static function enable() { + public function enable() { // Return true if already enabled. - if ( self::is_enabled() ) { + if ( $this->is_enabled() ) { return true; } return ( new Modules() )->activate( 'account-protection', false, false ); @@ -124,9 +124,9 @@ public static function enable() { * * @return bool */ - public static function disable() { + public function disable() { // Return true if already disabled. - if ( ! self::is_enabled() ) { + if ( ! $this->is_enabled() ) { return true; } return ( new Modules() )->deactivate( 'account-protection' ); @@ -137,7 +137,7 @@ public static function disable() { * * @return bool */ - public static function is_supported_environment() { + public function is_supported_environment() { // Do not run when killswitch is enabled if ( defined( 'DISABLE_JETPACK_ACCOUNT_PROTECTION' ) && DISABLE_JETPACK_ACCOUNT_PROTECTION ) { return false; @@ -153,8 +153,8 @@ public static function is_supported_environment() { * * @return array Array of module slugs. */ - public static function remove_module_on_unsupported_environments( $modules ) { - if ( ! self::is_supported_environment() ) { + public function remove_module_on_unsupported_environments( $modules ) { + if ( ! $this->is_supported_environment() ) { // Account protection should never be available on unsupported platforms. unset( $modules['account-protection'] ); } @@ -169,8 +169,8 @@ public static function remove_module_on_unsupported_environments( $modules ) { * * @return array Array of module slugs. */ - public static function remove_standalone_module_on_unsupported_environments( $modules ) { - if ( ! self::is_supported_environment() ) { + public function remove_standalone_module_on_unsupported_environments( $modules ) { + if ( ! $this->is_supported_environment() ) { // Account Protection should never be available on unsupported platforms. $modules = array_filter( $modules, @@ -189,7 +189,7 @@ function ( $module ) { * * @return array */ - public static function get_settings() { + public function get_settings() { $settings = array( self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), ); diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 61dac3b16959f..0dcc178e1d2ba 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -22,7 +22,7 @@ class Password_Detection { * * @return string The URL to redirect to. */ - public static function password_detection_redirect() { + public function password_detection_redirect() { return home_url( '/wp-login.php?action=password-detection' ); } @@ -33,7 +33,7 @@ public static function password_detection_redirect() { * @param string $password The password. * @return WP_User The user object. */ - public static function login_form_password_detection( $user, $password ) { + public function login_form_password_detection( $user, $password ) { // Check if the user is already a WP_Error object if ( is_wp_error( $user ) ) { return $user; @@ -44,14 +44,14 @@ public static function login_form_password_detection( $user, $password ) { return $user; } - if ( ! self::validate_password( $password ) ) { + if ( ! $this->validate_password( $password ) ) { // TODO: Ensure this usermeta is always up to date - self::add_password_detection_usermeta( $user->ID, 'unsafe' ); + $this->add_password_detection_usermeta( $user->ID, 'unsafe' ); // Redirect to the password detection page - add_filter( 'login_redirect', __CLASS__ . '::password_detection_redirect', 10, 3 ); + add_filter( 'login_redirect', array( $this, 'password_detection_redirect' ), 10, 3 ); } else { - self::add_password_detection_usermeta( $user->ID, 'safe' ); + $this->add_password_detection_usermeta( $user->ID, 'safe' ); } @@ -65,7 +65,7 @@ public static function login_form_password_detection( $user, $password ) { * * @return void */ - public static function render_password_detection_page() { + public function render_password_detection_page() { // Restrict direct access to logged in users $current_user = wp_get_current_user(); if ( 0 === $current_user->ID ) { @@ -74,7 +74,7 @@ public static function render_password_detection_page() { } // Restrict direct access to users with unsafe passwords - $user_password_status = self::get_password_detection_usermeta( $current_user->ID ); + $user_password_status = $this->get_password_detection_usermeta( $current_user->ID ); if ( ! $user_password_status || 'safe' === $user_password_status ) { wp_safe_redirect( admin_url() ); exit; @@ -89,7 +89,7 @@ public static function render_password_detection_page() { $context = 'Your current password was found in a public leak, which means your account might be at risk.'; $error = ''; - add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_password_detection_styles' ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_password_detection_styles' ) ); // Handle reset_password_action form submission if ( isset( $_POST['reset-password'] ) ) { @@ -99,7 +99,7 @@ public static function render_password_detection_page() { if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) { // Send password reset email if ( ! $email_sent_flag ) { - $email_sent = Password_Reset_Email::send( $current_user ); + $email_sent = ( new Password_Reset_Email() )->send( $current_user ); if ( $email_sent ) { // Set transient to mark the email as sent set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); @@ -108,7 +108,7 @@ public static function render_password_detection_page() { } } - add_action( 'wp_enqueue_scripts', __CLASS__ . '::enqueue_resend_password_reset_scripts' ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resend_password_reset_scripts' ) ); } else { $error = 'reset_passowrd_nonce_verification_error'; } @@ -126,14 +126,14 @@ public static function render_password_detection_page() { } } - self::render_password_detection_template( $reset, $context, $error, Password_Reset_Email::mask_email_address( $current_user->user_email ) ); + $this->render_password_detection_template( $reset, $context, $error, ( new Password_Reset_Email() )->mask_email_address( $current_user->user_email ) ); exit; } /** * Enqueue the resend password reset email scripts. */ - public static function enqueue_resend_password_reset_scripts() { + public function enqueue_resend_password_reset_scripts() { wp_enqueue_script( 'resend-password-reset', plugin_dir_url( __FILE__ ) . 'js/resend-password-reset.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); // Pass AJAX URL and nonce to the script @@ -150,7 +150,7 @@ public static function enqueue_resend_password_reset_scripts() { /** * Enqueue the password detection page styles. */ - public static function enqueue_password_detection_styles() { + public function enqueue_password_detection_styles() { wp_enqueue_style( 'password-detection-styles', plugin_dir_url( __FILE__ ) . 'css/password-detection.css', @@ -162,7 +162,7 @@ public static function enqueue_password_detection_styles() { /** * Run AJAX request to resend password reset email. */ - public static function ajax_resend_password_reset_email() { + public function ajax_resend_password_reset_email() { // Verify the nonce for security check_ajax_referer( 'resend_password_reset_nonce', 'security' ); @@ -175,7 +175,7 @@ public static function ajax_resend_password_reset_email() { $email = $current_user->user_email; // Resend the email - $email_sent = Password_Reset_Email::send( $current_user, $email ); + $email_sent = ( new Password_Reset_Email() )->send( $current_user, $email ); if ( $email_sent ) { wp_send_json_success( array( 'message' => 'Resend successful.' ) ); } else { @@ -189,7 +189,7 @@ public static function ajax_resend_password_reset_email() { * @param string $password The password to validate. * @return bool True if the password is valid, false otherwise. */ - public static function validate_password( $password ) { + public function validate_password( $password ) { // TODO: Uncomment out once endpoint is live // Check compromised and common passwords // $weak_password = self::check_weak_passwords( $password ); @@ -203,7 +203,7 @@ public static function validate_password( $password ) { * @param string $password The password to check. * @return bool|WP_Error True if the password is in the list of common/compromised passwords, false otherwise. */ - public static function check_weak_passwords( $password ) { + public function check_weak_passwords( $password ) { $api_url = '/jetpack-protect-weak-password'; $is_connected = ( new Connection_Manager() )->is_connected(); @@ -247,7 +247,7 @@ public static function check_weak_passwords( $password ) { * @param int $user_id The user ID. * @param string $setting The password detection setting. */ - public static function add_password_detection_usermeta( $user_id, $setting ) { + public function add_password_detection_usermeta( $user_id, $setting ) { update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); } @@ -256,7 +256,7 @@ public static function add_password_detection_usermeta( $user_id, $setting ) { * * @param int $user_id The user ID. */ - public static function remove_password_detection_usermeta( $user_id ) { + public function remove_password_detection_usermeta( $user_id ) { delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); } @@ -265,7 +265,7 @@ public static function remove_password_detection_usermeta( $user_id ) { * * @param int $user_id The user ID. */ - public static function get_password_detection_usermeta( $user_id ) { + public function get_password_detection_usermeta( $user_id ) { return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); } @@ -277,7 +277,7 @@ public static function get_password_detection_usermeta( $user_id ) { * @param string $error The error message to display. * @param string $masked_email The masked email address. */ - public static function render_password_detection_template( $reset, $context, $error, $masked_email ) { + public function render_password_detection_template( $reset, $context, $error, $masked_email ) { defined( 'ABSPATH' ) || exit; ?> diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-password-reset-email.php index 359710d2b6d6a..bac4668b6ac6a 100644 --- a/projects/packages/account-protection/src/class-password-reset-email.php +++ b/projects/packages/account-protection/src/class-password-reset-email.php @@ -18,7 +18,7 @@ class Password_Reset_Email { * @param string $email The email address to mask. * @return string The masked email address. */ - public static function mask_email_address( $email ) { + public function mask_email_address( $email ) { $parts = explode( '@', $email ); $name = $parts[0]; $domain = $parts[1]; @@ -39,7 +39,7 @@ public static function mask_email_address( $email ) { * @param WP_User $user The user object. * @return bool True if the email was sent successfully, false otherwise. */ - public static function send( $user ) { + public function send( $user ) { // $site_url = home_url(); // $parsed_url = wp_parse_url( $site_url ); // $domain_name = $parsed_url['host']; diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php index d8af3451bc861..e5a45972d1f9e 100644 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -22,9 +22,9 @@ class REST_Controller { * * @return void */ - public static function register_rest_routes() { + public function register_rest_routes() { // Ensure routes are only initialized once. - static $routes_registered = false; + $routes_registered = false; if ( $routes_registered ) { return; } @@ -34,8 +34,8 @@ public static function register_rest_routes() { '/account-protection', array( 'methods' => WP_REST_Server::READABLE, - 'callback' => __CLASS__ . '::get_settings', - 'permission_callback' => __CLASS__ . '::permissions_callback', + 'callback' => array( $this, 'get_settings' ), + 'permission_callback' => array( $this, 'permissions_callback' ), ) ); @@ -44,8 +44,8 @@ public static function register_rest_routes() { '/account-protection', array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => __CLASS__ . '::update_settings', - 'permission_callback' => __CLASS__ . '::permissions_callback', + 'callback' => array( $this, 'update_settings' ), + 'permission_callback' => array( $this, 'permissions_callback' ), ) ); @@ -57,8 +57,8 @@ public static function register_rest_routes() { * * @return WP_REST_Response */ - public static function get_settings() { - $settings = Account_Protection::get_settings(); + public function get_settings() { + $settings = ( new Account_Protection() )->get_settings(); return rest_ensure_response( $settings ); } @@ -70,13 +70,13 @@ public static function get_settings() { * * @return WP_REST_Response|WP_Error */ - public static function update_settings( $request ) { + public function update_settings( $request ) { // Strict Mode if ( isset( $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ) ) { update_option( Account_Protection::STRICT_MODE_OPTION_NAME, $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ? '1' : '' ); } - return self::get_settings(); + return $this->get_settings(); } /** @@ -84,7 +84,7 @@ public static function update_settings( $request ) { * * @return bool|WP_Error True if user can view the Jetpack admin page. */ - public static function permissions_callback() { + public function permissions_callback() { if ( current_user_can( 'manage_options' ) ) { return true; } diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index b84d338782098..554570f666289 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -15,4 +15,4 @@ use Automattic\Jetpack\Account_Protection\Account_Protection; -Account_Protection::init(); +( new Account_Protection() )->init(); diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 8c0ec4f1b5b99..fe13bd83a670e 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -138,7 +138,7 @@ public function init() { REST_Controller::init(); My_Jetpack_Initializer::init(); Site_Health::init(); - Account_Protection::init(); + ( new Account_Protection() )->init(); // Sets up JITMS. JITM::configure(); @@ -214,6 +214,7 @@ public function initial_state() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $refresh_status_from_wpcom = isset( $_GET['checkPlan'] ); $status = Status::get_status( $refresh_status_from_wpcom ); + $account_protection = new Account_Protection(); $initial_state = array( 'apiRoot' => esc_url_raw( rest_url() ), @@ -233,8 +234,8 @@ public function initial_state() { 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), 'accountProtection' => array( - 'isEnabled' => Account_Protection::is_enabled(), - 'settings' => Account_Protection::get_settings(), + 'isEnabled' => $account_protection->is_enabled(), + 'settings' => $account_protection->get_settings(), ), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index 32d85f5e8ad97..b6ddb432afa23 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -371,8 +371,9 @@ public static function api_scan() { * @return WP_REST_Response|WP_Error */ public static function api_toggle_account_protection() { - if ( Account_Protection::is_enabled() ) { - $disabled = Account_Protection::disable(); + $account_protection = new Account_Protection(); + if ( $account_protection->is_enabled() ) { + $disabled = $account_protection->disable(); if ( ! $disabled ) { return new WP_Error( 'account_protection_disable_failed', @@ -384,7 +385,7 @@ public static function api_toggle_account_protection() { return rest_ensure_response( true ); } - $enabled = Account_Protection::enable(); + $enabled = $account_protection->enable(); if ( ! $enabled ) { return new WP_Error( 'account_protection_enable_failed', @@ -402,7 +403,7 @@ public static function api_toggle_account_protection() { * @return WP_Rest_Response */ public static function api_get_account_protection() { - return new WP_REST_Response( Account_Protection::is_enabled() ); + return new WP_REST_Response( ( new Account_Protection() )->get_settings() ); } /** From 992f2889f0ef83bc8a354c306860f49216110823 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 14:30:51 -0800 Subject: [PATCH 027/120] Add constructors --- .../src/class-account-protection.php | 44 +++++++++++++++---- .../src/class-password-detection.php | 22 ++++++++-- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 42145fa3ede97..0d25dabfccfc2 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -18,6 +18,31 @@ class Account_Protection { const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; + /** + * Modules dependency. + * + * @var Modules + */ + private $modules; + + /** + * Password Detection dependency. + * + * @var Password_Detection + */ + private $password_detection; + + /** + * Constructor. + * + * @param Modules|null $modules Modules dependency. + * @param Password_Detection|null $password_detection Password detection dependency. + */ + public function __construct( Modules $modules = null, Password_Detection $password_detection = null ) { + $this->modules = $modules ?? new Modules(); + $this->password_detection = $password_detection ?? new Password_Detection(); + } + /** * Initializes the configurations needed for the account protection module. */ @@ -35,24 +60,24 @@ public function init() { if ( $this->is_enabled() ) { // Validate password after successful login - add_action( 'wp_authenticate_user', array( new Password_Detection(), 'login_form_password_detection' ), 10, 2 ); + add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); // Add password detection flow for users with unsafe passwords add_action( 'login_form_password-detection', - array( new Password_Detection(), 'render_password_detection_page' ), + array( $this->password_detection, 'render_password_detection_page' ), 10, 2 ); // Register AJAX resend password reset email action - add_action( 'wp_ajax_resend_password_reset', array( new Password_Detection(), 'ajax_resend_password_reset_email' ) ); + add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) ); // Remove password detection usermeta on password reset add_action( 'after_password_reset', function ( $user ) { - ( new Password_Detection() )->remove_password_detection_usermeta( $user->ID ); + $this->password_detection->remove_password_detection_usermeta( $user->ID ); }, 10, 2 @@ -68,7 +93,7 @@ function ( $user_id ) { ) { // Profile updates should include validation, but we should reset user meta to be safe if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - ( new Password_Detection() )->remove_password_detection_usermeta( $user_id ); + $this->password_detection->remove_password_detection_usermeta( $user_id ); } } }, @@ -83,6 +108,7 @@ function ( $user_id ) { * Activate the account protection on module activation. */ public function on_account_protection_activation() { + // Activation logic can be added here } /** @@ -92,7 +118,7 @@ public function on_account_protection_deactivation() { // Remove user meta on deactivation $users = get_users(); foreach ( $users as $user ) { - ( new Password_Detection() )->remove_password_detection_usermeta( $user->ID ); + $this->password_detection->remove_password_detection_usermeta( $user->ID ); // TODO: Remove usermeta on plugin deactivation } } @@ -103,7 +129,7 @@ public function on_account_protection_deactivation() { * @return bool */ public function is_enabled() { - return ( new Modules() )->is_active( 'account-protection' ); + return $this->modules->is_active( 'account-protection' ); } /** @@ -116,7 +142,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return ( new Modules() )->activate( 'account-protection', false, false ); + return $this->modules->activate( 'account-protection', false, false ); } /** @@ -129,7 +155,7 @@ public function disable() { if ( ! $this->is_enabled() ) { return true; } - return ( new Modules() )->deactivate( 'account-protection' ); + return $this->modules->deactivate( 'account-protection' ); } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 0dcc178e1d2ba..1281c137d95de 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -17,6 +17,22 @@ class Password_Detection { const PASSWORD_DETECTION_USER_META_KEY = 'jetpack_account_protection_password_status'; + /** + * Password reset email dependency. + * + * @var Password_Reset_Email + */ + private $password_reset_email; + + /** + * Constructor. + * + * @param Password_Reset_Email|null $password_reset_email Password reset email dependency. + */ + public function __construct( Password_Reset_Email $password_reset_email = null ) { + $this->password_reset_email = $password_reset_email ?? new Password_Reset_Email(); + } + /** * Redirect to the password detection page. * @@ -99,7 +115,7 @@ public function render_password_detection_page() { if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) { // Send password reset email if ( ! $email_sent_flag ) { - $email_sent = ( new Password_Reset_Email() )->send( $current_user ); + $email_sent = $this->password_reset_email->send( $current_user ); if ( $email_sent ) { // Set transient to mark the email as sent set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); @@ -126,7 +142,7 @@ public function render_password_detection_page() { } } - $this->render_password_detection_template( $reset, $context, $error, ( new Password_Reset_Email() )->mask_email_address( $current_user->user_email ) ); + $this->render_password_detection_template( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); exit; } @@ -175,7 +191,7 @@ public function ajax_resend_password_reset_email() { $email = $current_user->user_email; // Resend the email - $email_sent = ( new Password_Reset_Email() )->send( $current_user, $email ); + $email_sent = $this->password_reset_email->send( $current_user, $email ); if ( $email_sent ) { wp_send_json_success( array( 'message' => 'Resend successful.' ) ); } else { From 43d4cd0ec57a8bdf18dd6c6b17453284426a9b5a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 14:47:08 -0800 Subject: [PATCH 028/120] Reorg user meta methods --- .../src/class-account-protection.php | 68 +++++++------------ .../src/class-password-detection.php | 44 +++++++++--- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 0d25dabfccfc2..70a53e41a3e09 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -47,6 +47,17 @@ public function __construct( Modules $modules = null, Password_Detection $passwo * Initializes the configurations needed for the account protection module. */ public function init() { + $this->register_hooks(); + + if ( $this->is_enabled() ) { + $this->register_runtime_hooks(); + } + } + + /** + * Register hooks for module activation and environment validation. + */ + private function register_hooks() { // Account protection activation/deactivation hooks add_action( 'jetpack_activate_module_account-protection', array( $this, 'on_account_protection_activation' ) ); add_action( 'jetpack_deactivate_module_account-protection', array( $this, 'on_account_protection_deactivation' ) ); @@ -57,51 +68,24 @@ public function init() { // Register REST routes add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); + } - if ( $this->is_enabled() ) { - // Validate password after successful login - add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); - - // Add password detection flow for users with unsafe passwords - add_action( - 'login_form_password-detection', - array( $this->password_detection, 'render_password_detection_page' ), - 10, - 2 - ); + /** + * Register hooks for runtime operations. + */ + private function register_runtime_hooks() { + // Validate password after successful login + add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); - // Register AJAX resend password reset email action - add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) ); - - // Remove password detection usermeta on password reset - add_action( - 'after_password_reset', - function ( $user ) { - $this->password_detection->remove_password_detection_usermeta( $user->ID ); - }, - 10, - 2 - ); + // Add password detection flow for users with unsafe passwords + add_action( 'login_form_password-detection', array( $this->password_detection, 'render_password_detection_page' ), 10, 2 ); - // Remove password detection usermeta on profile password update - add_action( - 'profile_update', - function ( $user_id ) { - if ( - ! empty( $_POST['_wpnonce'] ) && - wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) - ) { - // Profile updates should include validation, but we should reset user meta to be safe - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $this->password_detection->remove_password_detection_usermeta( $user_id ); - } - } - }, - 10, - 2 - ); + // Remove password detection usermeta after password reset and on profile password update + add_action( 'after_password_reset', array( $this->password_detection, 'delete_password_detection_usermeta_after_password_reset' ), 10, 2 ); + add_action( 'profile_update', array( $this->password_detection, 'delete_password_detection_usermeta_on_profile_update' ), 10, 2 ); - } + // Register AJAX resend password reset email action + add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) ); } /** @@ -118,7 +102,7 @@ public function on_account_protection_deactivation() { // Remove user meta on deactivation $users = get_users(); foreach ( $users as $user ) { - $this->password_detection->remove_password_detection_usermeta( $user->ID ); + $this->password_detection->delete_password_detection_usermeta( $user->ID ); // TODO: Remove usermeta on plugin deactivation } } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 1281c137d95de..74a3ebd21675a 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -62,13 +62,12 @@ public function login_form_password_detection( $user, $password ) { if ( ! $this->validate_password( $password ) ) { // TODO: Ensure this usermeta is always up to date - $this->add_password_detection_usermeta( $user->ID, 'unsafe' ); + $this->update_password_detection_usermeta( $user->ID, 'unsafe' ); // Redirect to the password detection page add_filter( 'login_redirect', array( $this, 'password_detection_redirect' ), 10, 3 ); } else { - $this->add_password_detection_usermeta( $user->ID, 'safe' ); - + $this->update_password_detection_usermeta( $user->ID, 'safe' ); } return $user; @@ -258,31 +257,56 @@ public function check_weak_passwords( $password ) { } /** - * Add the password detection usermeta. + * Get the password detection usermeta. + * + * @param int $user_id The user ID. + */ + public function get_password_detection_usermeta( $user_id ) { + return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); + } + + /** + * Update the password detection usermeta. * * @param int $user_id The user ID. * @param string $setting The password detection setting. */ - public function add_password_detection_usermeta( $user_id, $setting ) { + public function update_password_detection_usermeta( $user_id, $setting ) { update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); } /** - * Remove the password detection usermeta. + * Delete the password detection usermeta. * * @param int $user_id The user ID. */ - public function remove_password_detection_usermeta( $user_id ) { + public function delete_password_detection_usermeta( $user_id ) { delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); } /** - * Get the password detection usermeta. + * Delete the password detection usermeta after password reset. + * + * @param WP_User $user The user object. + */ + public function delete_password_detection_usermeta_after_password_reset( $user ) { + $this->delete_password_detection_usermeta( $user->ID ); + } + + /** + * Delete the password detection usermeta on profile password update. * * @param int $user_id The user ID. */ - public function get_password_detection_usermeta( $user_id ) { - return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); + public function delete_password_detection_usermeta_on_profile_update( $user_id ) { + if ( + ! empty( $_POST['_wpnonce'] ) && + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) + ) { + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + $this->delete_password_detection_usermeta( $user_id ); + } + } } /** From 3cec8912b4482fdb5b1b515ab7b9ce9c3de02e0c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 15:09:09 -0800 Subject: [PATCH 029/120] Add type declarations and hinting --- .../src/class-account-protection.php | 37 ++++++++-------- .../src/class-password-detection.php | 43 ++++++++++--------- .../src/class-password-reset-email.php | 6 +-- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 70a53e41a3e09..25e8a4a0db525 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -13,7 +13,6 @@ * Class Account_Protection */ class Account_Protection { - const PACKAGE_VERSION = '1.0.0-alpha'; const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; @@ -46,7 +45,7 @@ public function __construct( Modules $modules = null, Password_Detection $passwo /** * Initializes the configurations needed for the account protection module. */ - public function init() { + public function init(): void { $this->register_hooks(); if ( $this->is_enabled() ) { @@ -57,10 +56,10 @@ public function init() { /** * Register hooks for module activation and environment validation. */ - private function register_hooks() { + private function register_hooks(): void { // Account protection activation/deactivation hooks - add_action( 'jetpack_activate_module_account-protection', array( $this, 'on_account_protection_activation' ) ); - add_action( 'jetpack_deactivate_module_account-protection', array( $this, 'on_account_protection_deactivation' ) ); + add_action( 'jetpack_activate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); + add_action( 'jetpack_deactivate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); // Do not run in unsupported environments add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) ); @@ -73,11 +72,11 @@ private function register_hooks() { /** * Register hooks for runtime operations. */ - private function register_runtime_hooks() { + private function register_runtime_hooks(): void { // Validate password after successful login add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); - // Add password detection flow for users with unsafe passwords + // Add password detection flow add_action( 'login_form_password-detection', array( $this->password_detection, 'render_password_detection_page' ), 10, 2 ); // Remove password detection usermeta after password reset and on profile password update @@ -91,14 +90,14 @@ private function register_runtime_hooks() { /** * Activate the account protection on module activation. */ - public function on_account_protection_activation() { + public function on_account_protection_activation(): void { // Activation logic can be added here } /** * Deactivate the account protection on module deactivation. */ - public function on_account_protection_deactivation() { + public function on_account_protection_deactivation(): void { // Remove user meta on deactivation $users = get_users(); foreach ( $users as $user ) { @@ -113,7 +112,7 @@ public function on_account_protection_deactivation() { * @return bool */ public function is_enabled() { - return $this->modules->is_active( 'account-protection' ); + return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -126,7 +125,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return $this->modules->activate( 'account-protection', false, false ); + return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -134,12 +133,12 @@ public function enable() { * * @return bool */ - public function disable() { + public function disable(): bool { // Return true if already disabled. if ( ! $this->is_enabled() ) { return true; } - return $this->modules->deactivate( 'account-protection' ); + return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -147,7 +146,7 @@ public function disable() { * * @return bool */ - public function is_supported_environment() { + public function is_supported_environment(): bool { // Do not run when killswitch is enabled if ( defined( 'DISABLE_JETPACK_ACCOUNT_PROTECTION' ) && DISABLE_JETPACK_ACCOUNT_PROTECTION ) { return false; @@ -163,10 +162,10 @@ public function is_supported_environment() { * * @return array Array of module slugs. */ - public function remove_module_on_unsupported_environments( $modules ) { + public function remove_module_on_unsupported_environments( array $modules ): array { if ( ! $this->is_supported_environment() ) { // Account protection should never be available on unsupported platforms. - unset( $modules['account-protection'] ); + unset( $modules[ self::ACCOUNT_PROTECTION_MODULE_NAME ] ); } return $modules; @@ -179,13 +178,13 @@ public function remove_module_on_unsupported_environments( $modules ) { * * @return array Array of module slugs. */ - public function remove_standalone_module_on_unsupported_environments( $modules ) { + public function remove_standalone_module_on_unsupported_environments( array $modules ): array { if ( ! $this->is_supported_environment() ) { // Account Protection should never be available on unsupported platforms. $modules = array_filter( $modules, function ( $module ) { - return $module !== 'account-protection'; + return $module !== self::ACCOUNT_PROTECTION_MODULE_NAME; } ); @@ -199,7 +198,7 @@ function ( $module ) { * * @return array */ - public function get_settings() { + public function get_settings(): array { $settings = array( self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), ); diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 74a3ebd21675a..383c12c08997b 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -38,18 +38,18 @@ public function __construct( Password_Reset_Email $password_reset_email = null ) * * @return string The URL to redirect to. */ - public function password_detection_redirect() { + public function password_detection_redirect(): string { return home_url( '/wp-login.php?action=password-detection' ); } /** * Check if the password is safe after login. * - * @param WP_User $user The user object. - * @param string $password The password. - * @return WP_User The user object. + * @param \WP_User $user The user object. + * @param string $password The password. + * @return \WP_User|\WP_Error The user object. */ - public function login_form_password_detection( $user, $password ) { + public function login_form_password_detection( \WP_User $user, string $password ): \WP_User { // Check if the user is already a WP_Error object if ( is_wp_error( $user ) ) { return $user; @@ -76,11 +76,9 @@ public function login_form_password_detection( $user, $password ) { /** * Render password detection page. * - * This page is shown to users with unsafe passwords after login. - * * @return void */ - public function render_password_detection_page() { + public function render_password_detection_page(): void { // Restrict direct access to logged in users $current_user = wp_get_current_user(); if ( 0 === $current_user->ID ) { @@ -147,8 +145,10 @@ public function render_password_detection_page() { /** * Enqueue the resend password reset email scripts. + * + * @return void */ - public function enqueue_resend_password_reset_scripts() { + public function enqueue_resend_password_reset_scripts(): void { wp_enqueue_script( 'resend-password-reset', plugin_dir_url( __FILE__ ) . 'js/resend-password-reset.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); // Pass AJAX URL and nonce to the script @@ -164,8 +164,10 @@ public function enqueue_resend_password_reset_scripts() { /** * Enqueue the password detection page styles. + * + * @return void */ - public function enqueue_password_detection_styles() { + public function enqueue_password_detection_styles(): void { wp_enqueue_style( 'password-detection-styles', plugin_dir_url( __FILE__ ) . 'css/password-detection.css', @@ -204,7 +206,7 @@ public function ajax_resend_password_reset_email() { * @param string $password The password to validate. * @return bool True if the password is valid, false otherwise. */ - public function validate_password( $password ) { + public function validate_password( string $password ): bool { // TODO: Uncomment out once endpoint is live // Check compromised and common passwords // $weak_password = self::check_weak_passwords( $password ); @@ -216,9 +218,9 @@ public function validate_password( $password ) { * Check if the password is in the list of common/compromised passwords. * * @param string $password The password to check. - * @return bool|WP_Error True if the password is in the list of common/compromised passwords, false otherwise. + * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise. */ - public function check_weak_passwords( $password ) { + public function check_weak_passwords( string $password ) { $api_url = '/jetpack-protect-weak-password'; $is_connected = ( new Connection_Manager() )->is_connected(); @@ -261,7 +263,7 @@ public function check_weak_passwords( $password ) { * * @param int $user_id The user ID. */ - public function get_password_detection_usermeta( $user_id ) { + public function get_password_detection_usermeta( int $user_id ) { return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); } @@ -271,7 +273,7 @@ public function get_password_detection_usermeta( $user_id ) { * @param int $user_id The user ID. * @param string $setting The password detection setting. */ - public function update_password_detection_usermeta( $user_id, $setting ) { + public function update_password_detection_usermeta( int $user_id, string $setting ) { update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); } @@ -280,16 +282,16 @@ public function update_password_detection_usermeta( $user_id, $setting ) { * * @param int $user_id The user ID. */ - public function delete_password_detection_usermeta( $user_id ) { + public function delete_password_detection_usermeta( int $user_id ) { delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); } /** * Delete the password detection usermeta after password reset. * - * @param WP_User $user The user object. + * @param \WP_User $user The user object. */ - public function delete_password_detection_usermeta_after_password_reset( $user ) { + public function delete_password_detection_usermeta_after_password_reset( \WP_User $user ) { $this->delete_password_detection_usermeta( $user->ID ); } @@ -298,7 +300,7 @@ public function delete_password_detection_usermeta_after_password_reset( $user ) * * @param int $user_id The user ID. */ - public function delete_password_detection_usermeta_on_profile_update( $user_id ) { + public function delete_password_detection_usermeta_on_profile_update( int $user_id ) { if ( ! empty( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) @@ -316,8 +318,9 @@ public function delete_password_detection_usermeta_on_profile_update( $user_id ) * @param string $context The context for the password detection page. * @param string $error The error message to display. * @param string $masked_email The masked email address. + * @return void */ - public function render_password_detection_template( $reset, $context, $error, $masked_email ) { + public function render_password_detection_template( bool $reset, string $context, string $error, string $masked_email ): void { defined( 'ABSPATH' ) || exit; ?> diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-password-reset-email.php index bac4668b6ac6a..5d2358317669a 100644 --- a/projects/packages/account-protection/src/class-password-reset-email.php +++ b/projects/packages/account-protection/src/class-password-reset-email.php @@ -18,7 +18,7 @@ class Password_Reset_Email { * @param string $email The email address to mask. * @return string The masked email address. */ - public function mask_email_address( $email ) { + public function mask_email_address( string $email ): string { $parts = explode( '@', $email ); $name = $parts[0]; $domain = $parts[1]; @@ -36,10 +36,10 @@ public function mask_email_address( $email ) { /** * Send password reset email. * - * @param WP_User $user The user object. + * @param \WP_User $user The user object. * @return bool True if the email was sent successfully, false otherwise. */ - public function send( $user ) { + public function send( \WP_User $user ): bool { // $site_url = home_url(); // $parsed_url = wp_parse_url( $site_url ); // $domain_name = $parsed_url['host']; From c61877b07709e7c9ff74b68e2e991d77745a7e17 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 17 Jan 2025 15:36:19 -0800 Subject: [PATCH 030/120] Simplify method naming --- .../src/class-account-protection.php | 15 +++---- .../src/class-password-detection.php | 44 ++++++++++++------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 25e8a4a0db525..a1c92f9c49093 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -77,11 +77,11 @@ private function register_runtime_hooks(): void { add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); // Add password detection flow - add_action( 'login_form_password-detection', array( $this->password_detection, 'render_password_detection_page' ), 10, 2 ); + add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 ); // Remove password detection usermeta after password reset and on profile password update - add_action( 'after_password_reset', array( $this->password_detection, 'delete_password_detection_usermeta_after_password_reset' ), 10, 2 ); - add_action( 'profile_update', array( $this->password_detection, 'delete_password_detection_usermeta_on_profile_update' ), 10, 2 ); + add_action( 'after_password_reset', array( $this->password_detection, 'delete_usermeta_after_password_reset' ), 10, 2 ); + add_action( 'profile_update', array( $this->password_detection, 'delete_usermeta_on_profile_update' ), 10, 2 ); // Register AJAX resend password reset email action add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) ); @@ -98,12 +98,9 @@ public function on_account_protection_activation(): void { * Deactivate the account protection on module deactivation. */ public function on_account_protection_deactivation(): void { - // Remove user meta on deactivation - $users = get_users(); - foreach ( $users as $user ) { - $this->password_detection->delete_password_detection_usermeta( $user->ID ); - // TODO: Remove usermeta on plugin deactivation - } + // Remove password detection user meta on deactivation + // TODO: Run on Jetpack and Protect deactivation + $this->password_detection->delete_all_usermeta(); } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 383c12c08997b..c6e39b533a439 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -61,13 +61,13 @@ public function login_form_password_detection( \WP_User $user, string $password } if ( ! $this->validate_password( $password ) ) { - // TODO: Ensure this usermeta is always up to date - $this->update_password_detection_usermeta( $user->ID, 'unsafe' ); + // TODO: Ensure usermeta is always up to date + $this->update_usermeta( $user->ID, 'unsafe' ); // Redirect to the password detection page add_filter( 'login_redirect', array( $this, 'password_detection_redirect' ), 10, 3 ); } else { - $this->update_password_detection_usermeta( $user->ID, 'safe' ); + $this->update_usermeta( $user->ID, 'safe' ); } return $user; @@ -78,7 +78,7 @@ public function login_form_password_detection( \WP_User $user, string $password * * @return void */ - public function render_password_detection_page(): void { + public function render_page(): void { // Restrict direct access to logged in users $current_user = wp_get_current_user(); if ( 0 === $current_user->ID ) { @@ -87,7 +87,7 @@ public function render_password_detection_page(): void { } // Restrict direct access to users with unsafe passwords - $user_password_status = $this->get_password_detection_usermeta( $current_user->ID ); + $user_password_status = $this->get_usermeta( $current_user->ID ); if ( ! $user_password_status || 'safe' === $user_password_status ) { wp_safe_redirect( admin_url() ); exit; @@ -102,7 +102,7 @@ public function render_password_detection_page(): void { $context = 'Your current password was found in a public leak, which means your account might be at risk.'; $error = ''; - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_password_detection_styles' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); // Handle reset_password_action form submission if ( isset( $_POST['reset-password'] ) ) { @@ -139,7 +139,7 @@ public function render_password_detection_page(): void { } } - $this->render_password_detection_template( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); + $this->render_content( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); exit; } @@ -167,7 +167,7 @@ public function enqueue_resend_password_reset_scripts(): void { * * @return void */ - public function enqueue_password_detection_styles(): void { + public function enqueue_styles(): void { wp_enqueue_style( 'password-detection-styles', plugin_dir_url( __FILE__ ) . 'css/password-detection.css', @@ -263,7 +263,7 @@ public function check_weak_passwords( string $password ) { * * @param int $user_id The user ID. */ - public function get_password_detection_usermeta( int $user_id ) { + public function get_usermeta( int $user_id ) { return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); } @@ -273,16 +273,26 @@ public function get_password_detection_usermeta( int $user_id ) { * @param int $user_id The user ID. * @param string $setting The password detection setting. */ - public function update_password_detection_usermeta( int $user_id, string $setting ) { + public function update_usermeta( int $user_id, string $setting ) { update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); } + /** + * Delete password detection usermeta for all users. + */ + public function delete_all_usermeta() { + $users = get_users(); + foreach ( $users as $user ) { + $this->delete_usermeta( $user->ID ); + } + } + /** * Delete the password detection usermeta. * * @param int $user_id The user ID. */ - public function delete_password_detection_usermeta( int $user_id ) { + public function delete_usermeta( int $user_id ) { delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); } @@ -291,8 +301,8 @@ public function delete_password_detection_usermeta( int $user_id ) { * * @param \WP_User $user The user object. */ - public function delete_password_detection_usermeta_after_password_reset( \WP_User $user ) { - $this->delete_password_detection_usermeta( $user->ID ); + public function delete_usermeta_after_password_reset( \WP_User $user ) { + $this->delete_usermeta( $user->ID ); } /** @@ -300,19 +310,19 @@ public function delete_password_detection_usermeta_after_password_reset( \WP_Use * * @param int $user_id The user ID. */ - public function delete_password_detection_usermeta_on_profile_update( int $user_id ) { + public function delete_usermeta_on_profile_update( int $user_id ) { if ( ! empty( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) ) { if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $this->delete_password_detection_usermeta( $user_id ); + $this->delete_usermeta( $user_id ); } } } /** - * Render template for password detection page. + * Render content for password detection page. * * @param bool $reset Whether the user is resetting their password. * @param string $context The context for the password detection page. @@ -320,7 +330,7 @@ public function delete_password_detection_usermeta_on_profile_update( int $user_ * @param string $masked_email The masked email address. * @return void */ - public function render_password_detection_template( bool $reset, string $context, string $error, string $masked_email ): void { + public function render_content( bool $reset, string $context, string $error, string $masked_email ): void { defined( 'ABSPATH' ) || exit; ?> From 7634ed26a0a64f057a29a51de7a9b258bb93c4a5 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 10:06:39 -0800 Subject: [PATCH 031/120] Use dynamic classes --- .../src/class-account-protection.php | 26 +++++++++---------- .../src/class-rest-controller.php | 20 +++++++------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index f7df6a27ca5fd..69cc546a74533 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -21,10 +21,10 @@ class Account_Protection { /** * Initializes the configurations needed for the account protection module. */ - public static function init() { + public function init() { // Account protection activation/deactivation hooks - add_action( 'jetpack_activate_module_account-protection', __CLASS__ . '::on_account_protection_activation' ); - add_action( 'jetpack_deactivate_module_account-protection', __CLASS__ . '::on_account_protection_deactivation' ); + add_action( 'jetpack_activate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); + add_action( 'jetpack_deactivate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); // Register REST routes add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); @@ -33,14 +33,14 @@ public static function init() { /** * Activate the account protection on module activation. */ - public static function on_account_protection_activation() { + public function on_account_protection_activation() { // Account protection activated } /** * Deactivate the account protection on module activation. */ - public static function on_account_protection_deactivation() { + public function on_account_protection_deactivation() { // Account protection deactivated } @@ -49,8 +49,8 @@ public static function on_account_protection_deactivation() { * * @return bool */ - public static function is_enabled() { - return ( new Modules() )->is_active( 'account-protection' ); + public function is_enabled() { + return ( new Modules() )->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -58,12 +58,12 @@ public static function is_enabled() { * * @return bool */ - public static function enable() { + public function enable() { // Return true if already enabled. - if ( self::is_enabled() ) { + if ( $this->is_enabled() ) { return true; } - return ( new Modules() )->activate( 'account-protection', false, false ); + return ( new Modules() )->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -71,11 +71,11 @@ public static function enable() { * * @return bool */ - public static function disable() { + public function disable() { // Return true if already disabled. - if ( ! self::is_enabled() ) { + if ( ! $this->is_enabled() ) { return true; } - return ( new Modules() )->deactivate( 'account-protection' ); + return ( new Modules() )->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } } diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php index 60e6f71f6cc74..998f150941873 100644 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -22,9 +22,9 @@ class REST_Controller { * * @return void */ - public static function register_rest_routes() { + public function register_rest_routes() { // Ensure routes are only initialized once. - static $routes_registered = false; + $routes_registered = false; if ( $routes_registered ) { return; } @@ -34,8 +34,8 @@ public static function register_rest_routes() { '/account-protection', array( 'methods' => WP_REST_Server::READABLE, - 'callback' => __CLASS__ . '::get_settings', - 'permission_callback' => __CLASS__ . '::permissions_callback', + 'callback' => array( $this, 'get_settings' ), + 'permission_callback' => array( $this, 'permissions_callback' ), ) ); @@ -44,8 +44,8 @@ public static function register_rest_routes() { '/account-protection', array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => __CLASS__ . '::update_settings', - 'permission_callback' => __CLASS__ . '::permissions_callback', + 'callback' => array( $this, 'update_settings' ), + 'permission_callback' => array( $this, 'permissions_callback' ), ) ); @@ -57,7 +57,7 @@ public static function register_rest_routes() { * * @return WP_REST_Response */ - public static function get_settings() { + public function get_settings() { return rest_ensure_response( array( Account_Protection::STRICT_MODE_OPTION_NAME => get_option( Account_Protection::STRICT_MODE_OPTION_NAME ), @@ -72,13 +72,13 @@ public static function get_settings() { * * @return WP_REST_Response|WP_Error */ - public static function update_settings( $request ) { + public function update_settings( $request ) { // Strict Mode if ( isset( $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ) ) { update_option( Account_Protection::STRICT_MODE_OPTION_NAME, $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ? '1' : '' ); } - return self::get_settings(); + return $this->get_settings(); } /** @@ -86,7 +86,7 @@ public static function update_settings( $request ) { * * @return bool|WP_Error True if user can view the Jetpack admin page. */ - public static function permissions_callback() { + public function permissions_callback() { if ( current_user_can( 'manage_options' ) ) { return true; } From 692db33338b4663ecc46532d6725465377fa11cb Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 10:12:46 -0800 Subject: [PATCH 032/120] Update class dependencies --- .../src/class-account-protection.php | 22 ++++++++++++++++--- .../jetpack/modules/account-protection.php | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 69cc546a74533..e08d0212867f0 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -18,6 +18,22 @@ class Account_Protection { const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; + /** + * Modules dependency. + * + * @var Modules + */ + private $modules; + + /** + * Constructor. + * + * @param Modules|null $modules Modules dependency. + */ + public function __construct( Modules $modules = null ) { + $this->modules = $modules ?? new Modules(); + } + /** * Initializes the configurations needed for the account protection module. */ @@ -50,7 +66,7 @@ public function on_account_protection_deactivation() { * @return bool */ public function is_enabled() { - return ( new Modules() )->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -63,7 +79,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return ( new Modules() )->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); + return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -76,6 +92,6 @@ public function disable() { if ( ! $this->is_enabled() ) { return true; } - return ( new Modules() )->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } } diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index b84d338782098..554570f666289 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -15,4 +15,4 @@ use Automattic\Jetpack\Account_Protection\Account_Protection; -Account_Protection::init(); +( new Account_Protection() )->init(); From 22d267848fa4b12690cda417ab80ad5b4518c2aa Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 10:29:37 -0800 Subject: [PATCH 033/120] Fix copy --- .../jetpack/_inc/client/security/account-protection.jsx | 2 +- .../ai-assistant-plugin/components/breve/features/events.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx index 6b5e9126b7493..7334eb9e3ca7d 100644 --- a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx +++ b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx @@ -142,7 +142,7 @@ const AccountProtection = class extends Component { label={
- { __( 'Reqiure strong passwords', 'jetpack' ) } + { __( 'Require strong passwords', 'jetpack' ) } { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ); + }, 100 ) as unknown as number; } export default function registerEvents( clientId: string ) { From 0fd3e4160b5cc1871b813aa56f706bd93509164e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 10:30:43 -0800 Subject: [PATCH 034/120] Revert unrelated changes --- .../ai-assistant-plugin/components/breve/features/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts index eb5eb0442d2e4..58075d8857569 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts @@ -97,7 +97,7 @@ async function handleMouseEnter( e: MouseEvent ) { target: el, virtual: virtual, } as Anchor ); - }, 500 ) as unknown as number; + }, 500 ); } function handleMouseLeave() { @@ -106,7 +106,7 @@ function handleMouseLeave() { highlightTimeout = setTimeout( () => { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ) as unknown as number; + }, 100 ); } export default function registerEvents( clientId: string ) { From 4383b5e10167f0cd4b8cc76524e9eecdbeb402e0 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 10:48:26 -0800 Subject: [PATCH 035/120] Revert unrelated changes --- .../ai-assistant-plugin/components/breve/features/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts index eb5eb0442d2e4..58075d8857569 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts @@ -97,7 +97,7 @@ async function handleMouseEnter( e: MouseEvent ) { target: el, virtual: virtual, } as Anchor ); - }, 500 ) as unknown as number; + }, 500 ); } function handleMouseLeave() { @@ -106,7 +106,7 @@ function handleMouseLeave() { highlightTimeout = setTimeout( () => { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ) as unknown as number; + }, 100 ); } export default function registerEvents( clientId: string ) { From 9a70647126ee289dd64b27f796e9611408430860 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 11:10:13 -0800 Subject: [PATCH 036/120] Fix method calls --- .../account-protection/src/class-account-protection.php | 2 +- .../account-protection/src/class-rest-controller.php | 2 +- projects/plugins/protect/src/class-jetpack-protect.php | 7 ++++--- projects/plugins/protect/src/class-rest-controller.php | 9 +++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index e7a33a482fecc..86241a74ac9b1 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -100,7 +100,7 @@ public function disable() { * * @return array */ - public static function get_settings() { + public function get_settings() { $settings = array( self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), ); diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php index 9d53af5c9b80f..e5a45972d1f9e 100644 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -57,7 +57,7 @@ public function register_rest_routes() { * * @return WP_REST_Response */ - public static function get_settings() { + public function get_settings() { $settings = ( new Account_Protection() )->get_settings(); return rest_ensure_response( $settings ); diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index c69b627931740..889bda69d50c9 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -138,7 +138,7 @@ public function init() { REST_Controller::init(); My_Jetpack_Initializer::init(); Site_Health::init(); - Account_Protection::init(); + ( new Account_Protection() )->init(); // Sets up JITMS. JITM::configure(); @@ -214,6 +214,7 @@ public function initial_state() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $refresh_status_from_wpcom = isset( $_GET['checkPlan'] ); $status = Status::get_status( $refresh_status_from_wpcom ); + $account_protection = new Account_Protection(); $initial_state = array( 'apiRoot' => esc_url_raw( rest_url() ), @@ -233,8 +234,8 @@ public function initial_state() { 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), 'accountProtection' => array( - 'isEnabled' => Account_Protection::is_enabled(), - 'settings' => Account_Protection::get_settings(), + 'isEnabled' => $account_protection->is_enabled(), + 'settings' => $account_protection->get_settings(), ), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index 32d85f5e8ad97..fb6dd1dba9fd4 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -371,8 +371,9 @@ public static function api_scan() { * @return WP_REST_Response|WP_Error */ public static function api_toggle_account_protection() { - if ( Account_Protection::is_enabled() ) { - $disabled = Account_Protection::disable(); + $account_protection = new Account_Protection(); + if ( $account_protection->is_enabled() ) { + $disabled = $account_protection->disable(); if ( ! $disabled ) { return new WP_Error( 'account_protection_disable_failed', @@ -384,7 +385,7 @@ public static function api_toggle_account_protection() { return rest_ensure_response( true ); } - $enabled = Account_Protection::enable(); + $enabled = $account_protection->enable(); if ( ! $enabled ) { return new WP_Error( 'account_protection_enable_failed', @@ -402,7 +403,7 @@ public static function api_toggle_account_protection() { * @return WP_Rest_Response */ public static function api_get_account_protection() { - return new WP_REST_Response( Account_Protection::is_enabled() ); + return new WP_REST_Response( ( new Account_Protection() )->is_enabled() ); } /** From 969102f5b4fa69a1c98424c1e1cf7bbdd18481cd Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 11:10:40 -0800 Subject: [PATCH 037/120] Do not activate by default --- projects/plugins/protect/src/class-jetpack-protect.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 889bda69d50c9..715a6b02cb7a1 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -286,13 +286,12 @@ public static function do_plugin_activation_activities() { } /** - * Activates the waf, brute force protection and account protection modules and disables the activation option + * Activates the waf and brute force protection modules and disables the activation option */ public static function activate_modules() { delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ); ( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false ); ( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false ); - ( new Modules() )->activate( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, false, false ); } /** From 8356bd43747057556614134b54bf18034d8f2251 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 11:22:21 -0800 Subject: [PATCH 038/120] Fix phan errors --- .../src/class-account-protection.php | 22 +++---------------- .../src/class-rest-controller.php | 12 +++++++--- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index e08d0212867f0..69cc546a74533 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -18,22 +18,6 @@ class Account_Protection { const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; - /** - * Modules dependency. - * - * @var Modules - */ - private $modules; - - /** - * Constructor. - * - * @param Modules|null $modules Modules dependency. - */ - public function __construct( Modules $modules = null ) { - $this->modules = $modules ?? new Modules(); - } - /** * Initializes the configurations needed for the account protection module. */ @@ -66,7 +50,7 @@ public function on_account_protection_deactivation() { * @return bool */ public function is_enabled() { - return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return ( new Modules() )->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -79,7 +63,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); + return ( new Modules() )->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -92,6 +76,6 @@ public function disable() { if ( ! $this->is_enabled() ) { return true; } - return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return ( new Modules() )->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } } diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php index 998f150941873..762fb90570c30 100644 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ b/projects/packages/account-protection/src/class-rest-controller.php @@ -17,6 +17,13 @@ * Defines our endponts. */ class REST_Controller { + /** + * Tracks whether routes have already been registered. + * + * @var bool + */ + private $routes_registered = false; + /** * Register REST API endpoints. * @@ -24,8 +31,7 @@ class REST_Controller { */ public function register_rest_routes() { // Ensure routes are only initialized once. - $routes_registered = false; - if ( $routes_registered ) { + if ( $this->routes_registered ) { return; } @@ -49,7 +55,7 @@ public function register_rest_routes() { ) ); - $routes_registered = true; + $this->routes_registered = true; } /** From 32f3ef6b8a28580fbf9b285c5b8343ba98d5578e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 13:36:55 -0800 Subject: [PATCH 039/120] Changelog --- .../add-jetpack-account-protection-security-settings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings diff --git a/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings b/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings new file mode 100644 index 0000000000000..af516388c3c6c --- /dev/null +++ b/projects/packages/account-protection/changelog/add-jetpack-account-protection-security-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds handling for module activation and deactivation From b02d5113d62f05649926e02ff966722cb7dfbb01 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 13:57:12 -0800 Subject: [PATCH 040/120] Update composer deps --- projects/packages/account-protection/composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/packages/account-protection/composer.json b/projects/packages/account-protection/composer.json index b6a0271497be0..42431a12e7a10 100644 --- a/projects/packages/account-protection/composer.json +++ b/projects/packages/account-protection/composer.json @@ -4,7 +4,9 @@ "type": "jetpack-library", "license": "GPL-2.0-or-later", "require": { - "php": ">=7.2" + "php": ">=7.2", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-status": "@dev" }, "require-dev": { "yoast/phpunit-polyfills": "^1.1.1", From 7c255ac1b616930b01ee5e5a03f926aef603e3ea Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:02:24 -0800 Subject: [PATCH 041/120] Update lock files, add constructor method --- .../src/class-account-protection.php | 22 ++++++++++++++++--- projects/plugins/jetpack/composer.lock | 4 +++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 69cc546a74533..7d2547f9566fb 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -18,6 +18,22 @@ class Account_Protection { const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; + /** + * Modules instance. + * + * @var Modules + */ + private $modules; + + /** + * Account_Protection constructor. + * + * @param Modules $modules Modules instance. + */ + public function __construct( Modules $modules = null ) { + $this->modules = $modules ?? new Modules(); + } + /** * Initializes the configurations needed for the account protection module. */ @@ -50,7 +66,7 @@ public function on_account_protection_deactivation() { * @return bool */ public function is_enabled() { - return ( new Modules() )->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -63,7 +79,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return ( new Modules() )->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); + return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -76,6 +92,6 @@ public function disable() { if ( ! $this->is_enabled() ) { return true; } - return ( new Modules() )->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } } diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index f9e12cf1d5ec8..5c4a01cd5a9f0 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -65,9 +65,11 @@ "dist": { "type": "path", "url": "../../packages/account-protection", - "reference": "c22829e6a80ff9f5cd10e4b4eece3d405f69e8f9" + "reference": "badc1036552f26a900a69608df22284e603981ed" }, "require": { + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-status": "@dev", "php": ">=7.2" }, "require-dev": { From cdb0ac8bea39d29a541a657c2d31bc3f68ae2d4b Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:07:52 -0800 Subject: [PATCH 042/120] Fix php warning --- .../account-protection/src/class-account-protection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 7d2547f9566fb..745900f5d11a2 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -28,9 +28,9 @@ class Account_Protection { /** * Account_Protection constructor. * - * @param Modules $modules Modules instance. + * @param ?Modules $modules Modules instance. */ - public function __construct( Modules $modules = null ) { + public function __construct( ?Modules $modules = null ) { $this->modules = $modules ?? new Modules(); } From 1ce68b95e55a498c1c5ffff8b70027d854b1ceb4 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:16:59 -0800 Subject: [PATCH 043/120] Update lock file --- projects/plugins/protect/composer.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/plugins/protect/composer.lock b/projects/plugins/protect/composer.lock index ffb94a7ebba8d..a81f82f860992 100644 --- a/projects/plugins/protect/composer.lock +++ b/projects/plugins/protect/composer.lock @@ -65,9 +65,11 @@ "dist": { "type": "path", "url": "../../packages/account-protection", - "reference": "c22829e6a80ff9f5cd10e4b4eece3d405f69e8f9" + "reference": "badc1036552f26a900a69608df22284e603981ed" }, "require": { + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-status": "@dev", "php": ">=7.2" }, "require-dev": { From 7a065083ec0239b9f5d4266b1b62368c72c78830 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:19:20 -0800 Subject: [PATCH 044/120] Changelog --- .../changelog/add-protect-account-protection-settings | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/account-protection/changelog/add-protect-account-protection-settings diff --git a/projects/packages/account-protection/changelog/add-protect-account-protection-settings b/projects/packages/account-protection/changelog/add-protect-account-protection-settings new file mode 100644 index 0000000000000..fc22c90153950 --- /dev/null +++ b/projects/packages/account-protection/changelog/add-protect-account-protection-settings @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Moves get_settings method to primary class From c128cf5c51dca3f39c87060228fadb9f50dab9f2 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:38:24 -0800 Subject: [PATCH 045/120] Fix Password_Detection constructor --- .../account-protection/src/class-password-detection.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 29c100405cb85..c665ed9b387fe 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -26,9 +26,9 @@ class Password_Detection { /** * Password_Detection constructor. * - * @param Password_Reset_Email $password_reset_email Password reset email instance. + * @param ?Password_Reset_Email $password_reset_email Password reset email instance. */ - public function __construct( Password_Reset_Email $password_reset_email = null ) { + public function __construct( ?Password_Reset_Email $password_reset_email = null ) { $this->password_reset_email = $password_reset_email ?? new Password_Reset_Email(); } @@ -138,7 +138,7 @@ public function render_page(): void { } } - $this->render_content( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); + $this->render_content( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); exit; } From 7a56b48003b56b569e343e083ea5d3ba655f254d Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:40:12 -0800 Subject: [PATCH 046/120] Changelog --- .../add-packages-account-protection-password-detection-flow | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow diff --git a/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow b/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow new file mode 100644 index 0000000000000..dde7e7363b212 --- /dev/null +++ b/projects/packages/account-protection/changelog/add-packages-account-protection-password-detection-flow @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Adds the password detection flow From bc7aa77287e04964c2b11dff1334082c430ed8b1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:44:21 -0800 Subject: [PATCH 047/120] More changelogs --- .../add-packages-account-protection-password-detection-flow | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/protect/changelog/add-packages-account-protection-password-detection-flow diff --git a/projects/plugins/protect/changelog/add-packages-account-protection-password-detection-flow b/projects/plugins/protect/changelog/add-packages-account-protection-password-detection-flow new file mode 100644 index 0000000000000..06faac5eedf17 --- /dev/null +++ b/projects/plugins/protect/changelog/add-packages-account-protection-password-detection-flow @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Fixes Account Protection endpoint callback From b28c8cf0ca5f2d02ee4f70854f1153998bb67d68 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 14:45:24 -0800 Subject: [PATCH 048/120] Remove comments --- .../src/class-password-reset-email.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-password-reset-email.php index 5d2358317669a..46331e3f5cc2f 100644 --- a/projects/packages/account-protection/src/class-password-reset-email.php +++ b/projects/packages/account-protection/src/class-password-reset-email.php @@ -40,17 +40,7 @@ public function mask_email_address( string $email ): string { * @return bool True if the email was sent successfully, false otherwise. */ public function send( \WP_User $user ): bool { - // $site_url = home_url(); - // $parsed_url = wp_parse_url( $site_url ); - // $domain_name = $parsed_url['host']; - // $username = $user->user_login; - // $email = $user->user_email; - - // $key = get_password_reset_key( $user ); - // $locale = get_user_locale( $user ); - // $password_reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $username ) . "&key=$key&action=rp", 'login' ) . '&wp_lang=' . $locale; - - // TODO: Update to use custom email method when available, passing $domain_name, $email, $username, and $password_reset_link + // TODO: Update to use custom email method when available return $user ? true : false; } } From 4bb5401d7208a5bbf3eb52c0f80387060e1bd10d Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:10:39 -0800 Subject: [PATCH 049/120] Fix static analysis errors --- .../src/class-password-detection.php | 17 +++++++---------- .../src/class-password-reset-email.php | 5 ++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index c665ed9b387fe..1506940e8cddd 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -44,11 +44,11 @@ public function password_detection_redirect(): string { /** * Check if the password is safe after login. * - * @param \WP_User $user The user object. - * @param string $password The password. + * @param \WP_User|\WP_Error $user The user or error object. + * @param string $password The password. * @return \WP_User|\WP_Error The user object. */ - public function login_form_password_detection( \WP_User $user, string $password ): \WP_User { + public function login_form_password_detection( $user, string $password ) { // Check if the user is already a WP_Error object if ( is_wp_error( $user ) ) { return $user; @@ -75,9 +75,9 @@ public function login_form_password_detection( \WP_User $user, string $password /** * Render password detection page. * - * @return void + * @return never */ - public function render_page(): void { + public function render_page(): never { // Restrict direct access to logged in users $current_user = wp_get_current_user(); if ( 0 === $current_user->ID ) { @@ -111,7 +111,7 @@ public function render_page(): void { if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) { // Send password reset email if ( ! $email_sent_flag ) { - $email_sent = $this->password_reset_email->send( $current_user ); + $email_sent = $this->password_reset_email->send(); if ( $email_sent ) { // Set transient to mark the email as sent set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); @@ -187,11 +187,8 @@ public function ajax_resend_password_reset_email() { wp_send_json_error( array( 'message' => 'User not authenticated' ) ); } - $current_user = wp_get_current_user(); - $email = $current_user->user_email; - // Resend the email - $email_sent = $this->password_reset_email->send( $current_user, $email ); + $email_sent = $this->password_reset_email->send(); if ( $email_sent ) { wp_send_json_success( array( 'message' => 'Resend successful.' ) ); } else { diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-password-reset-email.php index 46331e3f5cc2f..7afde4aab79ae 100644 --- a/projects/packages/account-protection/src/class-password-reset-email.php +++ b/projects/packages/account-protection/src/class-password-reset-email.php @@ -36,11 +36,10 @@ public function mask_email_address( string $email ): string { /** * Send password reset email. * - * @param \WP_User $user The user object. * @return bool True if the email was sent successfully, false otherwise. */ - public function send( \WP_User $user ): bool { + public function send(): bool { // TODO: Update to use custom email method when available - return $user ? true : false; + return true; } } From 20dec014e6bac8f0960b540b882f9c214efc7284 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:14:20 -0800 Subject: [PATCH 050/120] Remove top level phpunit.xml.dist --- .../packages/account-protection/phpunit.xml.dist | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 projects/packages/account-protection/phpunit.xml.dist diff --git a/projects/packages/account-protection/phpunit.xml.dist b/projects/packages/account-protection/phpunit.xml.dist deleted file mode 100644 index 3223c32458db2..0000000000000 --- a/projects/packages/account-protection/phpunit.xml.dist +++ /dev/null @@ -1,14 +0,0 @@ - - - - tests/php - - - - - - - src - - - From 2bdbf8ebb96d19491d002b785ea37964a2c8509b Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:17:41 -0800 Subject: [PATCH 051/120] Remove never return type --- .../account-protection/src/class-password-detection.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 1506940e8cddd..c72cde6b45213 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -74,10 +74,8 @@ public function login_form_password_detection( $user, string $password ) { /** * Render password detection page. - * - * @return never */ - public function render_page(): never { + public function render_page() { // Restrict direct access to logged in users $current_user = wp_get_current_user(); if ( 0 === $current_user->ID ) { From a80c024442d3620045afb7e33df70901886b9a6d Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:39:40 -0800 Subject: [PATCH 052/120] Revert tests dir changes in favour of a dedicated task --- .../tests/action-test-coverage.sh | 9 -------- .../tests/php/integration/bootstrap.php | 16 -------------- .../tests/php/integration/phpunit.xml.dist | 21 ------------------- .../tests/php/unit/bootstrap.php | 11 ---------- .../tests/php/unit/phpunit.xml.dist | 21 ------------------- 5 files changed, 78 deletions(-) delete mode 100755 projects/packages/account-protection/tests/action-test-coverage.sh delete mode 100644 projects/packages/account-protection/tests/php/integration/bootstrap.php delete mode 100644 projects/packages/account-protection/tests/php/integration/phpunit.xml.dist delete mode 100644 projects/packages/account-protection/tests/php/unit/bootstrap.php delete mode 100644 projects/packages/account-protection/tests/php/unit/phpunit.xml.dist diff --git a/projects/packages/account-protection/tests/action-test-coverage.sh b/projects/packages/account-protection/tests/action-test-coverage.sh deleted file mode 100755 index 8a7a1e9de6565..0000000000000 --- a/projects/packages/account-protection/tests/action-test-coverage.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -veo pipefail - -EXIT=0 -php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php "$COVERAGE_DIR/integration/php.cov" --configuration tests/php/integration/phpunit.xml.dist || EXIT=1 -php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php "$COVERAGE_DIR/unit/php.cov" --configuration tests/php/unit/phpunit.xml.dist || EXIT=1 - -exit $EXIT diff --git a/projects/packages/account-protection/tests/php/integration/bootstrap.php b/projects/packages/account-protection/tests/php/integration/bootstrap.php deleted file mode 100644 index 4c1205f352a1d..0000000000000 --- a/projects/packages/account-protection/tests/php/integration/bootstrap.php +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - ../../../src - - - - - . - - - diff --git a/projects/packages/account-protection/tests/php/unit/bootstrap.php b/projects/packages/account-protection/tests/php/unit/bootstrap.php deleted file mode 100644 index e16bad0ecf0bf..0000000000000 --- a/projects/packages/account-protection/tests/php/unit/bootstrap.php +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - ../../../src - - - - - . - - - From f07e52ec53734c45fb5593cef7ae086d3fa05cba Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:41:22 -0800 Subject: [PATCH 053/120] Add tests dir --- .../packages/account-protection/phpunit.xml.dist | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 projects/packages/account-protection/phpunit.xml.dist diff --git a/projects/packages/account-protection/phpunit.xml.dist b/projects/packages/account-protection/phpunit.xml.dist new file mode 100644 index 0000000000000..3223c32458db2 --- /dev/null +++ b/projects/packages/account-protection/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + tests/php + + + + + + + src + + + From 80d0e927f94140b38069fd1deebdaf01003d9da7 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:44:10 -0800 Subject: [PATCH 054/120] Reapply default test infrastructure --- .../account-protection/tests/php/bootstrap.php | 11 +++++++++++ .../packages/account-protection/tests/php/php.dir.xml | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 projects/packages/account-protection/tests/php/bootstrap.php create mode 100644 projects/packages/account-protection/tests/php/php.dir.xml diff --git a/projects/packages/account-protection/tests/php/bootstrap.php b/projects/packages/account-protection/tests/php/bootstrap.php new file mode 100644 index 0000000000000..46763b04a2cdb --- /dev/null +++ b/projects/packages/account-protection/tests/php/bootstrap.php @@ -0,0 +1,11 @@ + + + + From c03d626898489fad9ae02a0e177ed4eed41b6633 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:45:28 -0800 Subject: [PATCH 055/120] Reorg and rename --- .../account-protection/tests/{php/php.dir.xml => .phpcs.dir.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename projects/packages/account-protection/tests/{php/php.dir.xml => .phpcs.dir.xml} (100%) diff --git a/projects/packages/account-protection/tests/php/php.dir.xml b/projects/packages/account-protection/tests/.phpcs.dir.xml similarity index 100% rename from projects/packages/account-protection/tests/php/php.dir.xml rename to projects/packages/account-protection/tests/.phpcs.dir.xml From ae3b6b6722ab9915b6e1d8cdc02666d5cc9276f6 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 15:46:42 -0800 Subject: [PATCH 056/120] Update @package --- projects/packages/account-protection/tests/php/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/account-protection/tests/php/bootstrap.php b/projects/packages/account-protection/tests/php/bootstrap.php index 46763b04a2cdb..c53f9cb5415c3 100644 --- a/projects/packages/account-protection/tests/php/bootstrap.php +++ b/projects/packages/account-protection/tests/php/bootstrap.php @@ -2,7 +2,7 @@ /** * Bootstrap. * - * @package automattic/ + * @package automattic/jetpack-account-protection */ /** From 30f23292f63df71726b521fb885bf0c75773ce56 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 20 Jan 2025 16:02:37 -0800 Subject: [PATCH 057/120] Use never phpdoc return type as per static analysis error --- .../account-protection/src/class-password-detection.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index c72cde6b45213..ca195ed1207e6 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -74,6 +74,8 @@ public function login_form_password_detection( $user, string $password ) { /** * Render password detection page. + * + * @return never */ public function render_page() { // Restrict direct access to logged in users From aad7ff6c09bf85a041e1d059b20ade9aa97e2b8e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 21 Jan 2025 12:52:21 -0800 Subject: [PATCH 058/120] Enable module by default --- projects/plugins/jetpack/modules/account-protection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/modules/account-protection.php b/projects/plugins/jetpack/modules/account-protection.php index 554570f666289..c552efec4cc41 100644 --- a/projects/plugins/jetpack/modules/account-protection.php +++ b/projects/plugins/jetpack/modules/account-protection.php @@ -6,7 +6,7 @@ * First Introduced: 14.3 * Requires Connection: Yes * Requires User Connection: No - * Auto Activate: No + * Auto Activate: Yes * Module Tags: Account Protection * Feature: Security * From 448079b41dc8fa17a95dcfc51c2e4f9f4c5901bf Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 21 Jan 2025 12:53:39 -0800 Subject: [PATCH 059/120] Enable module by default --- projects/plugins/protect/src/class-jetpack-protect.php | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 715a6b02cb7a1..ea02244a44e5a 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -292,6 +292,7 @@ public static function activate_modules() { delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ); ( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false ); ( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false ); + ( new Modules() )->activate( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, false, false ); } /** From 4b1837523d4de18faf0e56658ba16354a1755084 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 10:17:49 -0800 Subject: [PATCH 060/120] Remove all reference to and functionality of strict mode --- ...tpack-account-protection-security-settings | 4 - projects/js-packages/api/index.jsx | 10 -- .../add-protect-account-protection-settings | 4 - .../src/class-account-protection.php | 17 --- .../src/class-rest-controller.php | 104 ------------- .../index.jsx | 44 ------ .../_inc/client/lib/plans/constants.js | 2 - .../client/security/account-protection.jsx | 144 +----------------- .../jetpack/_inc/client/security/style.scss | 20 --- .../state/account-protection/actions.js | 66 -------- .../client/state/account-protection/index.js | 2 - .../state/account-protection/reducer.js | 87 ----------- .../jetpack/_inc/client/state/action-types.js | 9 -- .../jetpack/_inc/client/state/reducer.js | 2 - .../lib/class.core-rest-api-endpoints.php | 8 - ....wpcom-json-api-site-settings-endpoint.php | 2 - .../protect/src/class-jetpack-protect.php | 6 +- .../protect/src/class-rest-controller.php | 2 +- projects/plugins/protect/src/js/api.ts | 7 - .../use-account-protection-mutation.ts | 57 ------- ...ggle-account-protection-module-mutation.ts | 15 +- .../use-account-protection-data/index.jsx | 59 ------- .../protect/src/js/routes/settings/index.jsx | 76 +++------ .../src/js/types/account-protection.ts | 12 -- 24 files changed, 29 insertions(+), 730 deletions(-) delete mode 100644 projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings delete mode 100644 projects/packages/account-protection/changelog/add-protect-account-protection-settings delete mode 100644 projects/packages/account-protection/src/class-rest-controller.php delete mode 100644 projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/actions.js delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/index.js delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js delete mode 100644 projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts delete mode 100644 projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx delete mode 100644 projects/plugins/protect/src/js/types/account-protection.ts diff --git a/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings b/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings deleted file mode 100644 index 778ccde6854ed..0000000000000 --- a/projects/js-packages/api/changelog/add-jetpack-account-protection-security-settings +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Adds Account Protection requests diff --git a/projects/js-packages/api/index.jsx b/projects/js-packages/api/index.jsx index 6f6cdffe0b325..8233d0ba8a616 100644 --- a/projects/js-packages/api/index.jsx +++ b/projects/js-packages/api/index.jsx @@ -510,16 +510,6 @@ function JetpackRestApiClient( root, nonce ) { getRequest( `${ wpcomOriginApiUrl }jetpack/v4/search/stats`, getParams ) .then( checkStatus ) .then( parseJsonResponse ), - fetchAccountProtectionSettings: () => - getRequest( `${ apiRoot }jetpack/v4/account-protection`, getParams ) - .then( checkStatus ) - .then( parseJsonResponse ), - updateAccountProtectionSettings: newSettings => - postRequest( `${ apiRoot }jetpack/v4/account-protection`, postParams, { - body: JSON.stringify( newSettings ), - } ) - .then( checkStatus ) - .then( parseJsonResponse ), fetchWafSettings: () => getRequest( `${ apiRoot }jetpack/v4/waf`, getParams ) .then( checkStatus ) diff --git a/projects/packages/account-protection/changelog/add-protect-account-protection-settings b/projects/packages/account-protection/changelog/add-protect-account-protection-settings deleted file mode 100644 index fc22c90153950..0000000000000 --- a/projects/packages/account-protection/changelog/add-protect-account-protection-settings +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: changed - -Moves get_settings method to primary class diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index a63d54545889f..2db8aaf73582d 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -15,7 +15,6 @@ class Account_Protection { const PACKAGE_VERSION = '0.1.0-alpha'; const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; - const STRICT_MODE_OPTION_NAME = 'jetpack_account_protection_strict_mode'; /** * Modules instance. @@ -64,9 +63,6 @@ private function register_hooks(): void { // Do not run in unsupported environments add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) ); add_action( 'jetpack_get_available_standalone_modules', array( $this, 'remove_standalone_module_on_unsupported_environments' ) ); - - // Register REST routes - add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); } /** @@ -189,17 +185,4 @@ function ( $module ) { return $modules; } - - /** - * Get the account protection settings. - * - * @return array - */ - public function get_settings(): array { - $settings = array( - self::STRICT_MODE_OPTION_NAME => get_option( self::STRICT_MODE_OPTION_NAME, false ), - ); - - return $settings; - } } diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php deleted file mode 100644 index a49c1b1fe7c65..0000000000000 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ /dev/null @@ -1,104 +0,0 @@ -routes_registered ) { - return; - } - - register_rest_route( - 'jetpack/v4', - '/account-protection', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_settings' ), - 'permission_callback' => array( $this, 'permissions_callback' ), - ) - ); - - register_rest_route( - 'jetpack/v4', - '/account-protection', - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_settings' ), - 'permission_callback' => array( $this, 'permissions_callback' ), - ) - ); - - $this->routes_registered = true; - } - - /** - * Account Protection Settings Endpoint - * - * @return WP_REST_Response - */ - public function get_settings() { - $settings = ( new Account_Protection() )->get_settings(); - - return rest_ensure_response( $settings ); - } - - /** - * Update Account Protection Settings Endpoint - * - * @param WP_REST_Request $request The API request. - * - * @return WP_REST_Response|WP_Error - */ - public function update_settings( $request ) { - // Strict Mode - if ( isset( $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ) ) { - update_option( Account_Protection::STRICT_MODE_OPTION_NAME, $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ? '1' : '' ); - } - - return $this->get_settings(); - } - - /** - * Account Protection Endpoint Permissions Callback - * - * @return bool|WP_Error True if user can view the Jetpack admin page. - */ - public function permissions_callback() { - if ( current_user_can( 'manage_options' ) ) { - return true; - } - - return new WP_Error( - 'invalid_user_permission_manage_options', - REST_Connector::get_user_permissions_error_msg(), - array( 'status' => rest_authorization_required_code() ) - ); - } -} diff --git a/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx b/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx deleted file mode 100644 index d86ec79e0917b..0000000000000 --- a/projects/plugins/jetpack/_inc/client/components/data/query-account-protection-settings/index.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { - fetchAccountProtectionSettings, - isFetchingAccountProtectionSettings, -} from 'state/account-protection'; -import { isOfflineMode } from 'state/connection'; - -class QueryAccountProtectionSettings extends Component { - static propTypes = { - isFetchingAccountProtectionSettings: PropTypes.bool, - isOfflineMode: PropTypes.bool, - }; - - static defaultProps = { - isFetchingAccountProtectionSettings: false, - isOfflineMode: false, - }; - - componentDidMount() { - if ( ! this.props.isFetchingAccountProtectionSettings && ! this.props.isOfflineMode ) { - this.props.fetchAccountProtectionSettings(); - } - } - - render() { - return null; - } -} - -export default connect( - state => { - return { - isFetchingAccountProtectionSettings: isFetchingAccountProtectionSettings( state ), - isOfflineMode: isOfflineMode( state ), - }; - }, - dispatch => { - return { - fetchAccountProtectionSettings: () => dispatch( fetchAccountProtectionSettings() ), - }; - } -)( QueryAccountProtectionSettings ); diff --git a/projects/plugins/jetpack/_inc/client/lib/plans/constants.js b/projects/plugins/jetpack/_inc/client/lib/plans/constants.js index 12a0743eb48cc..0a486259173e5 100644 --- a/projects/plugins/jetpack/_inc/client/lib/plans/constants.js +++ b/projects/plugins/jetpack/_inc/client/lib/plans/constants.js @@ -417,7 +417,6 @@ export const FEATURE_POST_BY_EMAIL = 'post-by-email-jetpack'; export const FEATURE_JETPACK_SOCIAL = 'social-jetpack'; export const FEATURE_JETPACK_BLAZE = 'blaze-jetpack'; export const FEATURE_JETPACK_EARN = 'earn-jetpack'; -export const FEATURE_JETPACK_ACCOUNT_PROTECTION = 'account-protection-jetpack'; // Upsells export const JETPACK_FEATURE_PRODUCT_UPSELL_MAP = { @@ -440,7 +439,6 @@ export const JETPACK_FEATURE_PRODUCT_UPSELL_MAP = { [ FEATURE_VIDEOPRESS ]: PLAN_JETPACK_VIDEOPRESS, [ FEATURE_NEWSLETTER_JETPACK ]: PLAN_JETPACK_CREATOR_YEARLY, [ FEATURE_WORDADS_JETPACK ]: PLAN_JETPACK_SECURITY_T1_YEARLY, - [ FEATURE_JETPACK_ACCOUNT_PROTECTION ]: PLAN_JETPACK_FREE, }; /** diff --git a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx index 7334eb9e3ca7d..4f8f51b250f10 100644 --- a/projects/plugins/jetpack/_inc/client/security/account-protection.jsx +++ b/projects/plugins/jetpack/_inc/client/security/account-protection.jsx @@ -1,98 +1,14 @@ -import { ToggleControl } from '@automattic/jetpack-components'; -import { ExternalLink } from '@wordpress/components'; -import { createInterpolateElement } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { FormFieldset } from 'components/forms'; -import { createNotice, removeNotice } from 'components/global-notices/state/notices/actions'; import { withModuleSettingsFormHelpers } from 'components/module-settings/with-module-settings-form-helpers'; import { ModuleToggle } from 'components/module-toggle'; import SettingsCard from 'components/settings-card'; import SettingsGroup from 'components/settings-group'; -import QueryAccountProtectionSettings from '../components/data/query-account-protection-settings'; -import InfoPopover from '../components/info-popover'; -import { FEATURE_JETPACK_ACCOUNT_PROTECTION } from '../lib/plans/constants'; -import { updateAccountProtectionSettings } from '../state/account-protection/actions'; -import { - getAccountProtectionSettings, - isFetchingAccountProtectionSettings, - isUpdatingAccountProtectionSettings, -} from '../state/account-protection/reducer'; - -const AccountProtection = class extends Component { - /** - * Get options for initial state. - * - * @return {object} - */ - state = { - strictMode: this.props.settings?.strictMode, - }; - - /** - * Keep the form values in sync with updates to the settings prop. - * - * @param {object} prevProps - Next render props. - */ - componentDidUpdate = prevProps => { - // Sync the form values with the settings prop. - if ( this.props.settings !== prevProps.settings ) { - this.setState( { - ...this.state, - strictMode: this.props.settings?.strictMode, - } ); - } - }; - - /** - * Handle settings updates. - * - * @return {void} - */ - onSubmit = () => { - this.props.removeNotice( 'module-setting-update' ); - this.props.removeNotice( 'module-setting-update-success' ); - - this.props.createNotice( 'is-info', __( 'Updating settings…', 'jetpack' ), { - id: 'module-setting-update', - } ); - this.props - .updateAccountProtectionSettings( this.state ) - .then( () => { - this.props.removeNotice( 'module-setting-update' ); - this.props.createNotice( 'is-success', __( 'Updated Settings.', 'jetpack' ), { - id: 'module-setting-update-success', - } ); - } ) - .catch( () => { - this.props.removeNotice( 'module-setting-update' ); - this.props.createNotice( 'is-error', __( 'Error updating settings.', 'jetpack' ), { - id: 'module-setting-update', - } ); - } ); - }; - - /** - * Toggle strict mode. - */ - toggleStrictMode = () => { - const state = { - ...this.state, - strictMode: ! this.state.strictMode, - }; - - this.setState( state, this.onSubmit ); - }; +const AccountProtectionComponent = class extends Component { render() { const isAccountProtectionActive = this.props.getOptionValue( 'account-protection' ), unavailableInOfflineMode = this.props.isUnavailableInOfflineMode( 'account-protection' ); - const baseInputDisabledCase = - ! isAccountProtectionActive || - unavailableInOfflineMode || - this.props.isFetchingAccountProtectionSettings || - this.props.isSavingAnyOption( [ 'account-protection' ] ); return ( - { isAccountProtectionActive && } - { isAccountProtectionActive && ( - -
- - - { __( 'Require strong passwords', 'jetpack' ) } - - - { createInterpolateElement( - __( - 'Allow Jetpack to enforce strict password rules. Learn more
Privacy Information', - 'jetpack' - ), - { - ExternalLink: , // TODO: Update this redirect URL - hr:
, - } - ) } -
-
- } - /> -
- - ) } ); } }; -export default connect( - state => { - return { - isFetchingSettings: isFetchingAccountProtectionSettings( state ), - isUpdatingAccountProtectionSettings: isUpdatingAccountProtectionSettings( state ), - settings: getAccountProtectionSettings( state ), - }; - }, - dispatch => { - return { - updateAccountProtectionSettings: newSettings => - dispatch( updateAccountProtectionSettings( newSettings ) ), - createNotice: ( type, message, props ) => dispatch( createNotice( type, message, props ) ), - removeNotice: notice => dispatch( removeNotice( notice ) ), - }; - } -)( withModuleSettingsFormHelpers( AccountProtection ) ); +export const AccountProtection = withModuleSettingsFormHelpers( AccountProtectionComponent ); diff --git a/projects/plugins/jetpack/_inc/client/security/style.scss b/projects/plugins/jetpack/_inc/client/security/style.scss index 385e7feaa710f..60855aa333edb 100644 --- a/projects/plugins/jetpack/_inc/client/security/style.scss +++ b/projects/plugins/jetpack/_inc/client/security/style.scss @@ -192,23 +192,3 @@ .jp-form-settings-group p { margin-bottom: 0.5rem; } - -.account-protection__settings { - &__toggle-setting { - flex-wrap: wrap; - display: flex; - margin-bottom: 24px; - - &__label { - display: flex; - align-items: center; - } - } - - &__strict-mode-popover { - display: flex; - align-items: center; - margin-left: 4px; - } - -} \ No newline at end of file diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js deleted file mode 100644 index feee531d78a38..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js +++ /dev/null @@ -1,66 +0,0 @@ -import restApi from '@automattic/jetpack-api'; -import { - ACCOUNT_PROTECTION_SETTINGS_FETCH, - ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - ACCOUNT_PROTECTION_SETTINGS_UPDATE, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, -} from 'state/action-types'; - -export const fetchAccountProtectionSettings = () => { - return dispatch => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH, - } ); - return restApi - .fetchAccountProtectionSettings() - .then( settings => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - settings, - } ); - return settings; - } ) - .catch( error => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - error: error, - } ); - } ); - }; -}; - -/** - * Update Account Protection Settings - * - * @param {object} newSettings - The new settings to be saved. - * @param {boolean} newSettings.strictMode - Whether strict mode is enabled. - * @return {Function} - The action. - */ -export const updateAccountProtectionSettings = newSettings => { - return dispatch => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE, - } ); - return restApi - .updateAccountProtectionSettings( { - jetpack_account_protection_strict_mode: newSettings.strictMode, - } ) - .then( settings => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - settings, - } ); - return settings; - } ) - .catch( error => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, - error: error, - } ); - - throw error; - } ); - }; -}; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js deleted file mode 100644 index 5e3164b4c9f72..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './reducer'; -export * from './actions'; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js deleted file mode 100644 index cb42d7bccc486..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js +++ /dev/null @@ -1,87 +0,0 @@ -import { assign, get } from 'lodash'; -import { combineReducers } from 'redux'; -import { - ACCOUNT_PROTECTION_SETTINGS_FETCH, - ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - ACCOUNT_PROTECTION_SETTINGS_UPDATE, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, -} from 'state/action-types'; - -export const data = ( state = {}, action ) => { - switch ( action.type ) { - case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: - return assign( {}, state, { - strictMode: Boolean( action.settings?.jetpack_account_protection_strict_mode ), - } ); - default: - return state; - } -}; - -export const initialRequestsState = { - isFetchingAccountProtectionSettings: false, - isUpdatingAccountProtectionSettings: false, -}; - -export const requests = ( state = initialRequestsState, action ) => { - switch ( action.type ) { - case ACCOUNT_PROTECTION_SETTINGS_FETCH: - return assign( {}, state, { - isFetchingAccountProtectionSettings: true, - } ); - case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: - case ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL: - return assign( {}, state, { - isFetchingAccountProtectionSettings: false, - } ); - case ACCOUNT_PROTECTION_SETTINGS_UPDATE: - return assign( {}, state, { - isUpdatingAccountProtectionSettings: true, - } ); - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL: - return assign( {}, state, { - isUpdatingAccountProtectionSettings: false, - } ); - default: - return state; - } -}; - -export const reducer = combineReducers( { - data, - requests, -} ); - -/** - * Returns true if currently requesting the account protection settings. Otherwise false. - * - * @param {object} state - Global state tree - * @return {boolean} Whether the account protection settings are being requested - */ -export function isFetchingAccountProtectionSettings( state ) { - return !! state.jetpack.accountProtection.requests.isFetchingAccountProtectionSettings; -} - -/** - * Returns true if currently updating the account protection settings. Otherwise false. - * - * @param {object} state - Global state tree - * @return {boolean} Whether the account protection settings are being requested - */ -export function isUpdatingAccountProtectionSettings( state ) { - return !! state.jetpack.accountProtection.requests.isUpdatingAccountProtectionSettings; -} - -/** - * Returns the account protection's settings. - * - * @param {object} state - Global state tree - * @return {string} File path to bootstrap.php - */ -export function getAccountProtectionSettings( state ) { - return get( state.jetpack.accountProtection, [ 'data' ], {} ); -} diff --git a/projects/plugins/jetpack/_inc/client/state/action-types.js b/projects/plugins/jetpack/_inc/client/state/action-types.js index 7da1fbb07cf1d..c4785d4a2ced5 100644 --- a/projects/plugins/jetpack/_inc/client/state/action-types.js +++ b/projects/plugins/jetpack/_inc/client/state/action-types.js @@ -245,15 +245,6 @@ export const JETPACK_LICENSING_GET_USER_LICENSES_FAILURE = export const JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL = 'JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH = 'ACCOUNT_PROTECTION_SETTINGS_FETCH'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE = - 'ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS = - 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL'; - export const WAF_SETTINGS_FETCH = 'WAF_SETTINGS_FETCH'; export const WAF_SETTINGS_FETCH_RECEIVE = 'WAF_SETTINGS_FETCH_RECEIVE'; export const WAF_SETTINGS_FETCH_FAIL = 'WAF_SETTINGS_FETCH_FAIL'; diff --git a/projects/plugins/jetpack/_inc/client/state/reducer.js b/projects/plugins/jetpack/_inc/client/state/reducer.js index 14e85f0fb5289..5ff156b807a49 100644 --- a/projects/plugins/jetpack/_inc/client/state/reducer.js +++ b/projects/plugins/jetpack/_inc/client/state/reducer.js @@ -1,6 +1,5 @@ import { combineReducers } from 'redux'; import { globalNotices } from 'components/global-notices/state/notices/reducer'; -import { reducer as accountProtection } from 'state/account-protection/reducer'; import { dashboard } from 'state/at-a-glance/reducer'; import { reducer as connection } from 'state/connection/reducer'; import { reducer as devCard } from 'state/dev-version/reducer'; @@ -47,7 +46,6 @@ const jetpackReducer = combineReducers( { disconnectSurvey, trackingSettings, licensing, - accountProtection, waf, introOffers, } ); diff --git a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php index 06cf25d1268f7..86c23729166e2 100644 --- a/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php +++ b/projects/plugins/jetpack/_inc/lib/class.core-rest-api-endpoints.php @@ -2358,14 +2358,6 @@ public static function get_updateable_data_list( $selector = '' ) { 'validate_callback' => __CLASS__ . '::validate_posint', 'jp_group' => 'custom-content-types', ), - // Account Protection. - 'jetpack_account_protection_strict_mode' => array( - 'description' => esc_html__( 'Strict mode - Require strong passwords.', 'jetpack' ), - 'type' => 'boolean', - 'default' => 0, - 'validate_callback' => __CLASS__ . '::validate_boolean', - 'jp_group' => 'account-protection', - ), // WAF. 'jetpack_waf_automatic_rules' => array( 'description' => esc_html__( 'Enable automatic rules - Protect your site against untrusted traffic sources with automatic security rules.', 'jetpack' ), diff --git a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php index 4b931dadb330f..9852478a7c53d 100644 --- a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php +++ b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php @@ -127,7 +127,6 @@ 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled', 'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled', 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt', - 'jetpack_account_protection_strict_mode' => '(bool) Whether to enforce strict password requirements', 'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules', 'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow', 'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled', @@ -491,7 +490,6 @@ function ( $newsletter_category ) { 'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ), 'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ), 'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ), - 'jetpack_account_protection_strict_mode' => (bool) get_option( 'jetpack_account_protection_strict_mode' ), 'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ), 'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ), 'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ), diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index ea02244a44e5a..e67b6eddb26f5 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -214,7 +214,6 @@ public function initial_state() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $refresh_status_from_wpcom = isset( $_GET['checkPlan'] ); $status = Status::get_status( $refresh_status_from_wpcom ); - $account_protection = new Account_Protection(); $initial_state = array( 'apiRoot' => esc_url_raw( rest_url() ), @@ -233,10 +232,7 @@ public function initial_state() { 'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ), 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), - 'accountProtection' => array( - 'isEnabled' => $account_protection->is_enabled(), - 'settings' => $account_protection->get_settings(), - ), + 'accountProtection' => ( new Account_Protection() )->is_enabled(), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), 'currentIp' => IP_Utils::get_ip(), diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index b6ddb432afa23..fb6dd1dba9fd4 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -403,7 +403,7 @@ public static function api_toggle_account_protection() { * @return WP_Rest_Response */ public static function api_get_account_protection() { - return new WP_REST_Response( ( new Account_Protection() )->get_settings() ); + return new WP_REST_Response( ( new Account_Protection() )->is_enabled() ); } /** diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts index 186ac89c2c513..98b543160b475 100644 --- a/projects/plugins/protect/src/js/api.ts +++ b/projects/plugins/protect/src/js/api.ts @@ -17,13 +17,6 @@ const API = { path: 'jetpack-protect/v1/toggle-account-protection', } ), - updateAccountProtection: data => - apiFetch( { - method: 'POST', - path: 'jetpack/v4/account-protection', - data, - } ).then( camelize ), - getWaf: (): Promise< WafStatus > => apiFetch( { path: 'jetpack-protect/v1/waf', diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts deleted file mode 100644 index abd7749a797cd..0000000000000 --- a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; -import { __ } from '@wordpress/i18n'; -import camelize from 'camelize'; -import API from '../../api'; -import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; -import useNotices from '../../hooks/use-notices'; -import { AccountProtectionStatus } from '../../types/account-protection'; - -/** - * Account Protection Mutatation Hook - * - * @return {UseMutationResult} useMutation result. - */ -export default function useAccountProtectionMutation(): UseMutationResult< - unknown, - { [ key: string ]: unknown }, - unknown, - { initialValue: AccountProtectionStatus } -> { - const queryClient = useQueryClient(); - const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); - - return useMutation( { - mutationFn: API.updateAccountProtection, - onMutate: settings => { - showSavingNotice(); - - // Get the current Account Protection settings. - const initialValue = queryClient.getQueryData( [ - QUERY_ACCOUNT_PROTECTION_KEY, - ] ) as AccountProtectionStatus; - - // Optimistically update the Account Protection settings. - queryClient.setQueryData( - [ QUERY_ACCOUNT_PROTECTION_KEY ], - ( accountProtectionStatus: AccountProtectionStatus ) => ( { - ...accountProtectionStatus, - settings: { - ...accountProtectionStatus.settings, - ...camelize( settings ), - }, - } ) - ); - - return { initialValue }; - }, - onSuccess: () => { - showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); - }, - onError: ( error, variables, context ) => { - // Reset the WAF config to its previous state. - queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], context.initialValue ); - - showErrorNotice( __( 'Error saving changes.', 'jetpack-protect' ) ); - }, - } ); -} diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts index 2f8ca342902ea..4032d0e6a42e6 100644 --- a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts +++ b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts @@ -3,7 +3,6 @@ import { __ } from '@wordpress/i18n'; import API from '../../api'; import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; import useNotices from '../../hooks/use-notices'; -import { AccountProtectionStatus } from '../../types/account-protection'; /** * Toggle Account Protection Mutatation @@ -20,18 +19,12 @@ export default function useToggleAccountProtectionMutation(): UseMutationResult showSavingNotice(); // Get the current Account Protection settings. - const initialValue = queryClient.getQueryData( [ - QUERY_ACCOUNT_PROTECTION_KEY, - ] ) as AccountProtectionStatus; + const initialValue = queryClient.getQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ] ); + + console.log( initialValue ); // TODO: Test the toggleAccountProtection mutation... // Optimistically update the Account Protection settings. - queryClient.setQueryData( - [ QUERY_ACCOUNT_PROTECTION_KEY ], - ( accountProtectionStatus: AccountProtectionStatus ) => ( { - ...accountProtectionStatus, - isEnabled: ! initialValue.isEnabled, - } ) - ); + // queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], () => ! initialValue ); return { initialValue }; }, diff --git a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx deleted file mode 100644 index 90e473c270bc6..0000000000000 --- a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback } from 'react'; -import useAccountProtectionMutation from '../../data/account-protection/use-account-protection-mutation'; -import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; -import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-module-mutation'; -import useAnalyticsTracks from '../use-analytics-tracks'; - -/** - * Use Account Protection Data Hook - * - * @return {object} Account Protection data and methods for interacting with it. - */ -const useAccountProtectionData = () => { - const { recordEvent } = useAnalyticsTracks(); - const { data: accountProtection } = useAccountProtectionQuery(); - const accountProtectionMutation = useAccountProtectionMutation(); - const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); - - /** - * Toggle Account Protection Module - * - * Flips the switch on the Account Protection module, and then refreshes the data. - */ - const toggleAccountProtection = useCallback( async () => { - toggleAccountProtectionMutation.mutate(); - }, [ toggleAccountProtectionMutation ] ); - - /** - * Toggle Strict Mode - * - * Flips the switch on the strict mode option, and then refreshes the data. - */ - const toggleStrictMode = useCallback( async () => { - const value = ! accountProtection.settings.jetpackAccountProtectionStrictMode; - const mutationObj = { jetpack_account_protection_strict_mode: value }; - if ( ! value ) { - mutationObj.jetpack_account_protection_strict_mode = false; - } - await accountProtectionMutation.mutateAsync( mutationObj ); - recordEvent( - mutationObj - ? 'jetpack_account_protection_strict_mode_enabled' - : 'jetpack_account_protection_strict_mode_disabled' - ); - }, [ - recordEvent, - accountProtection.settings.jetpackAccountProtectionStrictMode, - accountProtectionMutation, - ] ); - - return { - ...accountProtection, - isUpdating: accountProtectionMutation.isPending, - isToggling: toggleAccountProtectionMutation.isPending, - toggleAccountProtection, - toggleStrictMode, - }; -}; - -export default useAccountProtectionData; diff --git a/projects/plugins/protect/src/js/routes/settings/index.jsx b/projects/plugins/protect/src/js/routes/settings/index.jsx index 3fe6b735f7d3e..41904da5a20d7 100644 --- a/projects/plugins/protect/src/js/routes/settings/index.jsx +++ b/projects/plugins/protect/src/js/routes/settings/index.jsx @@ -9,21 +9,16 @@ import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon, warning } from '@wordpress/icons'; import AdminPage from '../../components/admin-page'; -import useAccountProtectionData from '../../hooks/use-account-protection-data'; +import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; +import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-mutation'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import usePlan from '../../hooks/use-plan'; import styles from './styles.module.scss'; const SettingsPage = () => { const { hasPlan } = usePlan(); - const { - settings: { jetpackAccountProtectionStrictMode: strictMode }, - isEnabled: isAccountProtectionEnabled, - toggleAccountProtection, - toggleStrictMode, - isToggling, - isUpdating, - } = useAccountProtectionData(); + const { data: accountProtection } = useAccountProtectionQuery(); + const toggleAccountProtection = useToggleAccountProtectionMutation(); // Track view for Protect Account Protection page. useAnalyticsTracks( { @@ -37,9 +32,9 @@ const SettingsPage = () => {
@@ -57,46 +52,20 @@ const SettingsPage = () => { } ) } -
-
- ); - - const strictModeSettings = ( - ); @@ -109,10 +78,7 @@ const SettingsPage = () => { -
- { accountProtectionSettings } - { isAccountProtectionEnabled && strictModeSettings } -
+
{ accountProtectionSettings }
diff --git a/projects/plugins/protect/src/js/types/account-protection.ts b/projects/plugins/protect/src/js/types/account-protection.ts deleted file mode 100644 index 37d557638982b..0000000000000 --- a/projects/plugins/protect/src/js/types/account-protection.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type AccountProtectionStatus = { - /** Whether the "account-protection" module is enabled. */ - isEnabled: boolean; - - /** The current Account Protetion settings. */ - settings: AccountProtectionSettings; -}; - -export type AccountProtectionSettings = { - /** Whether the user has enabled strict mode. */ - jetpackAccountProtectionStrictMode: boolean; -}; From 7d72fd96131d5f446eb8900cc0529cc95632257e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 13:19:20 -0800 Subject: [PATCH 061/120] Remove unneeded strict mode code, update Protect settings UI --- .../state/account-protection/actions.js | 66 -------------- .../client/state/account-protection/index.js | 2 - .../state/account-protection/reducer.js | 87 ------------------- ....wpcom-json-api-site-settings-endpoint.php | 2 - .../use-account-protection-mutation.ts | 57 ------------ ...ggle-account-protection-module-mutation.ts | 11 ++- .../use-account-protection-data/index.jsx | 59 ------------- .../protect/src/js/routes/settings/index.jsx | 18 +++- 8 files changed, 19 insertions(+), 283 deletions(-) delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/actions.js delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/index.js delete mode 100644 projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js delete mode 100644 projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts delete mode 100644 projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js b/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js deleted file mode 100644 index feee531d78a38..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/actions.js +++ /dev/null @@ -1,66 +0,0 @@ -import restApi from '@automattic/jetpack-api'; -import { - ACCOUNT_PROTECTION_SETTINGS_FETCH, - ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - ACCOUNT_PROTECTION_SETTINGS_UPDATE, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, -} from 'state/action-types'; - -export const fetchAccountProtectionSettings = () => { - return dispatch => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH, - } ); - return restApi - .fetchAccountProtectionSettings() - .then( settings => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - settings, - } ); - return settings; - } ) - .catch( error => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - error: error, - } ); - } ); - }; -}; - -/** - * Update Account Protection Settings - * - * @param {object} newSettings - The new settings to be saved. - * @param {boolean} newSettings.strictMode - Whether strict mode is enabled. - * @return {Function} - The action. - */ -export const updateAccountProtectionSettings = newSettings => { - return dispatch => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE, - } ); - return restApi - .updateAccountProtectionSettings( { - jetpack_account_protection_strict_mode: newSettings.strictMode, - } ) - .then( settings => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - settings, - } ); - return settings; - } ) - .catch( error => { - dispatch( { - type: ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, - error: error, - } ); - - throw error; - } ); - }; -}; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js b/projects/plugins/jetpack/_inc/client/state/account-protection/index.js deleted file mode 100644 index 5e3164b4c9f72..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './reducer'; -export * from './actions'; diff --git a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js b/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js deleted file mode 100644 index cb42d7bccc486..0000000000000 --- a/projects/plugins/jetpack/_inc/client/state/account-protection/reducer.js +++ /dev/null @@ -1,87 +0,0 @@ -import { assign, get } from 'lodash'; -import { combineReducers } from 'redux'; -import { - ACCOUNT_PROTECTION_SETTINGS_FETCH, - ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE, - ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL, - ACCOUNT_PROTECTION_SETTINGS_UPDATE, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS, - ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL, -} from 'state/action-types'; - -export const data = ( state = {}, action ) => { - switch ( action.type ) { - case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: - return assign( {}, state, { - strictMode: Boolean( action.settings?.jetpack_account_protection_strict_mode ), - } ); - default: - return state; - } -}; - -export const initialRequestsState = { - isFetchingAccountProtectionSettings: false, - isUpdatingAccountProtectionSettings: false, -}; - -export const requests = ( state = initialRequestsState, action ) => { - switch ( action.type ) { - case ACCOUNT_PROTECTION_SETTINGS_FETCH: - return assign( {}, state, { - isFetchingAccountProtectionSettings: true, - } ); - case ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE: - case ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL: - return assign( {}, state, { - isFetchingAccountProtectionSettings: false, - } ); - case ACCOUNT_PROTECTION_SETTINGS_UPDATE: - return assign( {}, state, { - isUpdatingAccountProtectionSettings: true, - } ); - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS: - case ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL: - return assign( {}, state, { - isUpdatingAccountProtectionSettings: false, - } ); - default: - return state; - } -}; - -export const reducer = combineReducers( { - data, - requests, -} ); - -/** - * Returns true if currently requesting the account protection settings. Otherwise false. - * - * @param {object} state - Global state tree - * @return {boolean} Whether the account protection settings are being requested - */ -export function isFetchingAccountProtectionSettings( state ) { - return !! state.jetpack.accountProtection.requests.isFetchingAccountProtectionSettings; -} - -/** - * Returns true if currently updating the account protection settings. Otherwise false. - * - * @param {object} state - Global state tree - * @return {boolean} Whether the account protection settings are being requested - */ -export function isUpdatingAccountProtectionSettings( state ) { - return !! state.jetpack.accountProtection.requests.isUpdatingAccountProtectionSettings; -} - -/** - * Returns the account protection's settings. - * - * @param {object} state - Global state tree - * @return {string} File path to bootstrap.php - */ -export function getAccountProtectionSettings( state ) { - return get( state.jetpack.accountProtection, [ 'data' ], {} ); -} diff --git a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php index 4b931dadb330f..9852478a7c53d 100644 --- a/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php +++ b/projects/plugins/jetpack/json-endpoints/class.wpcom-json-api-site-settings-endpoint.php @@ -127,7 +127,6 @@ 'jetpack_subscriptions_login_navigation_enabled' => '(bool) Whether the Subscriber Login block navigation placement is enabled', 'jetpack_subscriptions_subscribe_navigation_enabled' => '(Bool) Whether the Subscribe block navigation placement is enabled', 'wpcom_ai_site_prompt' => '(string) User input in the AI site prompt', - 'jetpack_account_protection_strict_mode' => '(bool) Whether to enforce strict password requirements', 'jetpack_waf_automatic_rules' => '(bool) Whether the WAF should enforce automatic firewall rules', 'jetpack_waf_ip_allow_list' => '(string) List of IP addresses to always allow', 'jetpack_waf_ip_allow_list_enabled' => '(bool) Whether the IP allow list is enabled', @@ -491,7 +490,6 @@ function ( $newsletter_category ) { 'jetpack_comment_form_color_scheme' => (string) get_option( 'jetpack_comment_form_color_scheme' ), 'in_site_migration_flow' => (string) get_option( 'in_site_migration_flow', '' ), 'migration_source_site_domain' => (string) get_option( 'migration_source_site_domain' ), - 'jetpack_account_protection_strict_mode' => (bool) get_option( 'jetpack_account_protection_strict_mode' ), 'jetpack_waf_automatic_rules' => (bool) get_option( 'jetpack_waf_automatic_rules' ), 'jetpack_waf_ip_allow_list' => (string) get_option( 'jetpack_waf_ip_allow_list' ), 'jetpack_waf_ip_allow_list_enabled' => (bool) get_option( 'jetpack_waf_ip_allow_list_enabled' ), diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts deleted file mode 100644 index 592c5b983c37a..0000000000000 --- a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-mutation.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; -import { __ } from '@wordpress/i18n'; -import camelize from 'camelize'; -import API from '../../api'; -import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; -import useNotices from '../../hooks/use-notices'; -import { AccountProtectionStatus } from '../../types/account-protection'; - -/** - * Account Protection Mutatation Hook - * - * @return {UseMutationResult} useMutation result. - */ -export default function useAccountProtectionMutation(): UseMutationResult< - unknown, - { [ key: string ]: unknown }, - unknown, - { initialValue: AccountProtectionStatus } -> { - const queryClient = useQueryClient(); - const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); - - return useMutation( { - mutationFn: API.updateAccountProtection, - onMutate: settings => { - showSavingNotice(); - - // Get the current Account Protection settings. - const initialValue = queryClient.getQueryData( [ - QUERY_ACCOUNT_PROTECTION_KEY, - ] ) as AccountProtectionStatus; - - // Optimistically update the Account Protection settings. - queryClient.setQueryData( - [ QUERY_ACCOUNT_PROTECTION_KEY ], - ( accountProtectionStatus: AccountProtectionStatus ) => ( { - ...accountProtectionStatus, - settings: { - ...accountProtectionStatus.settings, - ...camelize( settings ), - }, - } ) - ); - - return { initialValue }; - }, - onSuccess: () => { - showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); - }, - onError: ( error, variables, context ) => { - // Reset the account protection config to its previous state. - queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], context.initialValue ); - - showErrorNotice( __( 'Error saving changes.', 'jetpack-protect' ) ); - }, - } ); -} diff --git a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts index 4032d0e6a42e6..68c5b53ee0d40 100644 --- a/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts +++ b/projects/plugins/protect/src/js/data/account-protection/use-toggle-account-protection-module-mutation.ts @@ -18,13 +18,9 @@ export default function useToggleAccountProtectionMutation(): UseMutationResult onMutate: () => { showSavingNotice(); - // Get the current Account Protection settings. const initialValue = queryClient.getQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ] ); - console.log( initialValue ); // TODO: Test the toggleAccountProtection mutation... - - // Optimistically update the Account Protection settings. - // queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], () => ! initialValue ); + queryClient.setQueryData( [ QUERY_ACCOUNT_PROTECTION_KEY ], ! initialValue ); return { initialValue }; }, @@ -32,7 +28,10 @@ export default function useToggleAccountProtectionMutation(): UseMutationResult showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); }, onError: () => { - showErrorNotice( __( 'Error savings changes.', 'jetpack-protect' ) ); + showErrorNotice( __( 'An error occurred.', 'jetpack-protect' ) ); + }, + onSettled: () => { + queryClient.invalidateQueries( { queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ] } ); }, } ); } diff --git a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx b/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx deleted file mode 100644 index 90e473c270bc6..0000000000000 --- a/projects/plugins/protect/src/js/hooks/use-account-protection-data/index.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback } from 'react'; -import useAccountProtectionMutation from '../../data/account-protection/use-account-protection-mutation'; -import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; -import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-module-mutation'; -import useAnalyticsTracks from '../use-analytics-tracks'; - -/** - * Use Account Protection Data Hook - * - * @return {object} Account Protection data and methods for interacting with it. - */ -const useAccountProtectionData = () => { - const { recordEvent } = useAnalyticsTracks(); - const { data: accountProtection } = useAccountProtectionQuery(); - const accountProtectionMutation = useAccountProtectionMutation(); - const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); - - /** - * Toggle Account Protection Module - * - * Flips the switch on the Account Protection module, and then refreshes the data. - */ - const toggleAccountProtection = useCallback( async () => { - toggleAccountProtectionMutation.mutate(); - }, [ toggleAccountProtectionMutation ] ); - - /** - * Toggle Strict Mode - * - * Flips the switch on the strict mode option, and then refreshes the data. - */ - const toggleStrictMode = useCallback( async () => { - const value = ! accountProtection.settings.jetpackAccountProtectionStrictMode; - const mutationObj = { jetpack_account_protection_strict_mode: value }; - if ( ! value ) { - mutationObj.jetpack_account_protection_strict_mode = false; - } - await accountProtectionMutation.mutateAsync( mutationObj ); - recordEvent( - mutationObj - ? 'jetpack_account_protection_strict_mode_enabled' - : 'jetpack_account_protection_strict_mode_disabled' - ); - }, [ - recordEvent, - accountProtection.settings.jetpackAccountProtectionStrictMode, - accountProtectionMutation, - ] ); - - return { - ...accountProtection, - isUpdating: accountProtectionMutation.isPending, - isToggling: toggleAccountProtectionMutation.isPending, - toggleAccountProtection, - toggleStrictMode, - }; -}; - -export default useAccountProtectionData; diff --git a/projects/plugins/protect/src/js/routes/settings/index.jsx b/projects/plugins/protect/src/js/routes/settings/index.jsx index bec154de509fc..8373d94a5f7f2 100644 --- a/projects/plugins/protect/src/js/routes/settings/index.jsx +++ b/projects/plugins/protect/src/js/routes/settings/index.jsx @@ -8,9 +8,10 @@ import { import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon, warning } from '@wordpress/icons'; +import React, { useCallback } from 'react'; import AdminPage from '../../components/admin-page'; import useAccountProtectionQuery from '../../data/account-protection/use-account-protection-query'; -import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-mutation'; +import useToggleAccountProtectionMutation from '../../data/account-protection/use-toggle-account-protection-module-mutation'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import usePlan from '../../hooks/use-plan'; import styles from './styles.module.scss'; @@ -18,7 +19,16 @@ import styles from './styles.module.scss'; const SettingsPage = () => { const { hasPlan } = usePlan(); const { data: accountProtectionIsEnabled } = useAccountProtectionQuery(); - const toggleAccountProtection = useToggleAccountProtectionMutation(); + const toggleAccountProtectionMutation = useToggleAccountProtectionMutation(); + + /** + * Toggle Account Protect Module + * + * Flips the switch on the Account Protection module, and then refreshes the data. + */ + const toggleAccountProtection = useCallback( async () => { + toggleAccountProtectionMutation.mutate(); + }, [ toggleAccountProtectionMutation ] ); // Track view for Protect Account Protection page. useAnalyticsTracks( { @@ -34,7 +44,7 @@ const SettingsPage = () => {
@@ -52,7 +62,7 @@ const SettingsPage = () => { } ) } - { accountProtectionIsEnabled && ( + { ! accountProtectionIsEnabled && ( { createInterpolateElement( From 36f0945fde600194917aa8093dd41b4ecb6d3a4c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 13:27:01 -0800 Subject: [PATCH 062/120] Updates/fixes --- .../jetpack/_inc/client/state/action-types.js | 9 --------- .../plugins/jetpack/_inc/client/state/reducer.js | 2 -- projects/plugins/protect/src/js/api.ts | 3 +-- .../use-account-protection-query.ts | 3 +-- .../protect/src/js/types/account-protection.ts | 12 ------------ 5 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 projects/plugins/protect/src/js/types/account-protection.ts diff --git a/projects/plugins/jetpack/_inc/client/state/action-types.js b/projects/plugins/jetpack/_inc/client/state/action-types.js index 7da1fbb07cf1d..c4785d4a2ced5 100644 --- a/projects/plugins/jetpack/_inc/client/state/action-types.js +++ b/projects/plugins/jetpack/_inc/client/state/action-types.js @@ -245,15 +245,6 @@ export const JETPACK_LICENSING_GET_USER_LICENSES_FAILURE = export const JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL = 'JETPACK_CONNECTION_HAS_SEEN_WC_CONNECTION_MODAL'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH = 'ACCOUNT_PROTECTION_SETTINGS_FETCH'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE = - 'ACCOUNT_PROTECTION_SETTINGS_FETCH_RECEIVE'; -export const ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_FETCH_FAIL'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS = - 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_SUCCESS'; -export const ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL = 'ACCOUNT_PROTECTION_SETTINGS_UPDATE_FAIL'; - export const WAF_SETTINGS_FETCH = 'WAF_SETTINGS_FETCH'; export const WAF_SETTINGS_FETCH_RECEIVE = 'WAF_SETTINGS_FETCH_RECEIVE'; export const WAF_SETTINGS_FETCH_FAIL = 'WAF_SETTINGS_FETCH_FAIL'; diff --git a/projects/plugins/jetpack/_inc/client/state/reducer.js b/projects/plugins/jetpack/_inc/client/state/reducer.js index 14e85f0fb5289..5ff156b807a49 100644 --- a/projects/plugins/jetpack/_inc/client/state/reducer.js +++ b/projects/plugins/jetpack/_inc/client/state/reducer.js @@ -1,6 +1,5 @@ import { combineReducers } from 'redux'; import { globalNotices } from 'components/global-notices/state/notices/reducer'; -import { reducer as accountProtection } from 'state/account-protection/reducer'; import { dashboard } from 'state/at-a-glance/reducer'; import { reducer as connection } from 'state/connection/reducer'; import { reducer as devCard } from 'state/dev-version/reducer'; @@ -47,7 +46,6 @@ const jetpackReducer = combineReducers( { disconnectSurvey, trackingSettings, licensing, - accountProtection, waf, introOffers, } ); diff --git a/projects/plugins/protect/src/js/api.ts b/projects/plugins/protect/src/js/api.ts index 98b543160b475..b87875b8b188a 100644 --- a/projects/plugins/protect/src/js/api.ts +++ b/projects/plugins/protect/src/js/api.ts @@ -1,11 +1,10 @@ import { type FixersStatus, type ScanStatus } from '@automattic/jetpack-scan'; import apiFetch from '@wordpress/api-fetch'; import camelize from 'camelize'; -import { AccountProtectionStatus } from './types/account-protection'; import { WafStatus } from './types/waf'; const API = { - getAccountProtection: (): Promise< AccountProtectionStatus > => + getAccountProtection: () => apiFetch( { path: 'jetpack-protect/v1/account-protection', method: 'GET', diff --git a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts index 01dd3354432a9..8bd32f8ddf951 100644 --- a/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts +++ b/projects/plugins/protect/src/js/data/account-protection/use-account-protection-query.ts @@ -2,14 +2,13 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import camelize from 'camelize'; import API from '../../api'; import { QUERY_ACCOUNT_PROTECTION_KEY } from '../../constants'; -import { AccountProtectionStatus } from '../../types/account-protection'; /** * Account Protection Query Hook * * @return {UseQueryResult} useQuery result. */ -export default function useAccountProtectionQuery(): UseQueryResult< AccountProtectionStatus > { +export default function useAccountProtectionQuery(): UseQueryResult { return useQuery( { queryKey: [ QUERY_ACCOUNT_PROTECTION_KEY ], queryFn: API.getAccountProtection, diff --git a/projects/plugins/protect/src/js/types/account-protection.ts b/projects/plugins/protect/src/js/types/account-protection.ts deleted file mode 100644 index 37d557638982b..0000000000000 --- a/projects/plugins/protect/src/js/types/account-protection.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type AccountProtectionStatus = { - /** Whether the "account-protection" module is enabled. */ - isEnabled: boolean; - - /** The current Account Protetion settings. */ - settings: AccountProtectionSettings; -}; - -export type AccountProtectionSettings = { - /** Whether the user has enabled strict mode. */ - jetpackAccountProtectionStrictMode: boolean; -}; From 37e0aa2574a3b3aa8488f7219b1ac42e70d79f9d Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 13:30:35 -0800 Subject: [PATCH 063/120] Fix import --- projects/plugins/jetpack/_inc/client/security/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/_inc/client/security/index.jsx b/projects/plugins/jetpack/_inc/client/security/index.jsx index d4677461de9ea..ff1ec0efad4f2 100644 --- a/projects/plugins/jetpack/_inc/client/security/index.jsx +++ b/projects/plugins/jetpack/_inc/client/security/index.jsx @@ -12,7 +12,7 @@ import { isModuleFound } from 'state/search'; import { getSettings } from 'state/settings'; import { siteHasFeature } from 'state/site'; import { isPluginActive, isPluginInstalled } from 'state/site/plugins'; -import AccountProtection from './account-protection'; +import { AccountProtection } from './account-protection'; import AllowList from './allowList'; import Antispam from './antispam'; import BackupsScan from './backups-scan'; From d51016d0c9236a0d1ea090884b29654ba6a5d940 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 13:36:11 -0800 Subject: [PATCH 064/120] Update placeholder content --- .../ai-assistant-plugin/components/breve/features/events.ts | 4 ++-- projects/plugins/protect/src/js/routes/settings/index.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts index 58075d8857569..eb5eb0442d2e4 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts @@ -97,7 +97,7 @@ async function handleMouseEnter( e: MouseEvent ) { target: el, virtual: virtual, } as Anchor ); - }, 500 ); + }, 500 ) as unknown as number; } function handleMouseLeave() { @@ -106,7 +106,7 @@ function handleMouseLeave() { highlightTimeout = setTimeout( () => { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ); + }, 100 ) as unknown as number; } export default function registerEvents( clientId: string ) { diff --git a/projects/plugins/protect/src/js/routes/settings/index.jsx b/projects/plugins/protect/src/js/routes/settings/index.jsx index 8373d94a5f7f2..459bc542586b1 100644 --- a/projects/plugins/protect/src/js/routes/settings/index.jsx +++ b/projects/plugins/protect/src/js/routes/settings/index.jsx @@ -54,7 +54,7 @@ const SettingsPage = () => { { createInterpolateElement( __( - 'When enabled, users can only set passwords that meet strong security standards, helping protect their accounts and your site.', + 'Protect your site with enhanced password detection and profile management security.', 'jetpack-protect' ), { From 30b86d5bd70033930e8a24150f5dfb22d29af729 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 13:37:50 -0800 Subject: [PATCH 065/120] Revert unrelated changes --- .../ai-assistant-plugin/components/breve/features/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts index eb5eb0442d2e4..58075d8857569 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/events.ts @@ -97,7 +97,7 @@ async function handleMouseEnter( e: MouseEvent ) { target: el, virtual: virtual, } as Anchor ); - }, 500 ) as unknown as number; + }, 500 ); } function handleMouseLeave() { @@ -106,7 +106,7 @@ function handleMouseLeave() { highlightTimeout = setTimeout( () => { ( dispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setHighlightHover( false ); - }, 100 ) as unknown as number; + }, 100 ); } export default function registerEvents( clientId: string ) { From 6b34d2594d18794c62413b487ac6329a8764c943 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 24 Jan 2025 14:23:00 -0800 Subject: [PATCH 066/120] Remove missed code --- projects/js-packages/api/index.jsx | 10 -- .../src/class-rest-controller.php | 104 ------------------ 2 files changed, 114 deletions(-) delete mode 100644 projects/packages/account-protection/src/class-rest-controller.php diff --git a/projects/js-packages/api/index.jsx b/projects/js-packages/api/index.jsx index 6f6cdffe0b325..8233d0ba8a616 100644 --- a/projects/js-packages/api/index.jsx +++ b/projects/js-packages/api/index.jsx @@ -510,16 +510,6 @@ function JetpackRestApiClient( root, nonce ) { getRequest( `${ wpcomOriginApiUrl }jetpack/v4/search/stats`, getParams ) .then( checkStatus ) .then( parseJsonResponse ), - fetchAccountProtectionSettings: () => - getRequest( `${ apiRoot }jetpack/v4/account-protection`, getParams ) - .then( checkStatus ) - .then( parseJsonResponse ), - updateAccountProtectionSettings: newSettings => - postRequest( `${ apiRoot }jetpack/v4/account-protection`, postParams, { - body: JSON.stringify( newSettings ), - } ) - .then( checkStatus ) - .then( parseJsonResponse ), fetchWafSettings: () => getRequest( `${ apiRoot }jetpack/v4/waf`, getParams ) .then( checkStatus ) diff --git a/projects/packages/account-protection/src/class-rest-controller.php b/projects/packages/account-protection/src/class-rest-controller.php deleted file mode 100644 index a49c1b1fe7c65..0000000000000 --- a/projects/packages/account-protection/src/class-rest-controller.php +++ /dev/null @@ -1,104 +0,0 @@ -routes_registered ) { - return; - } - - register_rest_route( - 'jetpack/v4', - '/account-protection', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_settings' ), - 'permission_callback' => array( $this, 'permissions_callback' ), - ) - ); - - register_rest_route( - 'jetpack/v4', - '/account-protection', - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_settings' ), - 'permission_callback' => array( $this, 'permissions_callback' ), - ) - ); - - $this->routes_registered = true; - } - - /** - * Account Protection Settings Endpoint - * - * @return WP_REST_Response - */ - public function get_settings() { - $settings = ( new Account_Protection() )->get_settings(); - - return rest_ensure_response( $settings ); - } - - /** - * Update Account Protection Settings Endpoint - * - * @param WP_REST_Request $request The API request. - * - * @return WP_REST_Response|WP_Error - */ - public function update_settings( $request ) { - // Strict Mode - if ( isset( $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ) ) { - update_option( Account_Protection::STRICT_MODE_OPTION_NAME, $request[ Account_Protection::STRICT_MODE_OPTION_NAME ] ? '1' : '' ); - } - - return $this->get_settings(); - } - - /** - * Account Protection Endpoint Permissions Callback - * - * @return bool|WP_Error True if user can view the Jetpack admin page. - */ - public function permissions_callback() { - if ( current_user_can( 'manage_options' ) ) { - return true; - } - - return new WP_Error( - 'invalid_user_permission_manage_options', - REST_Connector::get_user_permissions_error_msg(), - array( 'status' => rest_authorization_required_code() ) - ); - } -} From 40a6edf626782379ac9c7d32418d01776fc4dbf8 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 27 Jan 2025 10:30:49 -0800 Subject: [PATCH 067/120] Update reset email to two factor auth email --- .../src/class-account-protection.php | 4 +- .../src/class-password-detection.php | 124 +++++++----------- ...il.php => class-two-factor-auth-email.php} | 7 +- .../src/css/password-detection.css | 33 +++-- ...ord-reset.js => resend-two-factor-auth.js} | 6 +- 5 files changed, 82 insertions(+), 92 deletions(-) rename projects/packages/account-protection/src/{class-password-reset-email.php => class-two-factor-auth-email.php} (87%) rename projects/packages/account-protection/src/js/{resend-password-reset.js => resend-two-factor-auth.js} (91%) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 2db8aaf73582d..065a0a3775047 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -79,8 +79,8 @@ private function register_runtime_hooks(): void { add_action( 'after_password_reset', array( $this->password_detection, 'delete_usermeta_after_password_reset' ), 10, 2 ); add_action( 'profile_update', array( $this->password_detection, 'delete_usermeta_on_profile_update' ), 10, 2 ); - // Register AJAX resend password reset email action - add_action( 'wp_ajax_resend_password_reset', array( $this->password_detection, 'ajax_resend_password_reset_email' ) ); + // Register AJAX resend two factor auth email action + add_action( 'wp_ajax_resend_two_factor_auth', array( $this->password_detection, 'ajax_resend_two_factor_auth_email' ) ); } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index ca195ed1207e6..39cb7c4680bdc 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -17,19 +17,19 @@ class Password_Detection { const PASSWORD_DETECTION_USER_META_KEY = 'jetpack_account_protection_password_status'; /** - * Password reset email dependency. + * Two factor auth email dependency. * - * @var Password_Reset_Email + * @var Two_Factor_Auth_Email */ - private $password_reset_email; + private $two_factor_auth_email; /** * Password_Detection constructor. * - * @param ?Password_Reset_Email $password_reset_email Password reset email instance. + * @param ?Two_Factor_Auth_Email $two_factor_auth_email Two factor auth email instance. */ - public function __construct( ?Password_Reset_Email $password_reset_email = null ) { - $this->password_reset_email = $password_reset_email ?? new Password_Reset_Email(); + public function __construct( ?Two_Factor_Auth_Email $two_factor_auth_email = null ) { + $this->two_factor_auth_email = $two_factor_auth_email ?? new Two_Factor_Auth_Email(); } /** @@ -93,70 +93,59 @@ public function render_page() { } // Use a transient to track email sent status - $transient_key = 'password_reset_email_sent_' . $current_user->ID; + $transient_key = 'two_factor_auth_email_sent_' . $current_user->ID; $email_sent_flag = get_transient( $transient_key ); // Initialize template variables - $reset = false; - $context = 'Your current password was found in a public leak, which means your account might be at risk.'; + $context = "We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security"; $error = ''; add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); - // Handle reset_password_action form submission - if ( isset( $_POST['reset-password'] ) ) { - $reset = true; - - // Verify nonce - if ( isset( $_POST['_wpnonce_reset_password'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_reset_password'] ) ), 'reset_password_action' ) ) { - // Send password reset email - if ( ! $email_sent_flag ) { - $email_sent = $this->password_reset_email->send(); - if ( $email_sent ) { - // Set transient to mark the email as sent - set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); - } else { - $error = 'email_send_error'; - } - } - - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resend_password_reset_scripts' ) ); + // Send verification code email + if ( ! $email_sent_flag ) { + $email_sent = $this->two_factor_auth_email->send(); + if ( $email_sent ) { + // Set transient to mark the email as sent + set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); } else { - $error = 'reset_passowrd_nonce_verification_error'; + $error = 'email_send_error'; } + } - // Handle proceed_action form submission - } elseif ( isset( $_POST['proceed'] ) ) { - $reset = true; + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resend_two_factor_auth_scripts' ) ); + + // Handle verify form submission + if ( isset( $_POST['verify'] ) ) { // Verify nonce - if ( isset( $_POST['_wpnonce_proceed'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_proceed'] ) ), 'proceed_action' ) ) { + if ( isset( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { wp_safe_redirect( admin_url() ); exit; } else { - $error = 'proceed_nonce_verification_error'; + $error = 'verify_nonce_verification_error'; } } - $this->render_content( $reset, $context, $error, $this->password_reset_email->mask_email_address( $current_user->user_email ) ); + $this->render_content( $context, $error, $this->two_factor_auth_email->mask_email_address( $current_user->user_email ) ); exit; } /** - * Enqueue the resend password reset email scripts. + * Enqueue the resend two factor auth email scripts. * * @return void */ - public function enqueue_resend_password_reset_scripts(): void { - wp_enqueue_script( 'resend-password-reset', plugin_dir_url( __FILE__ ) . 'js/resend-password-reset.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); + public function enqueue_resend_two_factor_auth_scripts(): void { + wp_enqueue_script( 'resend-two-factor-auth', plugin_dir_url( __FILE__ ) . 'js/resend-two-factor-auth.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); // Pass AJAX URL and nonce to the script wp_localize_script( - 'resend-password-reset', + 'resend-two-factor-auth', 'ajaxObject', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'resend_password_reset_nonce' ), + 'nonce' => wp_create_nonce( 'resend_two_factor_auth_nonce' ), ) ); } @@ -176,11 +165,11 @@ public function enqueue_styles(): void { } /** - * Run AJAX request to resend password reset email. + * Run AJAX request to resend two factor auth email. */ - public function ajax_resend_password_reset_email() { + public function ajax_resend_two_factor_auth_email() { // Verify the nonce for security - check_ajax_referer( 'resend_password_reset_nonce', 'security' ); + check_ajax_referer( 'resend_two_factor_auth_nonce', 'security' ); // Check if the user is logged in if ( ! is_user_logged_in() ) { @@ -188,7 +177,7 @@ public function ajax_resend_password_reset_email() { } // Resend the email - $email_sent = $this->password_reset_email->send(); + $email_sent = $this->two_factor_auth_email->send(); if ( $email_sent ) { wp_send_json_success( array( 'message' => 'Resend successful.' ) ); } else { @@ -320,13 +309,12 @@ public function delete_usermeta_on_profile_update( int $user_id ) { /** * Render content for password detection page. * - * @param bool $reset Whether the user is resetting their password. * @param string $context The context for the password detection page. * @param string $error The error message to display. * @param string $masked_email The masked email address. * @return void */ - public function render_content( bool $reset, string $context, string $error, string $masked_email ): void { + public function render_content( string $context, string $error, string $masked_email ): void { defined( 'ABSPATH' ) || exit; ?> @@ -334,51 +322,41 @@ public function render_content( bool $reset, string $context, string $error, str - <?php echo esc_html( $reset ? 'Jetpack - Stay Secure' : 'Jetpack - Secure Your Account' ); ?> + <?php echo esc_html( 'Jetpack - Secure Your Account' ); ?> diff --git a/projects/packages/account-protection/src/class-password-reset-email.php b/projects/packages/account-protection/src/class-two-factor-auth-email.php similarity index 87% rename from projects/packages/account-protection/src/class-password-reset-email.php rename to projects/packages/account-protection/src/class-two-factor-auth-email.php index 7afde4aab79ae..ac674c5e902e7 100644 --- a/projects/packages/account-protection/src/class-password-reset-email.php +++ b/projects/packages/account-protection/src/class-two-factor-auth-email.php @@ -1,6 +1,6 @@ Date: Mon, 27 Jan 2025 16:36:34 -0800 Subject: [PATCH 068/120] Updates and improvements --- .../src/class-account-protection.php | 14 +- .../src/class-password-detection.php | 260 ++++++++---------- .../src/class-two-factor-auth-email.php | 25 +- .../src/js/resend-two-factor-auth.js | 71 ----- 4 files changed, 139 insertions(+), 231 deletions(-) delete mode 100644 projects/packages/account-protection/src/js/resend-two-factor-auth.js diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 065a0a3775047..56271fa3d7491 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -72,15 +72,11 @@ private function register_runtime_hooks(): void { // Validate password after successful login add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); + // Handle password detection login failure + add_action( 'wp_login_failed', array( $this->password_detection, 'handle_password_detection_login_failure' ), 10, 2 ); + // Add password detection flow add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 ); - - // Remove password detection usermeta after password reset and on profile password update - add_action( 'after_password_reset', array( $this->password_detection, 'delete_usermeta_after_password_reset' ), 10, 2 ); - add_action( 'profile_update', array( $this->password_detection, 'delete_usermeta_on_profile_update' ), 10, 2 ); - - // Register AJAX resend two factor auth email action - add_action( 'wp_ajax_resend_two_factor_auth', array( $this->password_detection, 'ajax_resend_two_factor_auth_email' ) ); } /** @@ -94,9 +90,7 @@ public function on_account_protection_activation(): void { * Deactivate the account protection on module deactivation. */ public function on_account_protection_deactivation(): void { - // Remove password detection user meta on deactivation - // TODO: Run on Jetpack and Protect deactivation - $this->password_detection->delete_all_usermeta(); + // Deactivation logic can be added here } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 39cb7c4680bdc..5789b9c980265 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -9,13 +9,12 @@ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager as Connection_Manager; +use WP_Error; /** * Class Password_Detection */ class Password_Detection { - const PASSWORD_DETECTION_USER_META_KEY = 'jetpack_account_protection_password_status'; - /** * Two factor auth email dependency. * @@ -33,12 +32,21 @@ public function __construct( ?Two_Factor_Auth_Email $two_factor_auth_email = nul } /** - * Redirect to the password detection page. + * Handle password detection login failure. * - * @return string The URL to redirect to. + * @param string $username The username. + * @param WP_Error $error The error object. + * @return never */ - public function password_detection_redirect(): string { - return home_url( '/wp-login.php?action=password-detection' ); + public function handle_password_detection_login_failure( $username, $error ) { + // Check for the validation error + if ( isset( $error->errors['password_detection_validation_error'] ) ) { + // Get the token from the error data + $token = $error->get_error_data()['token']; + // Redirect the user back to the login page with a custom error message + wp_safe_redirect( home_url( '/wp-login.php?action=password-detection&token=' . $token ) ); + exit; + } } /** @@ -54,19 +62,36 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - // Ensure the password is correct for this user - if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) { + // Check if the user has the required role or capabilities, Author role or higher + if ( ! user_can( $user, 'publish_posts' ) && ! user_can( $user, 'edit_published_posts' ) ) { return $user; } - if ( ! $this->validate_password( $password ) ) { - // TODO: Ensure usermeta is always up to date - $this->update_usermeta( $user->ID, 'unsafe' ); - - // Redirect to the password detection page - add_filter( 'login_redirect', array( $this, 'password_detection_redirect' ), 10, 3 ); - } else { - $this->update_usermeta( $user->ID, 'safe' ); + // Ensure the password is correct for this user + if ( wp_check_password( $password, $user->user_pass, $user->ID ) ) { + // TODO: Only run validation only if we haven't already checked? + if ( ! $this->validate_password( $password ) ) { + // Use a transient to track email sent status + $sent_transient_key = 'two_factor_auth_email_sent_' . $user->ID; + $email_sent_flag = get_transient( $sent_transient_key ); + + // Send verification code email + if ( ! $email_sent_flag ) { + $email_sent = $this->two_factor_auth_email->send( $user->ID, $user->user_email ); + if ( $email_sent ) { + // Set transient to mark the email as sent + set_transient( $sent_transient_key, true, 10 * MINUTE_IN_SECONDS ); + } + } + + // Generate a unique token and store user details in a transient + // TODO: Ensure we are clearing all transients after use + $token = wp_generate_password( 32, false, false ); + set_transient( "password_detection_$token", $user->ID, 10 * MINUTE_IN_SECONDS ); + + // Return error to redirect to the password detection page + return new \WP_Error( 'password_detection_validation_error', 'Password detection validation error', array( 'token' => $token ) ); + } } return $user; @@ -78,78 +103,104 @@ public function login_form_password_detection( $user, string $password ) { * @return never */ public function render_page() { - // Restrict direct access to logged in users - $current_user = wp_get_current_user(); - if ( 0 === $current_user->ID ) { + // Ensure the user is logged out + if ( is_user_logged_in() ) { + wp_safe_redirect( admin_url() ); + exit; + } + + // Get the token from the query string + $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : null; + + // Get the user ID from the transient + $user_id = $token ? get_transient( "password_detection_$token" ) : null; + if ( ! $user_id ) { wp_safe_redirect( wp_login_url() ); exit; } - // Restrict direct access to users with unsafe passwords - $user_password_status = $this->get_usermeta( $current_user->ID ); - if ( ! $user_password_status || 'safe' === $user_password_status ) { - wp_safe_redirect( admin_url() ); + // Get the user details + $current_user = get_user_by( 'ID', $user_id ); + if ( ! $current_user ) { + wp_safe_redirect( wp_login_url() ); exit; } - // Use a transient to track email sent status - $transient_key = 'two_factor_auth_email_sent_' . $current_user->ID; - $email_sent_flag = get_transient( $transient_key ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); // Initialize template variables - $context = "We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security"; - $error = ''; + $context = "We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security"; + $error = ''; + $redirect_url = '/wp-login.php?action=password-detection&token=' . $token; - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); + // Use a transient to track email resent status + $resent_transient_key = 'two_factor_auth_email_resent_' . $current_user->ID; + $email_resent_attempts = get_transient( $resent_transient_key ); - // Send verification code email - if ( ! $email_sent_flag ) { - $email_sent = $this->two_factor_auth_email->send(); - if ( $email_sent ) { - // Set transient to mark the email as sent - set_transient( $transient_key, true, 15 * MINUTE_IN_SECONDS ); + if ( false === $email_resent_attempts ) { + $email_resent_attempts = 0; + } + + // Handle resend email request + if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { + if ( $email_resent_attempts >= 3 ) { + // User has exceeded the maximum resend attempts + $error = 'maximum_resend_attempts_exceeded'; } else { - $error = 'email_send_error'; + // Resend email + $email_resent = $this->two_factor_auth_email->send( $current_user->ID, $current_user->user_email ); + if ( $email_resent ) { + // Increment the resend attempts and set the transient + ++$email_resent_attempts; + set_transient( $resent_transient_key, $email_resent_attempts, 10 * MINUTE_IN_SECONDS ); + } else { + $error = 'email_resend_error'; + } } + + // Redirect to the password detection page + wp_safe_redirect( home_url( $redirect_url ) ); } - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_resend_two_factor_auth_scripts' ) ); + // TODO: Separate out large blocks of code into functions + // TODO: Update resend message when attempts met or error // Handle verify form submission if ( isset( $_POST['verify'] ) ) { // Verify nonce if ( isset( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { - wp_safe_redirect( admin_url() ); - exit; + // If the auth code is correct, log the user in and clear transient + $auth_code = get_transient( "password_detection_auth_code_$user_id" ); + $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; + + if ( $auth_code && $user_input && $auth_code === $user_input ) { + // Clear the transients + delete_transient( "password_detection_$token" ); + delete_transient( "password_detection_auth_code_$user_id" ); + delete_transient( $resent_transient_key ); + delete_transient( "two_factor_auth_email_sent_$user_id" ); + + // Log the user in + wp_set_auth_cookie( $user_id, true ); + + // Redirect to the admin dashboard + wp_safe_redirect( admin_url() ); + + // TODO: How can we notify the user to update their password? + exit; + } else { + $error = 'invalid_auth_code'; + } } else { $error = 'verify_nonce_verification_error'; } } - $this->render_content( $context, $error, $this->two_factor_auth_email->mask_email_address( $current_user->user_email ) ); + $this->render_content( $context, $error, $redirect_url, $this->two_factor_auth_email->mask_email_address( $current_user->user_email ) ); exit; } - /** - * Enqueue the resend two factor auth email scripts. - * - * @return void - */ - public function enqueue_resend_two_factor_auth_scripts(): void { - wp_enqueue_script( 'resend-two-factor-auth', plugin_dir_url( __FILE__ ) . 'js/resend-two-factor-auth.js', array( 'jquery' ), Account_Protection::PACKAGE_VERSION, true ); - - // Pass AJAX URL and nonce to the script - wp_localize_script( - 'resend-two-factor-auth', - 'ajaxObject', - array( - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'resend_two_factor_auth_nonce' ), - ) - ); - } - /** * Enqueue the password detection page styles. * @@ -164,27 +215,6 @@ public function enqueue_styles(): void { ); } - /** - * Run AJAX request to resend two factor auth email. - */ - public function ajax_resend_two_factor_auth_email() { - // Verify the nonce for security - check_ajax_referer( 'resend_two_factor_auth_nonce', 'security' ); - - // Check if the user is logged in - if ( ! is_user_logged_in() ) { - wp_send_json_error( array( 'message' => 'User not authenticated' ) ); - } - - // Resend the email - $email_sent = $this->two_factor_auth_email->send(); - if ( $email_sent ) { - wp_send_json_success( array( 'message' => 'Resend successful.' ) ); - } else { - wp_send_json_error( array( 'message' => 'Resend failed. ' ) ); - } - } - /** * Password validation. * @@ -243,78 +273,16 @@ public function check_weak_passwords( string $password ) { return false; } - /** - * Get the password detection usermeta. - * - * @param int $user_id The user ID. - */ - public function get_usermeta( int $user_id ) { - return get_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, true ); - } - - /** - * Update the password detection usermeta. - * - * @param int $user_id The user ID. - * @param string $setting The password detection setting. - */ - public function update_usermeta( int $user_id, string $setting ) { - update_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY, $setting ); - } - - /** - * Delete password detection usermeta for all users. - */ - public function delete_all_usermeta() { - $users = get_users(); - foreach ( $users as $user ) { - $this->delete_usermeta( $user->ID ); - } - } - - /** - * Delete the password detection usermeta. - * - * @param int $user_id The user ID. - */ - public function delete_usermeta( int $user_id ) { - delete_user_meta( $user_id, self::PASSWORD_DETECTION_USER_META_KEY ); - } - - /** - * Delete the password detection usermeta after password reset. - * - * @param \WP_User $user The user object. - */ - public function delete_usermeta_after_password_reset( \WP_User $user ) { - $this->delete_usermeta( $user->ID ); - } - - /** - * Delete the password detection usermeta on profile password update. - * - * @param int $user_id The user ID. - */ - public function delete_usermeta_on_profile_update( int $user_id ) { - if ( - ! empty( $_POST['_wpnonce'] ) && - wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) - ) { - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $this->delete_usermeta( $user_id ); - } - } - } - /** * Render content for password detection page. * * @param string $context The context for the password detection page. * @param string $error The error message to display. + * @param string $redirect_url The redirect URL. * @param string $masked_email The masked email address. * @return void */ - public function render_content( string $context, string $error, string $masked_email ): void { + public function render_content( string $context, string $error, string $redirect_url, string $masked_email ): void { defined( 'ABSPATH' ) || exit; ?> @@ -354,8 +322,8 @@ class="action-input"
diff --git a/projects/packages/account-protection/src/class-two-factor-auth-email.php b/projects/packages/account-protection/src/class-two-factor-auth-email.php index ac674c5e902e7..c63a1fe76e62e 100644 --- a/projects/packages/account-protection/src/class-two-factor-auth-email.php +++ b/projects/packages/account-protection/src/class-two-factor-auth-email.php @@ -36,11 +36,28 @@ public function mask_email_address( string $email ): string { /** * Send two factor auth email. * + * @param int $user_id The user ID to send the email to. + * @param string $email The email address to send the email to. * @return bool True if the email was sent successfully, false otherwise. */ - public function send(): bool { - // TODO: Attempt to send via wp_mail() first - // TODO: Update to use custom email method when available - return true; + public function send( $user_id, $email ): bool { + // Generate an auth code and store in a transient + // TODO: Ensure we are clearing all transients after use + $auth_code = wp_rand( 100000, 999999 ); + set_transient( "password_detection_auth_code_$user_id", $auth_code, 10 * MINUTE_IN_SECONDS ); + + // Attempt to send auth code via wp_mail() + $subject = 'Your Authentication Code'; + $message = "Hello,\n\nWe detected a password issue with your account. To proceed, please use the following authentication code:\n\nAuth Code: $auth_code\n\nThis code is valid for 10 minutes.\n\nThank you,\nThe Team"; + + // Email headers + $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); + + // Send the email using wp_mail() + $wp_mail_sent = wp_mail( $email, $subject, $message, $headers ); + + // TODO: If ! $wp_mail_sent attempt to use custom method to send the email + + return $wp_mail_sent; } } diff --git a/projects/packages/account-protection/src/js/resend-two-factor-auth.js b/projects/packages/account-protection/src/js/resend-two-factor-auth.js deleted file mode 100644 index 5c000de610669..0000000000000 --- a/projects/packages/account-protection/src/js/resend-two-factor-auth.js +++ /dev/null @@ -1,71 +0,0 @@ -/* global jQuery, ajaxObject */ -( function ( $ ) { - $( document ).ready( function () { - const attemptLimit = 3; - let attempts = 0; - - $( '#resend-code' ).on( 'click', function ( e ) { - e.preventDefault(); // Prevent the default action - - const message = $( '#resend-code-message' ); - const button = $( this ); - - // Store the original text of the message - const originalMessageText = message.text(); - - // Update message and hide button while resending - message.text( 'Resending email...' ); - button.hide(); - - attempts++; - - // Perform the AJAX request - $.ajax( { - url: ajaxObject.ajax_url, - type: 'POST', - data: { - action: 'resend_two_factor_auth', - security: ajaxObject.nonce, - }, - success: function ( response ) { - if ( response.success ) { - // Show success message - message.text( response.data.message ).show(); - - // Hide the status message and show the button after 5 seconds - setTimeout( function () { - let messageText = originalMessageText; - if ( attempts < attemptLimit ) { - button.show(); - } else { - messageText += 'Please try again later.'; - } - message.text( messageText ).show(); - }, 5000 ); - } else { - // Show error message - let messageText = 'An error occurred. '; - if ( attempts < attemptLimit ) { - button.text( 'Please try again' ).show(); - } else { - messageText += 'Please contact support.'; // TODO: Add support redirect - } - - message.text( messageText ).show(); - } - }, - error: function () { - // Show error message - let messageText = 'An error occurred. '; - if ( attempts < attemptLimit ) { - button.text( 'Please try again' ).show(); - } else { - messageText += 'Please contact support.'; // TODO: Add support redirect - } - - message.text( messageText ).show(); - }, - } ); - } ); - } ); -} )( jQuery ); From 612f6551ffe3087d95dd198a91bf40aa5fac3ec6 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 27 Jan 2025 19:33:32 -0800 Subject: [PATCH 069/120] Reorg --- .../src/class-password-detection.php | 136 +++++++++--------- .../src/class-password-validation.php | 27 ++++ .../src/class-two-factor-auth-email.php | 24 +--- 3 files changed, 100 insertions(+), 87 deletions(-) create mode 100644 projects/packages/account-protection/src/class-password-validation.php diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 5789b9c980265..dc08566555a38 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -31,24 +31,6 @@ public function __construct( ?Two_Factor_Auth_Email $two_factor_auth_email = nul $this->two_factor_auth_email = $two_factor_auth_email ?? new Two_Factor_Auth_Email(); } - /** - * Handle password detection login failure. - * - * @param string $username The username. - * @param WP_Error $error The error object. - * @return never - */ - public function handle_password_detection_login_failure( $username, $error ) { - // Check for the validation error - if ( isset( $error->errors['password_detection_validation_error'] ) ) { - // Get the token from the error data - $token = $error->get_error_data()['token']; - // Redirect the user back to the login page with a custom error message - wp_safe_redirect( home_url( '/wp-login.php?action=password-detection&token=' . $token ) ); - exit; - } - } - /** * Check if the password is safe after login. * @@ -57,46 +39,54 @@ public function handle_password_detection_login_failure( $username, $error ) { * @return \WP_User|\WP_Error The user object. */ public function login_form_password_detection( $user, string $password ) { - // Check if the user is already a WP_Error object - if ( is_wp_error( $user ) ) { + if ( is_wp_error( $user ) || ! $this->user_requires_protection( $user, $password ) ) { return $user; } - // Check if the user has the required role or capabilities, Author role or higher - if ( ! user_can( $user, 'publish_posts' ) && ! user_can( $user, 'edit_published_posts' ) ) { - return $user; - } + if ( ! ( new Password_Validation() )->validate_password( $password ) ) { + $sent_transient_key = 'two_factor_auth_email_sent_' . $user->ID; + $email_sent_flag = get_transient( $sent_transient_key ); - // Ensure the password is correct for this user - if ( wp_check_password( $password, $user->user_pass, $user->ID ) ) { - // TODO: Only run validation only if we haven't already checked? - if ( ! $this->validate_password( $password ) ) { - // Use a transient to track email sent status - $sent_transient_key = 'two_factor_auth_email_sent_' . $user->ID; - $email_sent_flag = get_transient( $sent_transient_key ); - - // Send verification code email - if ( ! $email_sent_flag ) { - $email_sent = $this->two_factor_auth_email->send( $user->ID, $user->user_email ); - if ( $email_sent ) { - // Set transient to mark the email as sent - set_transient( $sent_transient_key, true, 10 * MINUTE_IN_SECONDS ); - } + if ( ! $email_sent_flag ) { + $email_sent = $this->two_factor_auth_email->send( $user->ID, $user->user_email ); + if ( $email_sent ) { + // Set transient to mark the email as sent + set_transient( $sent_transient_key, true, 10 * MINUTE_IN_SECONDS ); } + } - // Generate a unique token and store user details in a transient - // TODO: Ensure we are clearing all transients after use - $token = wp_generate_password( 32, false, false ); - set_transient( "password_detection_$token", $user->ID, 10 * MINUTE_IN_SECONDS ); + // TODO: Ensure we are clearing all transients after use, consider various cases + $token = wp_generate_password( 32, false, false ); + set_transient( "password_detection_$token", $user->ID, 10 * MINUTE_IN_SECONDS ); - // Return error to redirect to the password detection page - return new \WP_Error( 'password_detection_validation_error', 'Password detection validation error', array( 'token' => $token ) ); - } + return new WP_Error( + 'password_detection_validation_error', + 'Password detection validation error', + array( 'token' => $token ) + ); } return $user; } + /** + * Handle password detection login failure. + * + * @param string $username The username. + * @param WP_Error $error The error object. + * @return never + */ + public function handle_password_detection_login_failure( $username, $error ) { + // Check for the validation error + if ( isset( $error->errors['password_detection_validation_error'] ) ) { + // Get the token from the error data + $token = $error->get_error_data()['token']; + // Redirect the user back to the login page with a custom error message + wp_safe_redirect( home_url( '/wp-login.php?action=password-detection&token=' . $token ) ); + exit; + } + } + /** * Render password detection page. * @@ -168,18 +158,17 @@ public function render_page() { // Handle verify form submission if ( isset( $_POST['verify'] ) ) { - // Verify nonce if ( isset( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { - // If the auth code is correct, log the user in and clear transient $auth_code = get_transient( "password_detection_auth_code_$user_id" ); $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; + // If the auth code is correct if ( $auth_code && $user_input && $auth_code === $user_input ) { // Clear the transients delete_transient( "password_detection_$token" ); delete_transient( "password_detection_auth_code_$user_id" ); - delete_transient( $resent_transient_key ); delete_transient( "two_factor_auth_email_sent_$user_id" ); + delete_transient( $resent_transient_key ); // Log the user in wp_set_auth_cookie( $user_id, true ); @@ -197,10 +186,22 @@ public function render_page() { } } - $this->render_content( $context, $error, $redirect_url, $this->two_factor_auth_email->mask_email_address( $current_user->user_email ) ); + $this->render_content( $context, $error, $redirect_url, $this->mask_email_address( $current_user->user_email ) ); exit; } + /** + * Check if the user requires password protection. + * + * @param \WP_User $user The user object. + * @param string $password The password. + * @return bool + */ + private function user_requires_protection( $user, $password ) { + // TODO: Only run validation if we haven't already checked? + return ( user_can( $user, 'publish_posts' ) || user_can( $user, 'edit_published_posts' ) ) && wp_check_password( $password, $user->user_pass, $user->ID ); + } + /** * Enqueue the password detection page styles. * @@ -215,20 +216,6 @@ public function enqueue_styles(): void { ); } - /** - * Password validation. - * - * @param string $password The password to validate. - * @return bool True if the password is valid, false otherwise. - */ - public function validate_password( string $password ): bool { - // TODO: Uncomment out once endpoint is live - // Check compromised and common passwords - // $weak_password = self::check_weak_passwords( $password ); - - return $password ? false : true; - } - /** * Check if the password is in the list of common/compromised passwords. * @@ -273,6 +260,27 @@ public function check_weak_passwords( string $password ) { return false; } + /** + * Mask an email address like d*****@g*****.com. + * + * @param string $email The email address to mask. + * @return string The masked email address. + */ + public function mask_email_address( string $email ): string { + $parts = explode( '@', $email ); + $name = $parts[0]; + $domain = $parts[1]; + + // Mask the name part (first letter + asterisks) + $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); + + // Mask the domain part (first letter + asterisks + domain extension) + $domain_parts = explode( '.', $domain ); + $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; + + return $masked_name . '@' . $masked_domain; + } + /** * Render content for password detection page. * diff --git a/projects/packages/account-protection/src/class-password-validation.php b/projects/packages/account-protection/src/class-password-validation.php new file mode 100644 index 0000000000000..f1ed1bc5a4947 --- /dev/null +++ b/projects/packages/account-protection/src/class-password-validation.php @@ -0,0 +1,27 @@ + Date: Tue, 28 Jan 2025 11:16:36 -0800 Subject: [PATCH 070/120] Optimizations and reorganizations --- .../src/class-account-protection.php | 19 +- .../account-protection/src/class-config.php | 19 + .../src/class-email-service.php | 111 ++++++ .../src/class-password-detection.php | 372 ++++++++---------- .../src/class-password-validation.php | 27 -- .../src/class-two-factor-auth-email.php | 41 -- .../src/class-validation-service.php | 72 ++++ 7 files changed, 372 insertions(+), 289 deletions(-) create mode 100644 projects/packages/account-protection/src/class-config.php create mode 100644 projects/packages/account-protection/src/class-email-service.php delete mode 100644 projects/packages/account-protection/src/class-password-validation.php delete mode 100644 projects/packages/account-protection/src/class-two-factor-auth-email.php create mode 100644 projects/packages/account-protection/src/class-validation-service.php diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 56271fa3d7491..6d0452cce3e74 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -13,9 +13,6 @@ * Class Account_Protection */ class Account_Protection { - const PACKAGE_VERSION = '0.1.0-alpha'; - const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; - /** * Modules instance. * @@ -57,8 +54,8 @@ public function init(): void { */ private function register_hooks(): void { // Account protection activation/deactivation hooks - add_action( 'jetpack_activate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); - add_action( 'jetpack_deactivate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); + add_action( 'jetpack_activate_module_' . Config::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); + add_action( 'jetpack_deactivate_module_' . Config::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); // Do not run in unsupported environments add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) ); @@ -73,7 +70,7 @@ private function register_runtime_hooks(): void { add_action( 'wp_authenticate_user', array( $this->password_detection, 'login_form_password_detection' ), 10, 2 ); // Handle password detection login failure - add_action( 'wp_login_failed', array( $this->password_detection, 'handle_password_detection_login_failure' ), 10, 2 ); + add_action( 'wp_login_failed', array( $this->password_detection, 'handle_password_detection_validation_error' ), 10, 2 ); // Add password detection flow add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 ); @@ -99,7 +96,7 @@ public function on_account_protection_deactivation(): void { * @return bool */ public function is_enabled() { - return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->is_active( Config::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -112,7 +109,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); + return $this->modules->activate( Config::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -125,7 +122,7 @@ public function disable(): bool { if ( ! $this->is_enabled() ) { return true; } - return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->deactivate( Config::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -152,7 +149,7 @@ public function is_supported_environment(): bool { public function remove_module_on_unsupported_environments( array $modules ): array { if ( ! $this->is_supported_environment() ) { // Account protection should never be available on unsupported platforms. - unset( $modules[ self::ACCOUNT_PROTECTION_MODULE_NAME ] ); + unset( $modules[ Config::ACCOUNT_PROTECTION_MODULE_NAME ] ); } return $modules; @@ -171,7 +168,7 @@ public function remove_standalone_module_on_unsupported_environments( array $mod $modules = array_filter( $modules, function ( $module ) { - return $module !== self::ACCOUNT_PROTECTION_MODULE_NAME; + return $module !== Config::ACCOUNT_PROTECTION_MODULE_NAME; } ); diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php new file mode 100644 index 0000000000000..359e6c7648491 --- /dev/null +++ b/projects/packages/account-protection/src/class-config.php @@ -0,0 +1,19 @@ +wp_send_auth_email( $email, $auth_code ); + + // if ( ! $wp_send ) { + // $api_send = $this->api_send_auth_email( $email, $auth_code ); + // return $api_send; + // } + + return false; + } + + /** + * Send the email using wp_mail(). + * + * @param string $email The email address to send the email to. + * @param string $auth_code The authentication code. + * @return bool True if the email was sent successfully, false otherwise. + */ + private function wp_send_auth_email( $email, $auth_code ) { + $subject = 'Your Authentication Code'; + $message = "Hello,\n\nWe detected a password issue with your account. To proceed, please use the following authentication code:\n\nAuth Code: $auth_code\n\nThis code is valid for 10 minutes.\n\nThank you,\nThe Team"; + $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); + + return wp_mail( $email, $subject, $message, $headers ); + } + + /** + * Send the email using the API. + * + * @param string $email The email address to send the email to. + * @param string $auth_code The authentication code. + * @return bool True if the email was sent successfully, false otherwise. + */ + private function api_send_auth_email( $email, $auth_code ) { + // TODO: Hook up to API to send email + return true; + } + + /** + * Resend email attempts. + * + * @param string $email The email address to send the email to. + * @param array $transient_data The transient data. + * @param string $token The token. + * @return bool True if the email was resent successfully, false otherwise. + */ + public function resend_auth_email( $email, $transient_data, $token ): bool { + ++$transient_data['resend_attempts']; + + if ( $transient_data['resend_attempts'] > Config::MAX_RESEND_ATTEMPTS ) { + return false; + } + + $auth_code = $this->generate_auth_code(); + $transient_data['auth_code'] = $auth_code; + + if ( ! set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::EMAIL_SENT_EXPIRATION ) ) { + return false; + } + + if ( ! $this->send_auth_email( $email, $auth_code ) ) { + return false; + } + + return true; + } + + /** + * Generate an auth code. + * + * @return int The generated auth code. + */ + public function generate_auth_code(): int { + return wp_rand( 100000, 999999 ); + } + + /** + * Mask an email address like d*****@g*****.com. + * + * @param string $email The email address to mask. + * @return string The masked email address. + */ + public function mask_email_address( string $email ): string { + $parts = explode( '@', $email ); + $name = substr( $parts[0], 0, 1 ) . str_repeat( '*', strlen( $parts[0] ) - 1 ); + $domain_parts = explode( '.', $parts[1] ); + $domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ); + + return "{$name}@{$domain}.{$domain_parts[1]}"; + } +} diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index dc08566555a38..630254148c13e 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -7,28 +7,33 @@ namespace Automattic\Jetpack\Account_Protection; -use Automattic\Jetpack\Connection\Client; -use Automattic\Jetpack\Connection\Manager as Connection_Manager; -use WP_Error; - /** * Class Password_Detection */ class Password_Detection { /** - * Two factor auth email dependency. + * Email service dependency. + * + * @var Email_Service + */ + private $email_service; + + /** + * Validation service dependency. * - * @var Two_Factor_Auth_Email + * @var Validation_Service */ - private $two_factor_auth_email; + private $validation_service; /** * Password_Detection constructor. * - * @param ?Two_Factor_Auth_Email $two_factor_auth_email Two factor auth email instance. + * @param ?Email_Service $email_service Email service instance. + * @param ?Validation_Service $validation_service Validation service instance. */ - public function __construct( ?Two_Factor_Auth_Email $two_factor_auth_email = null ) { - $this->two_factor_auth_email = $two_factor_auth_email ?? new Two_Factor_Auth_Email(); + public function __construct( ?Email_Service $email_service = null, ?Validation_Service $validation_service = null ) { + $this->email_service = $email_service ?? new Email_Service(); + $this->validation_service = $validation_service ?? new Validation_Service(); } /** @@ -43,26 +48,19 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - if ( ! ( new Password_Validation() )->validate_password( $password ) ) { - $sent_transient_key = 'two_factor_auth_email_sent_' . $user->ID; - $email_sent_flag = get_transient( $sent_transient_key ); + if ( ! $this->validation_service->validate_password( $password ) ) { + // TODO: Every time the user logs in we generate a new token based transient. This is not ideal. + $transient = $this->generate_and_store_transient_data( $user->ID ); - if ( ! $email_sent_flag ) { - $email_sent = $this->two_factor_auth_email->send( $user->ID, $user->user_email ); - if ( $email_sent ) { - // Set transient to mark the email as sent - set_transient( $sent_transient_key, true, 10 * MINUTE_IN_SECONDS ); - } + $email_sent = $this->email_service->send_auth_email( $user->user_email, $transient['auth_code'] ); + if ( ! $email_sent ) { + // $this->add_error( 'email_send_error', 'Failed to send the authentication email.' ); } - // TODO: Ensure we are clearing all transients after use, consider various cases - $token = wp_generate_password( 32, false, false ); - set_transient( "password_detection_$token", $user->ID, 10 * MINUTE_IN_SECONDS ); - - return new WP_Error( + return new \WP_Error( 'password_detection_validation_error', 'Password detection validation error', - array( 'token' => $token ) + array( 'token' => $transient['token'] ) ); } @@ -70,19 +68,16 @@ public function login_form_password_detection( $user, string $password ) { } /** - * Handle password detection login failure. + * Handle password detection validation error. * - * @param string $username The username. - * @param WP_Error $error The error object. + * @param string $username The username. + * @param \WP_Error $error The error object. * @return never */ - public function handle_password_detection_login_failure( $username, $error ) { - // Check for the validation error + public function handle_password_detection_validation_error( $username, $error ) { if ( isset( $error->errors['password_detection_validation_error'] ) ) { - // Get the token from the error data $token = $error->get_error_data()['token']; - // Redirect the user back to the login page with a custom error message - wp_safe_redirect( home_url( '/wp-login.php?action=password-detection&token=' . $token ) ); + wp_safe_redirect( $this->get_redirect_url( $token ) ); exit; } } @@ -93,204 +88,55 @@ public function handle_password_detection_login_failure( $username, $error ) { * @return never */ public function render_page() { - // Ensure the user is logged out if ( is_user_logged_in() ) { wp_safe_redirect( admin_url() ); exit; } - // Get the token from the query string - $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : null; - - // Get the user ID from the transient - $user_id = $token ? get_transient( "password_detection_$token" ) : null; - if ( ! $user_id ) { - wp_safe_redirect( wp_login_url() ); - exit; + $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : null; + $transient_data = get_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); + if ( ! $transient_data ) { + $this->redirect_to_login(); } - // Get the user details - $current_user = get_user_by( 'ID', $user_id ); + $user_id = $transient_data['user_id'] ?? null; + $current_user = $user_id ? get_user_by( 'ID', $user_id ) : null; + if ( ! $current_user ) { - wp_safe_redirect( wp_login_url() ); - exit; + $this->redirect_to_login(); } add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); - // Initialize template variables - $context = "We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security"; - $error = ''; - $redirect_url = '/wp-login.php?action=password-detection&token=' . $token; - - // Use a transient to track email resent status - $resent_transient_key = 'two_factor_auth_email_resent_' . $current_user->ID; - $email_resent_attempts = get_transient( $resent_transient_key ); - - if ( false === $email_resent_attempts ) { - $email_resent_attempts = 0; - } - // Handle resend email request if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { - if ( $email_resent_attempts >= 3 ) { - // User has exceeded the maximum resend attempts - $error = 'maximum_resend_attempts_exceeded'; - } else { - // Resend email - $email_resent = $this->two_factor_auth_email->send( $current_user->ID, $current_user->user_email ); - if ( $email_resent ) { - // Increment the resend attempts and set the transient - ++$email_resent_attempts; - set_transient( $resent_transient_key, $email_resent_attempts, 10 * MINUTE_IN_SECONDS ); - } else { - $error = 'email_resend_error'; - } + $email_resent = $this->email_service->resend_auth_email( $current_user->user_email, $transient_data, $token ); + + if ( ! $email_resent ) { + // $this->add_error( 'email_resend_error', 'Failed to resend the authentication email.' ); } - // Redirect to the password detection page - wp_safe_redirect( home_url( $redirect_url ) ); + wp_safe_redirect( $this->get_redirect_url( $token ) ); + exit; } - // TODO: Separate out large blocks of code into functions - // TODO: Update resend message when attempts met or error - // Handle verify form submission if ( isset( $_POST['verify'] ) ) { - - if ( isset( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { - $auth_code = get_transient( "password_detection_auth_code_$user_id" ); - $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; - - // If the auth code is correct - if ( $auth_code && $user_input && $auth_code === $user_input ) { - // Clear the transients - delete_transient( "password_detection_$token" ); - delete_transient( "password_detection_auth_code_$user_id" ); - delete_transient( "two_factor_auth_email_sent_$user_id" ); - delete_transient( $resent_transient_key ); - - // Log the user in - wp_set_auth_cookie( $user_id, true ); - - // Redirect to the admin dashboard - wp_safe_redirect( admin_url() ); - - // TODO: How can we notify the user to update their password? - exit; - } else { - $error = 'invalid_auth_code'; - } - } else { - $error = 'verify_nonce_verification_error'; - } + $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null ); } - $this->render_content( $context, $error, $redirect_url, $this->mask_email_address( $current_user->user_email ) ); + $this->render_content( $this->get_redirect_url( $token ), $this->email_service->mask_email_address( $current_user->user_email ) ); exit; } - /** - * Check if the user requires password protection. - * - * @param \WP_User $user The user object. - * @param string $password The password. - * @return bool - */ - private function user_requires_protection( $user, $password ) { - // TODO: Only run validation if we haven't already checked? - return ( user_can( $user, 'publish_posts' ) || user_can( $user, 'edit_published_posts' ) ) && wp_check_password( $password, $user->user_pass, $user->ID ); - } - - /** - * Enqueue the password detection page styles. - * - * @return void - */ - public function enqueue_styles(): void { - wp_enqueue_style( - 'password-detection-styles', - plugin_dir_url( __FILE__ ) . 'css/password-detection.css', - array(), - Account_Protection::PACKAGE_VERSION - ); - } - - /** - * Check if the password is in the list of common/compromised passwords. - * - * @param string $password The password to check. - * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise. - */ - public function check_weak_passwords( string $password ) { - $api_url = '/jetpack-protect-weak-password'; - - $is_connected = ( new Connection_Manager() )->is_connected(); - - if ( ! $is_connected ) { - return new \WP_Error( 'site_not_connected' ); - } - - // Hash pass with sha1, and pass first 5 characters to the API - $hashed_password = sha1( $password ); - $password_prefix = substr( $hashed_password, 0, 5 ); - - $response = Client::wpcom_json_api_request_as_blog( - $api_url . '/' . $password_prefix, - '2', - array( 'method' => 'GET' ), - null, - 'wpcom' - ); - - $response_code = wp_remote_retrieve_response_code( $response ); - - if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { - return new \WP_Error( 'failed_fetching_weak_passwords', 'Failed to fetch weak passwords from the server', array( 'status' => $response_code ) ); - } - - $body = json_decode( wp_remote_retrieve_body( $response ), true ); - - // Check if the password is in the list of common/compromised passwords - $password_suffix = substr( $hashed_password, 5 ); - if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { - return true; - } - - return false; - } - - /** - * Mask an email address like d*****@g*****.com. - * - * @param string $email The email address to mask. - * @return string The masked email address. - */ - public function mask_email_address( string $email ): string { - $parts = explode( '@', $email ); - $name = $parts[0]; - $domain = $parts[1]; - - // Mask the name part (first letter + asterisks) - $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); - - // Mask the domain part (first letter + asterisks + domain extension) - $domain_parts = explode( '.', $domain ); - $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; - - return $masked_name . '@' . $masked_domain; - } - /** * Render content for password detection page. * - * @param string $context The context for the password detection page. - * @param string $error The error message to display. * @param string $redirect_url The redirect URL. * @param string $masked_email The masked email address. * @return void */ - public function render_content( string $context, string $error, string $redirect_url, string $masked_email ): void { + public function render_content( string $redirect_url, string $masked_email ): void { defined( 'ABSPATH' ) || exit; ?> @@ -305,17 +151,8 @@ public function render_content( string $context, string $error, string $redirect

-

- - -

We've encountered an issue verifying your request to proceed without updating your password.

- - While attempting to send a code to , an error occurred. -

- - -

We've sent a code to . Please check your inbox and enter the code below to verify it's really you.

- +

We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security.

+

We've sent a code to . Please check your inbox and enter the code below to verify it's really you.

@@ -339,4 +176,119 @@ class="action-input" user_pass, $user->ID ); + } + + /** + * Generate and store a consolidated transient for the user. + * + * @param int $user_id The user ID. + * @return string The generated token. + */ + private function generate_and_store_transient_data( $user_id ) { + $token = wp_generate_password( 32, false, false ); + $auth_code = $this->email_service->generate_auth_code(); + + $data = array( + 'user_id' => $user_id, + 'auth_code' => $auth_code, + 'resend_attempts' => 0, + ); + + $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); + + if ( ! $transient_set ) { + // $this->add_error( 'transient_set_error', 'Failed to set transient data.' ); + } + + return array( + 'token' => $token, + 'auth_code' => $auth_code, + ); + } + + /** + * Redirect to the login page. + */ + private function redirect_to_login() { + wp_safe_redirect( wp_login_url() ); + exit; + } + + /** + * Get redirect URL. + * + * @param string $token The token. + * @return string The redirect URL. + */ + private function get_redirect_url( $token ) { + return home_url( '/wp-login.php?action=password-detection&token=' . $token ); + } + + /** + * Handle auth form submission. + * + * @param \WP_User $current_user The current user. + * @param string $token The token. + * @param string $auth_code The expected auth code. + */ + private function handle_auth_form_submission( $current_user, $token, $auth_code ) { + if ( ! isset( $_POST['_wpnonce_verify'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { + // $this->add_error( 'nonce_verification_error', 'Nonce verification failed.' ); + } + + $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; + + if ( $auth_code && $auth_code === $user_input ) { + delete_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); + // TODO: Ensure all transient are removed on module and/or plugin deactivation + wp_set_auth_cookie( $current_user->ID, true ); + wp_safe_redirect( admin_url() ); + // TODO: Notify user to update their password/redirect to password update page + exit; + } else { + // $this->add_error( 'auth_code_verification_error', 'Authentication code verification failed.' ); + } + } + + /** + * Mask an email address like d*****@g*****.com. + * + * @param string $email The email address to mask. + * @return string The masked email address. + */ + public function mask_email_address( string $email ): string { + $parts = explode( '@', $email ); + $name = $parts[0]; + $domain = $parts[1]; + $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); + $domain_parts = explode( '.', $domain ); + $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; + + return $masked_name . '@' . $masked_domain; + } + + /** + * Enqueue the password detection page styles. + * + * @return void + */ + public function enqueue_styles(): void { + wp_enqueue_style( + 'password-detection-styles', + plugin_dir_url( __FILE__ ) . 'css/password-detection.css', + array(), + Config::PACKAGE_VERSION + ); + } } diff --git a/projects/packages/account-protection/src/class-password-validation.php b/projects/packages/account-protection/src/class-password-validation.php deleted file mode 100644 index f1ed1bc5a4947..0000000000000 --- a/projects/packages/account-protection/src/class-password-validation.php +++ /dev/null @@ -1,27 +0,0 @@ -is_connected(); + if ( ! $is_connected ) { + return new \WP_Error( 'site_not_connected' ); + } + + $hashed_password = sha1( $password ); + $password_prefix = substr( $hashed_password, 0, 5 ); + + $response = Client::wpcom_json_api_request_as_blog( + $api_url . '/' . $password_prefix, + '2', + array( 'method' => 'GET' ), + null, + 'wpcom' + ); + + $response_code = wp_remote_retrieve_response_code( $response ); + + if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { + return new \WP_Error( 'failed_fetching_weak_passwords', 'Failed to fetch weak passwords from the server', array( 'status' => $response_code ) ); + } + + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + + // TODO: Check if the password is in the list of common/compromised passwords + $password_suffix = substr( $hashed_password, 5 ); + if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { + return true; + } + + return false; + } +} From 664558bb0cfb6527e3d0b922c89a0cbaf0ebf210 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 15:08:33 -0800 Subject: [PATCH 071/120] Hook up email service --- .../src/class-email-service.php | 117 +++++++++++++----- .../src/class-password-detection.php | 6 +- .../src/class-validation-service.php | 25 ++-- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index 3c8cc415b2432..977c792ac1872 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -7,6 +7,10 @@ namespace Automattic\Jetpack\Account_Protection; +use Automattic\Jetpack\Connection\Client; +use Automattic\Jetpack\Connection\Manager as Connection_Manager; +use Jetpack_Options; + /** * Class Email_Service */ @@ -14,71 +18,118 @@ class Email_Service { /** * Send auth email. * - * @param string $email The email address to send the email to. - * @param string $auth_code The authentication code. + * @param WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ - public function send_auth_email( $email, $auth_code ): bool { - // $wp_send = $this->wp_send_auth_email( $email, $auth_code ); + public function send_auth_email( $user, $auth_code ): bool { + $wp_send = $this->wp_send_auth_email( $user, $auth_code ); + + if ( ! $wp_send ) { + $api_send = $this->api_send_auth_email( $user, $auth_code ); - // if ( ! $wp_send ) { - // $api_send = $this->api_send_auth_email( $email, $auth_code ); - // return $api_send; - // } + return $api_send; + } - return false; + return true; } /** * Send the email using wp_mail(). * - * @param string $email The email address to send the email to. - * @param string $auth_code The authentication code. + * @param WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ - private function wp_send_auth_email( $email, $auth_code ) { - $subject = 'Your Authentication Code'; - $message = "Hello,\n\nWe detected a password issue with your account. To proceed, please use the following authentication code:\n\nAuth Code: $auth_code\n\nThis code is valid for 10 minutes.\n\nThank you,\nThe Team"; - $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); - - return wp_mail( $email, $subject, $message, $headers ); + private function wp_send_auth_email( $user, $auth_code ) { + $blog_name = esc_html( get_bloginfo( 'name' ) ); + $user_url = ! empty( $user->user_url ) ? esc_url( $user->user_url ) : esc_url( home_url() ); + + $subject = 'Verify your identity at Jetpack'; + $message = sprintf( + ' +

Hi %s,

+

Your current password for %s was found in a public leak, which means your account might be at risk.

+

To help protect your account, please enter this code at the login prompt:

+

%s

+

If you didn\'t just log into %s, please do so now and change your password.

+

Stay secure,
Jetpack

+ ', + esc_html( $user->user_login ), + $user_url, + $blog_name, + esc_html( $auth_code ), + $user_url, + $blog_name + ); + + $headers = array( 'Content-Type: text/html; charset=UTF-8' ); + + return wp_mail( $user->user_email, $subject, $message, $headers ); } /** * Send the email using the API. * - * @param string $email The email address to send the email to. - * @param string $auth_code The authentication code. + * @param WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ - private function api_send_auth_email( $email, $auth_code ) { - // TODO: Hook up to API to send email - return true; + private function api_send_auth_email( $user, $auth_code ) { + $blog_id = Jetpack_Options::get_option( 'id' ); + $is_connected = ( new Connection_Manager() )->is_connected(); + + if ( ! $blog_id || ! $is_connected ) { + return false; + } + + $body = array( + 'user_login' => $user->user_login, + 'user_email' => $user->user_email, + 'code' => $auth_code, + ); + + $response = Client::wpcom_json_api_request_as_blog( + sprintf( '/sites/%d/jetpack-protect-send-verification-code', $blog_id ), + '2', + array( + 'method' => 'POST', + ), + $body, + 'wpcom' + ); + + $response_code = wp_remote_retrieve_response_code( $response ); + if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { + return false; + } + + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + return $body['success'] ?? false; } /** * Resend email attempts. * - * @param string $email The email address to send the email to. - * @param array $transient_data The transient data. - * @param string $token The token. + * @param WP_User $user The user. + * @param array $transient_data The transient data. + * @param string $token The token. * @return bool True if the email was resent successfully, false otherwise. */ - public function resend_auth_email( $email, $transient_data, $token ): bool { - ++$transient_data['resend_attempts']; - - if ( $transient_data['resend_attempts'] > Config::MAX_RESEND_ATTEMPTS ) { + public function resend_auth_email( $user, $transient_data, $token ): bool { + if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { return false; } $auth_code = $this->generate_auth_code(); $transient_data['auth_code'] = $auth_code; + ++$transient_data['resend_attempts']; if ( ! set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::EMAIL_SENT_EXPIRATION ) ) { return false; } - if ( ! $this->send_auth_email( $email, $auth_code ) ) { + if ( ! $this->send_auth_email( $user, $auth_code ) ) { return false; } @@ -88,10 +139,10 @@ public function resend_auth_email( $email, $transient_data, $token ): bool { /** * Generate an auth code. * - * @return int The generated auth code. + * @return string The generated auth code. */ - public function generate_auth_code(): int { - return wp_rand( 100000, 999999 ); + public function generate_auth_code(): string { + return (string) wp_rand( 100000, 999999 ); } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 630254148c13e..5ce1cfd52a612 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -48,11 +48,11 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - if ( ! $this->validation_service->validate_password( $password ) ) { + if ( ! $this->validation_service->check_weak_passwords( $password ) ) { // TODO: Every time the user logs in we generate a new token based transient. This is not ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); - $email_sent = $this->email_service->send_auth_email( $user->user_email, $transient['auth_code'] ); + $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); if ( ! $email_sent ) { // $this->add_error( 'email_send_error', 'Failed to send the authentication email.' ); } @@ -110,7 +110,7 @@ public function render_page() { // Handle resend email request if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { - $email_resent = $this->email_service->resend_auth_email( $current_user->user_email, $transient_data, $token ); + $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { // $this->add_error( 'email_resend_error', 'Failed to resend the authentication email.' ); diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 3a0edcff8ae38..297e2b757fefc 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -15,25 +15,11 @@ */ class Validation_Service { /** - * Password validation. + * Check if the password is in the list of common/compromised passwords. * - * @param string $password The password to validate. - * @return bool True if the password is valid, false otherwise. + * @param string $password The password to check. + * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise. */ - public function validate_password( string $password ): bool { - // TODO: Uncomment out once endpoint is live - // Check compromised and common passwords - // $weak_password = self::check_weak_passwords( $password ); - - return $password ? false : true; - } - - /** - * Check if the password is in the list of common/compromised passwords. - * - * @param string $password The password to check. - * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise. - */ public function check_weak_passwords( string $password ) { $api_url = '/jetpack-protect-weak-password'; @@ -61,12 +47,15 @@ public function check_weak_passwords( string $password ) { $body = json_decode( wp_remote_retrieve_body( $response ), true ); - // TODO: Check if the password is in the list of common/compromised passwords $password_suffix = substr( $hashed_password, 5 ); if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { return true; } + if ( in_array( $password_suffix, $body['common'] ?? array(), true ) ) { + return true; + } + return false; } } From 915504d8fc064a5eb8d2733aed2e81817444d345 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 16:00:07 -0800 Subject: [PATCH 072/120] Update error handling todos, fix weak password check --- .../src/class-password-detection.php | 12 ++++++------ .../src/class-validation-service.php | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 5ce1cfd52a612..61e2ceb48fbc3 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -48,13 +48,13 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - if ( ! $this->validation_service->check_weak_passwords( $password ) ) { + if ( $this->validation_service->check_weak_passwords( $password ) ) { // TODO: Every time the user logs in we generate a new token based transient. This is not ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); if ( ! $email_sent ) { - // $this->add_error( 'email_send_error', 'Failed to send the authentication email.' ); + // TODO: Add error handling -> 'email_send_error', 'Failed to send the authentication email.'; } return new \WP_Error( @@ -113,7 +113,7 @@ public function render_page() { $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { - // $this->add_error( 'email_resend_error', 'Failed to resend the authentication email.' ); + // TODO: Add error handling -> 'email_resend_error', 'Failed to resend the authentication email or reached the maximum number of attempts.' } wp_safe_redirect( $this->get_redirect_url( $token ) ); @@ -208,7 +208,7 @@ private function generate_and_store_transient_data( $user_id ) { $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); if ( ! $transient_set ) { - // $this->add_error( 'transient_set_error', 'Failed to set transient data.' ); + // TODO: Add error handling -> 'transient_set_error', 'Failed to set transient data.' } return array( @@ -244,7 +244,7 @@ private function get_redirect_url( $token ) { */ private function handle_auth_form_submission( $current_user, $token, $auth_code ) { if ( ! isset( $_POST['_wpnonce_verify'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { - // $this->add_error( 'nonce_verification_error', 'Nonce verification failed.' ); + // TODO: Add error handling -> 'nonce_verification_error', 'Nonce verification failed.' } $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; @@ -257,7 +257,7 @@ private function handle_auth_form_submission( $current_user, $token, $auth_code // TODO: Notify user to update their password/redirect to password update page exit; } else { - // $this->add_error( 'auth_code_verification_error', 'Authentication code verification failed.' ); + // TODO: Add error handling -> 'auth_code_verification_error', 'Authentication code verification failed.' } } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 297e2b757fefc..8fa0a6373186f 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -42,7 +42,8 @@ public function check_weak_passwords( string $password ) { $response_code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { - return new \WP_Error( 'failed_fetching_weak_passwords', 'Failed to fetch weak passwords from the server', array( 'status' => $response_code ) ); + return false; + // TODO: Return or log error? } $body = json_decode( wp_remote_retrieve_body( $response ), true ); From 87445c276fec1d1eb5782aaf081234e46b93acca Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 16:05:52 -0800 Subject: [PATCH 073/120] Test From 9ef7e9df144d9b156c6e9b893926b8565664e0ff Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 18:50:30 -0800 Subject: [PATCH 074/120] Localize text content --- .../src/class-email-service.php | 20 +++++++------- .../src/class-password-detection.php | 26 ++++++++++++------- .../src/class-validation-service.php | 4 +-- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index 977c792ac1872..c4058afb7afa1 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -45,16 +45,18 @@ private function wp_send_auth_email( $user, $auth_code ) { $blog_name = esc_html( get_bloginfo( 'name' ) ); $user_url = ! empty( $user->user_url ) ? esc_url( $user->user_url ) : esc_url( home_url() ); - $subject = 'Verify your identity at Jetpack'; + $subject = esc_html__( 'Verify your identity at Jetpack', 'jetpack-account-protection' ); $message = sprintf( - ' -

Hi %s,

-

Your current password for %s was found in a public leak, which means your account might be at risk.

-

To help protect your account, please enter this code at the login prompt:

-

%s

-

If you didn\'t just log into %s, please do so now and change your password.

-

Stay secure,
Jetpack

- ', + /* translators: 1: User login, 2: User URL, 3: Blog name, 4: Authentication code, 5: User URL, 6: Blog name */ + __( + '

Hi %1$s,

+

Your current password for %3$s was found in a public leak, which means your account might be at risk.

+

To help protect your account, please enter this code at the login prompt:

+

%4$s

+

If you didn\'t just log into %5$s, please do so now and change your password.

+

Stay secure,
Jetpack

', + 'jetpack-account-protection' + ), esc_html( $user->user_login ), $user_url, $blog_name, diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 61e2ceb48fbc3..d4b4500e30b18 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -111,7 +111,6 @@ public function render_page() { // Handle resend email request if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); - if ( ! $email_resent ) { // TODO: Add error handling -> 'email_resend_error', 'Failed to resend the authentication email or reached the maximum number of attempts.' } @@ -144,15 +143,23 @@ public function render_content( string $redirect_url, string $masked_email ): vo - <?php echo esc_html( 'Jetpack - Secure Your Account' ); ?> + <?php echo esc_html__( 'Jetpack - Secure Your Account', 'jetpack-account-protection' ); ?>
-

-

We've noticed that your current password may have been compromised in a public leak. To keep your account safe, we've added an extra layer of security.

-

We've sent a code to . Please check your inbox and enter the code below to verify it's really you.

+

+

+

+ +

@@ -160,15 +167,15 @@ public function render_content( string $redirect_url, string $masked_email ): vo type="number" name="user_input" class="action-input" - placeholder="Enter verification code" + placeholder="" required /> - +
@@ -206,7 +213,6 @@ private function generate_and_store_transient_data( $user_id ) { ); $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); - if ( ! $transient_set ) { // TODO: Add error handling -> 'transient_set_error', 'Failed to set transient data.' } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 8fa0a6373186f..c59020a0f3e36 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -15,10 +15,10 @@ */ class Validation_Service { /** - * Check if the password is in the list of common/compromised passwords. + * Check if the password is in the list of compromised/common passwords. * * @param string $password The password to check. - * @return bool|\WP_Error True if the password is in the list of common/compromised passwords, false otherwise. + * @return bool|\WP_Error True if the password is in the list of compromised/common passwords, false otherwise. */ public function check_weak_passwords( string $password ) { $api_url = '/jetpack-protect-weak-password'; From 4c794c5f953589f1b9624661e89fc5c666f647c2 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 19:07:21 -0800 Subject: [PATCH 075/120] Fix lint warnings/errors --- .../src/class-password-detection.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index d4b4500e30b18..358213358d6f2 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -121,7 +121,13 @@ public function render_page() { // Handle verify form submission if ( isset( $_POST['verify'] ) ) { - $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null ); + if ( ! empty( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { + $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; + + $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null, $user_input ); + } else { + // TODO: Add error handling -> 'nonce_verification_error', 'Nonce verification failed.' + } } $this->render_content( $this->get_redirect_url( $token ), $this->email_service->mask_email_address( $current_user->user_email ) ); @@ -247,14 +253,9 @@ private function get_redirect_url( $token ) { * @param \WP_User $current_user The current user. * @param string $token The token. * @param string $auth_code The expected auth code. + * @param string $user_input The user input. */ - private function handle_auth_form_submission( $current_user, $token, $auth_code ) { - if ( ! isset( $_POST['_wpnonce_verify'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { - // TODO: Add error handling -> 'nonce_verification_error', 'Nonce verification failed.' - } - - $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; - + private function handle_auth_form_submission( $current_user, $token, $auth_code, $user_input ) { if ( $auth_code && $auth_code === $user_input ) { delete_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); // TODO: Ensure all transient are removed on module and/or plugin deactivation From 0b493b8129e358015fc563f2a76c09a0cf2814be Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 19:12:42 -0800 Subject: [PATCH 076/120] Update todos --- .../account-protection/src/class-password-detection.php | 7 +++---- .../account-protection/src/class-validation-service.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 358213358d6f2..e8d7f441820a5 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -49,7 +49,7 @@ public function login_form_password_detection( $user, string $password ) { } if ( $this->validation_service->check_weak_passwords( $password ) ) { - // TODO: Every time the user logs in we generate a new token based transient. This is not ideal. + // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); @@ -198,7 +198,6 @@ class="action-input" * @return bool */ private function user_requires_protection( $user, $password ) { - // TODO: Only run validation if we haven't already checked? return ( user_can( $user, 'publish_posts' ) || user_can( $user, 'edit_published_posts' ) ) && wp_check_password( $password, $user->user_pass, $user->ID ); } @@ -257,11 +256,11 @@ private function get_redirect_url( $token ) { */ private function handle_auth_form_submission( $current_user, $token, $auth_code, $user_input ) { if ( $auth_code && $auth_code === $user_input ) { + // TODO: Ensure all transient are also removed on module and/or plugin deactivation delete_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); - // TODO: Ensure all transient are removed on module and/or plugin deactivation wp_set_auth_cookie( $current_user->ID, true ); - wp_safe_redirect( admin_url() ); // TODO: Notify user to update their password/redirect to password update page + wp_safe_redirect( admin_url() ); exit; } else { // TODO: Add error handling -> 'auth_code_verification_error', 'Authentication code verification failed.' diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index c59020a0f3e36..3cc842e0dadb6 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -43,7 +43,7 @@ public function check_weak_passwords( string $password ) { if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { return false; - // TODO: Return or log error? + // TODO: Return false or log error? } $body = json_decode( wp_remote_retrieve_body( $response ), true ); From 82d9ff2cc14b60a8e464156dd817f3071c9c3ae1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 28 Jan 2025 20:01:29 -0800 Subject: [PATCH 077/120] Add error handling, enforce input restrictions --- .../src/class-email-service.php | 7 +-- .../src/class-password-detection.php | 46 +++++++++++++------ .../src/css/password-detection.css | 3 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index c4058afb7afa1..27fb623812061 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -125,13 +125,14 @@ public function resend_auth_email( $user, $transient_data, $token ): bool { $auth_code = $this->generate_auth_code(); $transient_data['auth_code'] = $auth_code; - ++$transient_data['resend_attempts']; - if ( ! set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::EMAIL_SENT_EXPIRATION ) ) { + if ( ! $this->send_auth_email( $user, $auth_code ) ) { return false; } - if ( ! $this->send_auth_email( $user, $auth_code ) ) { + ++$transient_data['resend_attempts']; + + if ( ! set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::EMAIL_SENT_EXPIRATION ) ) { return false; } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index e8d7f441820a5..d3c59b8a0147d 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -54,7 +54,7 @@ public function login_form_password_detection( $user, string $password ) { $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); if ( ! $email_sent ) { - // TODO: Add error handling -> 'email_send_error', 'Failed to send the authentication email.'; + set_transient( "password_detection_error_{$user->ID}", esc_html__( 'Failed to send authentication email. Please try again.', 'jetpack-account-protection' ), 60 ); } return new \WP_Error( @@ -112,7 +112,11 @@ public function render_page() { if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { - // TODO: Add error handling -> 'email_resend_error', 'Failed to resend the authentication email or reached the maximum number of attempts.' + if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { + set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ), 60 ); + } else { + set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ), 60 ); + } } wp_safe_redirect( $this->get_redirect_url( $token ) ); @@ -126,22 +130,27 @@ public function render_page() { $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null, $user_input ); } else { - // TODO: Add error handling -> 'nonce_verification_error', 'Nonce verification failed.' + set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ), 60 ); } } - $this->render_content( $this->get_redirect_url( $token ), $this->email_service->mask_email_address( $current_user->user_email ) ); + $this->render_content( $current_user->ID, $this->get_redirect_url( $token ), $this->email_service->mask_email_address( $current_user->user_email ) ); exit; } /** * Render content for password detection page. * + * @param int $user_id The user ID. * @param string $redirect_url The redirect URL. * @param string $masked_email The masked email address. * @return void */ - public function render_content( string $redirect_url, string $masked_email ): void { + public function render_content( $user_id, string $redirect_url, string $masked_email ): void { + $transient_key = "password_detection_error_{$user_id}"; + $error_message = get_transient( $transient_key ); + delete_transient( $transient_key ); + defined( 'ABSPATH' ) || exit; ?> @@ -169,12 +178,17 @@ public function render_content( string $redirect_url, string $masked_email ): vo
-
@@ -183,6 +197,9 @@ class="action-input"

+ +

+
@@ -214,12 +231,13 @@ private function generate_and_store_transient_data( $user_id ) { $data = array( 'user_id' => $user_id, 'auth_code' => $auth_code, - 'resend_attempts' => 0, + 'resend_attempts' => 1, ); $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); if ( ! $transient_set ) { - // TODO: Add error handling -> 'transient_set_error', 'Failed to set transient data.' + set_transient( "password_detection_error_{$user_id}", esc_html__( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ), 60 ); + } return array( @@ -263,7 +281,7 @@ private function handle_auth_form_submission( $current_user, $token, $auth_code, wp_safe_redirect( admin_url() ); exit; } else { - // TODO: Add error handling -> 'auth_code_verification_error', 'Authentication code verification failed.' + set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ), 60 ); } } diff --git a/projects/packages/account-protection/src/css/password-detection.css b/projects/packages/account-protection/src/css/password-detection.css index 62d352e6dee2c..d568caa5b0017 100644 --- a/projects/packages/account-protection/src/css/password-detection.css +++ b/projects/packages/account-protection/src/css/password-detection.css @@ -55,6 +55,7 @@ font-size: 13px; } -.email-status { +.email-status, +.error-message { text-align: center; } From b466475f471c2d2339813cafc84e537cbd2587bf Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 05:21:18 -0800 Subject: [PATCH 078/120] Move main constants back entry file --- .../src/class-account-protection.php | 17 ++++++++++------- .../account-protection/src/class-config.php | 8 +++----- .../src/class-password-detection.php | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 6d0452cce3e74..3c8015052a9f9 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -13,6 +13,9 @@ * Class Account_Protection */ class Account_Protection { + public const PACKAGE_VERSION = '0.1.0-alpha'; + public const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; + /** * Modules instance. * @@ -54,8 +57,8 @@ public function init(): void { */ private function register_hooks(): void { // Account protection activation/deactivation hooks - add_action( 'jetpack_activate_module_' . Config::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); - add_action( 'jetpack_deactivate_module_' . Config::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); + add_action( 'jetpack_activate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_activation' ) ); + add_action( 'jetpack_deactivate_module_' . self::ACCOUNT_PROTECTION_MODULE_NAME, array( $this, 'on_account_protection_deactivation' ) ); // Do not run in unsupported environments add_action( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) ); @@ -96,7 +99,7 @@ public function on_account_protection_deactivation(): void { * @return bool */ public function is_enabled() { - return $this->modules->is_active( Config::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -109,7 +112,7 @@ public function enable() { if ( $this->is_enabled() ) { return true; } - return $this->modules->activate( Config::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); + return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false ); } /** @@ -122,7 +125,7 @@ public function disable(): bool { if ( ! $this->is_enabled() ) { return true; } - return $this->modules->deactivate( Config::ACCOUNT_PROTECTION_MODULE_NAME ); + return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME ); } /** @@ -149,7 +152,7 @@ public function is_supported_environment(): bool { public function remove_module_on_unsupported_environments( array $modules ): array { if ( ! $this->is_supported_environment() ) { // Account protection should never be available on unsupported platforms. - unset( $modules[ Config::ACCOUNT_PROTECTION_MODULE_NAME ] ); + unset( $modules[ self::ACCOUNT_PROTECTION_MODULE_NAME ] ); } return $modules; @@ -168,7 +171,7 @@ public function remove_standalone_module_on_unsupported_environments( array $mod $modules = array_filter( $modules, function ( $module ) { - return $module !== Config::ACCOUNT_PROTECTION_MODULE_NAME; + return $module !== self::ACCOUNT_PROTECTION_MODULE_NAME; } ); diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index 359e6c7648491..239569c540a4c 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -11,9 +11,7 @@ * Class Config */ class Config { - public const PACKAGE_VERSION = '0.1.0-alpha'; - public const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; - public const TRANSIENT_PREFIX = 'password_detection'; - public const EMAIL_SENT_EXPIRATION = 600; // 10 minutes - public const MAX_RESEND_ATTEMPTS = 3; + public const TRANSIENT_PREFIX = 'password_detection'; + public const EMAIL_SENT_EXPIRATION = 600; // 10 minutes + public const MAX_RESEND_ATTEMPTS = 3; } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index d3c59b8a0147d..c6f64b77e120e 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -312,7 +312,7 @@ public function enqueue_styles(): void { 'password-detection-styles', plugin_dir_url( __FILE__ ) . 'css/password-detection.css', array(), - Config::PACKAGE_VERSION + Account_Protection::PACKAGE_VERSION ); } } From 7f7b57dc47d01b61a291ed4efb3e49c1c2aba1a1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 05:38:25 -0800 Subject: [PATCH 079/120] Fix package version check --- .../account-protection/src/class-account-protection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 3c8015052a9f9..c499b8140a990 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -13,8 +13,8 @@ * Class Account_Protection */ class Account_Protection { - public const PACKAGE_VERSION = '0.1.0-alpha'; - public const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; + const PACKAGE_VERSION = '0.1.0-alpha'; + const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; /** * Modules instance. From fe79de3d0974896a7b7170d1300c060dc0ec9b26 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 06:10:59 -0800 Subject: [PATCH 080/120] Optimize setting error transient --- .../account-protection/src/class-config.php | 2 + .../src/class-password-detection.php | 40 +++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index 239569c540a4c..99d461441752a 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -12,6 +12,8 @@ */ class Config { public const TRANSIENT_PREFIX = 'password_detection'; + public const ERROR_CODE = 'password_detection_validation_error'; + public const ERROR_MESSAGE = 'Password validation failed.'; public const EMAIL_SENT_EXPIRATION = 600; // 10 minutes public const MAX_RESEND_ATTEMPTS = 3; } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index c6f64b77e120e..f38e698f48d2e 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -54,12 +54,12 @@ public function login_form_password_detection( $user, string $password ) { $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); if ( ! $email_sent ) { - set_transient( "password_detection_error_{$user->ID}", esc_html__( 'Failed to send authentication email. Please try again.', 'jetpack-account-protection' ), 60 ); + $this->set_transient_error( $user->ID, __( 'Failed to send authentication email. Please try again.', 'jetpack-account-protection' ) ); } return new \WP_Error( - 'password_detection_validation_error', - 'Password detection validation error', + Config::ERROR_CODE, + Config::ERROR_MESSAGE, array( 'token' => $transient['token'] ) ); } @@ -112,11 +112,13 @@ public function render_page() { if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { + $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); + if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { - set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ), 60 ); - } else { - set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ), 60 ); + $message = __( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ); } + + $this->set_transient_error( $current_user->ID, $message ); } wp_safe_redirect( $this->get_redirect_url( $token ) ); @@ -130,7 +132,7 @@ public function render_page() { $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null, $user_input ); } else { - set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ), 60 ); + $this->set_transient_error( $current_user->ID, __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); } } @@ -147,7 +149,7 @@ public function render_page() { * @return void */ public function render_content( $user_id, string $redirect_url, string $masked_email ): void { - $transient_key = "password_detection_error_{$user_id}"; + $transient_key = Config::TRANSIENT_PREFIX . "_error_{$user_id}"; $error_message = get_transient( $transient_key ); delete_transient( $transient_key ); @@ -215,7 +217,11 @@ class="action-input" * @return bool */ private function user_requires_protection( $user, $password ) { - return ( user_can( $user, 'publish_posts' ) || user_can( $user, 'edit_published_posts' ) ) && wp_check_password( $password, $user->user_pass, $user->ID ); + if ( ! user_can( $user, 'publish_posts' ) && ! user_can( $user, 'edit_published_posts' ) ) { + return false; + } + + return wp_check_password( $password, $user->user_pass, $user->ID ); } /** @@ -236,8 +242,7 @@ private function generate_and_store_transient_data( $user_id ) { $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); if ( ! $transient_set ) { - set_transient( "password_detection_error_{$user_id}", esc_html__( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ), 60 ); - + $this->set_transient_error( $user_id, __( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ) ); } return array( @@ -281,10 +286,21 @@ private function handle_auth_form_submission( $current_user, $token, $auth_code, wp_safe_redirect( admin_url() ); exit; } else { - set_transient( "password_detection_error_{$current_user->ID}", esc_html__( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ), 60 ); + $this->set_transient_error( $current_user->ID, __( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ) ); } } + /** + * Set a transient error message. + * + * @param int $user_id The user ID. + * @param string $message The error message. + * @param int $expiration The expiration time in seconds. + */ + private function set_transient_error( int $user_id, string $message, int $expiration = 60 ): void { + set_transient( Config::TRANSIENT_PREFIX . "_error_{$user_id}", esc_html( $message ), $expiration ); + } + /** * Mask an email address like d*****@g*****.com. * From 674384135ee6eeb2774a86698fd5fa931add09a1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 06:22:33 -0800 Subject: [PATCH 081/120] Add nonce check for resend email action --- .../src/class-password-detection.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index f38e698f48d2e..27af1c8fc8bbb 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -109,8 +109,12 @@ public function render_page() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); // Handle resend email request - if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { - $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); + if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' + && isset( $_GET['_wpnonce'] ) + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'resend_email_nonce' ) + ) { + + $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); @@ -121,8 +125,11 @@ public function render_page() { $this->set_transient_error( $current_user->ID, $message ); } - wp_safe_redirect( $this->get_redirect_url( $token ) ); - exit; + wp_safe_redirect( $this->get_redirect_url( $token ) ); + exit; + } else { + $this->set_transient_error( $current_user->ID, __( 'Resend nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + } // Handle verify form submission @@ -132,7 +139,7 @@ public function render_page() { $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null, $user_input ); } else { - $this->set_transient_error( $current_user->ID, __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + $this->set_transient_error( $current_user->ID, __( 'Verify nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); } } @@ -197,7 +204,9 @@ class="action-input"

From 88eed6ee01f6b9f219f3d71a568c2545a344bf05 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 06:24:28 -0800 Subject: [PATCH 082/120] Fix spacing --- .../packages/account-protection/src/class-password-detection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 27af1c8fc8bbb..788e0f6fa3053 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -113,7 +113,6 @@ public function render_page() { && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'resend_email_nonce' ) ) { - $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); if ( ! $email_resent ) { $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); From 4e0be98b9a7668d2e67befa70e99cc64754c465c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 06:29:23 -0800 Subject: [PATCH 083/120] Fix resend nonce handling --- .../src/class-password-detection.php | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 788e0f6fa3053..82b8229432280 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -109,26 +109,27 @@ public function render_page() { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); // Handle resend email request - if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' - && isset( $_GET['_wpnonce'] ) - && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'resend_email_nonce' ) - ) { - $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); - if ( ! $email_resent ) { - $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); - - if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { - $message = __( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ); + if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { + if ( isset( $_GET['_wpnonce'] ) + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'resend_email_nonce' ) + ) { + $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); + if ( ! $email_resent ) { + $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); + + if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { + $message = __( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ); + } + + $this->set_transient_error( $current_user->ID, $message ); } - $this->set_transient_error( $current_user->ID, $message ); - } - - wp_safe_redirect( $this->get_redirect_url( $token ) ); - exit; - } else { - $this->set_transient_error( $current_user->ID, __( 'Resend nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + wp_safe_redirect( $this->get_redirect_url( $token ) ); + exit; + } else { + $this->set_transient_error( $current_user->ID, __( 'Resend nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + } } // Handle verify form submission From 490e50b698a0d5ccabb8a2f1001179582a31143a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 10:14:34 -0800 Subject: [PATCH 084/120] Email service fixes --- .../src/class-email-service.php | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index 27fb623812061..bdd59d4199ec5 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -18,17 +18,15 @@ class Email_Service { /** * Send auth email. * - * @param WP_User $user The user. - * @param string $auth_code The authentication code. + * @param \WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ public function send_auth_email( $user, $auth_code ): bool { $wp_send = $this->wp_send_auth_email( $user, $auth_code ); if ( ! $wp_send ) { - $api_send = $this->api_send_auth_email( $user, $auth_code ); - - return $api_send; + return $this->api_send_auth_email( $user, $auth_code ); } return true; @@ -37,13 +35,13 @@ public function send_auth_email( $user, $auth_code ): bool { /** * Send the email using wp_mail(). * - * @param WP_User $user The user. - * @param string $auth_code The authentication code. + * @param \WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ private function wp_send_auth_email( $user, $auth_code ) { $blog_name = esc_html( get_bloginfo( 'name' ) ); - $user_url = ! empty( $user->user_url ) ? esc_url( $user->user_url ) : esc_url( home_url() ); + $blog_url = esc_url( get_bloginfo( 'url' ) ); $subject = esc_html__( 'Verify your identity at Jetpack', 'jetpack-account-protection' ); $message = sprintf( @@ -58,10 +56,10 @@ private function wp_send_auth_email( $user, $auth_code ) { 'jetpack-account-protection' ), esc_html( $user->user_login ), - $user_url, + $blog_url, $blog_name, esc_html( $auth_code ), - $user_url, + $blog_url, $blog_name ); @@ -73,8 +71,8 @@ private function wp_send_auth_email( $user, $auth_code ) { /** * Send the email using the API. * - * @param WP_User $user The user. - * @param string $auth_code The authentication code. + * @param \WP_User $user The user. + * @param string $auth_code The authentication code. * @return bool True if the email was sent successfully, false otherwise. */ private function api_send_auth_email( $user, $auth_code ) { From d47a220ee162aaf54d6c271ceba2d1a345de66be Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 11:00:57 -0800 Subject: [PATCH 085/120] Fixes, improvements to doc consistency --- .../src/class-account-protection.php | 14 ++- .../src/class-email-service.php | 19 ++-- .../src/class-password-detection.php | 98 +++++++++---------- .../src/class-validation-service.php | 5 +- 4 files changed, 72 insertions(+), 64 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index c499b8140a990..72900f144154f 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -43,6 +43,8 @@ public function __construct( ?Modules $modules = null, ?Password_Detection $pass /** * Initializes the configurations needed for the account protection module. + * + * @return void */ public function init(): void { $this->register_hooks(); @@ -54,6 +56,8 @@ public function init(): void { /** * Register hooks for module activation and environment validation. + * + * @return void */ private function register_hooks(): void { // Account protection activation/deactivation hooks @@ -67,6 +71,8 @@ private function register_hooks(): void { /** * Register hooks for runtime operations. + * + * @return void */ private function register_runtime_hooks(): void { // Validate password after successful login @@ -81,6 +87,8 @@ private function register_runtime_hooks(): void { /** * Activate the account protection on module activation. + * + * @return void */ public function on_account_protection_activation(): void { // Activation logic can be added here @@ -88,6 +96,8 @@ public function on_account_protection_activation(): void { /** * Deactivate the account protection on module deactivation. + * + * @return void */ public function on_account_protection_deactivation(): void { // Deactivation logic can be added here @@ -98,7 +108,7 @@ public function on_account_protection_deactivation(): void { * * @return bool */ - public function is_enabled() { + public function is_enabled(): bool { return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME ); } @@ -107,7 +117,7 @@ public function is_enabled() { * * @return bool */ - public function enable() { + public function enable(): bool { // Return true if already enabled. if ( $this->is_enabled() ) { return true; diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index bdd59d4199ec5..6c3df30ef8483 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -20,9 +20,10 @@ class Email_Service { * * @param \WP_User $user The user. * @param string $auth_code The authentication code. + * * @return bool True if the email was sent successfully, false otherwise. */ - public function send_auth_email( $user, $auth_code ): bool { + public function send_auth_email( \WP_User $user, string $auth_code ): bool { $wp_send = $this->wp_send_auth_email( $user, $auth_code ); if ( ! $wp_send ) { @@ -37,9 +38,10 @@ public function send_auth_email( $user, $auth_code ): bool { * * @param \WP_User $user The user. * @param string $auth_code The authentication code. + * * @return bool True if the email was sent successfully, false otherwise. */ - private function wp_send_auth_email( $user, $auth_code ) { + private function wp_send_auth_email( \WP_User $user, string $auth_code ): bool { $blog_name = esc_html( get_bloginfo( 'name' ) ); $blog_url = esc_url( get_bloginfo( 'url' ) ); @@ -73,9 +75,10 @@ private function wp_send_auth_email( $user, $auth_code ) { * * @param \WP_User $user The user. * @param string $auth_code The authentication code. + * * @return bool True if the email was sent successfully, false otherwise. */ - private function api_send_auth_email( $user, $auth_code ) { + private function api_send_auth_email( \WP_User $user, string $auth_code ): bool { $blog_id = Jetpack_Options::get_option( 'id' ); $is_connected = ( new Connection_Manager() )->is_connected(); @@ -111,12 +114,13 @@ private function api_send_auth_email( $user, $auth_code ) { /** * Resend email attempts. * - * @param WP_User $user The user. - * @param array $transient_data The transient data. - * @param string $token The token. + * @param \WP_User $user The user. + * @param array $transient_data The transient data. + * @param string $token The token. + * * @return bool True if the email was resent successfully, false otherwise. */ - public function resend_auth_email( $user, $transient_data, $token ): bool { + public function resend_auth_email( \WP_User $user, array $transient_data, string $token ): bool { if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { return false; } @@ -150,6 +154,7 @@ public function generate_auth_code(): string { * Mask an email address like d*****@g*****.com. * * @param string $email The email address to mask. + * * @return string The masked email address. */ public function mask_email_address( string $email ): string { diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 82b8229432280..d9534a26f7975 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -41,6 +41,7 @@ public function __construct( ?Email_Service $email_service = null, ?Validation_S * * @param \WP_User|\WP_Error $user The user or error object. * @param string $password The password. + * * @return \WP_User|\WP_Error The user object. */ public function login_form_password_detection( $user, string $password ) { @@ -72,9 +73,10 @@ public function login_form_password_detection( $user, string $password ) { * * @param string $username The username. * @param \WP_Error $error The error object. - * @return never + * + * @return void */ - public function handle_password_detection_validation_error( $username, $error ) { + public function handle_password_detection_validation_error( string $username, \WP_Error $error ): void { if ( isset( $error->errors['password_detection_validation_error'] ) ) { $token = $error->get_error_data()['token']; wp_safe_redirect( $this->get_redirect_url( $token ) ); @@ -85,9 +87,9 @@ public function handle_password_detection_validation_error( $username, $error ) /** * Render password detection page. * - * @return never + * @return void */ - public function render_page() { + public function render_page(): void { if ( is_user_logged_in() ) { wp_safe_redirect( admin_url() ); exit; @@ -99,10 +101,9 @@ public function render_page() { $this->redirect_to_login(); } - $user_id = $transient_data['user_id'] ?? null; - $current_user = $user_id ? get_user_by( 'ID', $user_id ) : null; - - if ( ! $current_user ) { + $user_id = $transient_data['user_id'] ?? null; + $user = $user_id ? get_user_by( 'ID', $user_id ) : null; + if ( ! $user ) { $this->redirect_to_login(); } @@ -113,7 +114,7 @@ public function render_page() { if ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'resend_email_nonce' ) ) { - $email_resent = $this->email_service->resend_auth_email( $current_user, $transient_data, $token ); + $email_resent = $this->email_service->resend_auth_email( $user, $transient_data, $token ); if ( ! $email_resent ) { $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); @@ -121,14 +122,13 @@ public function render_page() { $message = __( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ); } - $this->set_transient_error( $current_user->ID, $message ); + $this->set_transient_error( $user->ID, $message ); } - wp_safe_redirect( $this->get_redirect_url( $token ) ); - exit; + wp_safe_redirect( $this->get_redirect_url( $token ) ); + exit; } else { - $this->set_transient_error( $current_user->ID, __( 'Resend nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); - + $this->set_transient_error( $user->ID, __( 'Resend nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); } } @@ -137,26 +137,26 @@ public function render_page() { if ( ! empty( $_POST['_wpnonce_verify'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_verify'] ) ), 'verify_action' ) ) { $user_input = isset( $_POST['user_input'] ) ? sanitize_text_field( wp_unslash( $_POST['user_input'] ) ) : null; - $this->handle_auth_form_submission( $current_user, $token, $transient_data['auth_code'] ?? null, $user_input ); + $this->handle_auth_form_submission( $user, $token, $transient_data['auth_code'] ?? null, $user_input ); } else { - $this->set_transient_error( $current_user->ID, __( 'Verify nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + $this->set_transient_error( $user->ID, __( 'Verify nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); } } - $this->render_content( $current_user->ID, $this->get_redirect_url( $token ), $this->email_service->mask_email_address( $current_user->user_email ) ); + $this->render_content( $user, $token ); exit; } /** * Render content for password detection page. * - * @param int $user_id The user ID. - * @param string $redirect_url The redirect URL. - * @param string $masked_email The masked email address. + * @param \WP_User $user The user. + * @param string $token The token. + * * @return void */ - public function render_content( $user_id, string $redirect_url, string $masked_email ): void { - $transient_key = Config::TRANSIENT_PREFIX . "_error_{$user_id}"; + public function render_content( \WP_User $user, string $token ): void { + $transient_key = Config::TRANSIENT_PREFIX . "_error_{$user->ID}"; $error_message = get_transient( $transient_key ); delete_transient( $transient_key ); @@ -167,20 +167,20 @@ public function render_content( $user_id, string $redirect_url, string $masked_e - <?php echo esc_html__( 'Jetpack - Secure Your Account', 'jetpack-account-protection' ); ?> + <?php esc_html_e( 'Jetpack - Secure Your Account', 'jetpack-account-protection' ); ?>
-

-

+

+

email_service->mask_email_address( $user->user_email ) ) ); ?>

@@ -204,7 +204,7 @@ class="action-input"
@@ -223,9 +223,10 @@ class="action-input" * * @param \WP_User $user The user object. * @param string $password The password. + * * @return bool */ - private function user_requires_protection( $user, $password ) { + private function user_requires_protection( \WP_User $user, string $password ): bool { if ( ! user_can( $user, 'publish_posts' ) && ! user_can( $user, 'edit_published_posts' ) ) { return false; } @@ -237,9 +238,10 @@ private function user_requires_protection( $user, $password ) { * Generate and store a consolidated transient for the user. * * @param int $user_id The user ID. - * @return string The generated token. + * + * @return array An array of the generated token and auth code. */ - private function generate_and_store_transient_data( $user_id ) { + private function generate_and_store_transient_data( int $user_id ): array { $token = wp_generate_password( 32, false, false ); $auth_code = $this->email_service->generate_auth_code(); @@ -262,8 +264,10 @@ private function generate_and_store_transient_data( $user_id ) { /** * Redirect to the login page. + * + * @return void */ - private function redirect_to_login() { + private function redirect_to_login(): void { wp_safe_redirect( wp_login_url() ); exit; } @@ -272,30 +276,33 @@ private function redirect_to_login() { * Get redirect URL. * * @param string $token The token. + * * @return string The redirect URL. */ - private function get_redirect_url( $token ) { + private function get_redirect_url( string $token ): string { return home_url( '/wp-login.php?action=password-detection&token=' . $token ); } /** * Handle auth form submission. * - * @param \WP_User $current_user The current user. + * @param \WP_User $user The current user. * @param string $token The token. * @param string $auth_code The expected auth code. * @param string $user_input The user input. + * + * @return void */ - private function handle_auth_form_submission( $current_user, $token, $auth_code, $user_input ) { + private function handle_auth_form_submission( \WP_User $user, string $token, string $auth_code, string $user_input ): void { if ( $auth_code && $auth_code === $user_input ) { // TODO: Ensure all transient are also removed on module and/or plugin deactivation delete_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); - wp_set_auth_cookie( $current_user->ID, true ); + wp_set_auth_cookie( $user->ID, true ); // TODO: Notify user to update their password/redirect to password update page wp_safe_redirect( admin_url() ); exit; } else { - $this->set_transient_error( $current_user->ID, __( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ) ); + $this->set_transient_error( $user->ID, __( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ) ); } } @@ -305,28 +312,13 @@ private function handle_auth_form_submission( $current_user, $token, $auth_code, * @param int $user_id The user ID. * @param string $message The error message. * @param int $expiration The expiration time in seconds. + * + * @return void */ private function set_transient_error( int $user_id, string $message, int $expiration = 60 ): void { set_transient( Config::TRANSIENT_PREFIX . "_error_{$user_id}", esc_html( $message ), $expiration ); } - /** - * Mask an email address like d*****@g*****.com. - * - * @param string $email The email address to mask. - * @return string The masked email address. - */ - public function mask_email_address( string $email ): string { - $parts = explode( '@', $email ); - $name = $parts[0]; - $domain = $parts[1]; - $masked_name = substr( $name, 0, 1 ) . str_repeat( '*', strlen( $name ) - 1 ); - $domain_parts = explode( '.', $domain ); - $masked_domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 ) . '.' . $domain_parts[1]; - - return $masked_name . '@' . $masked_domain; - } - /** * Enqueue the password detection page styles. * diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 3cc842e0dadb6..2446746c3ff58 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -18,9 +18,10 @@ class Validation_Service { * Check if the password is in the list of compromised/common passwords. * * @param string $password The password to check. - * @return bool|\WP_Error True if the password is in the list of compromised/common passwords, false otherwise. + * + * @return bool True if the password is in the list of compromised/common passwords, false otherwise. */ - public function check_weak_passwords( string $password ) { + public function check_weak_passwords( string $password ): bool { $api_url = '/jetpack-protect-weak-password'; $is_connected = ( new Connection_Manager() )->is_connected(); From 7e87875dba7698451d5b991db9a7104bf4e7822a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 11:37:18 -0800 Subject: [PATCH 086/120] Add remaining password validation --- .../src/class-password-detection.php | 3 + .../src/class-validation-service.php | 149 ++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index d9534a26f7975..21ac671d72f94 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -49,6 +49,9 @@ public function login_form_password_detection( $user, string $password ) { return $user; } + // TODO: Do we want to do this for every user, every time? + $this->validation_service->save_recent_password( $user->ID, $password ); + if ( $this->validation_service->check_weak_passwords( $password ) ) { // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 2446746c3ff58..5c6aa4c0e5c52 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -14,6 +14,131 @@ * Class Validation_Service */ class Validation_Service { + /** + * Save the new password to the user's recent passwords list. + * + * @param int $user_id The user ID. + * @param string $password The password to store. + */ + public function save_recent_password( int $user_id, string $password ) { + // TODO: Use a timestamp for last used password and remove old ones - beyond 1 year? + $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); + + if ( ! is_array( $recent_passwords ) ) { + $recent_passwords = array(); + } + + $hashed_password = wp_hash_password( $password ); + if ( in_array( $hashed_password, $recent_passwords, true ) ) { + return; + } + + // Add the new hashed password and keep only the last 5 + array_unshift( $recent_passwords, $hashed_password ); + $recent_passwords = array_slice( $recent_passwords, 0, 5 ); + + update_user_meta( $user_id, 'recent_passwords', $recent_passwords ); + } + + /** + * Validate password against security conditions. + * + * @param string $password The password to check. + * @param int $user_id The user ID. + * + * @return array An array of validation errors (if any). + */ + public function validate_password( string $password, int $user_id ): array { + $errors = array(); + + if ( $this->contains_backslash( $password ) ) { + $errors[] = __( 'Doesn\'t contain a backslash (\\) character', 'jetpack-account-protection' ); + } + + if ( ! $this->check_length( $password ) ) { + $errors[] = __( 'Between 6 and 150 characters', 'jetpack-account-protection' ); + } + + if ( $this->check_weak_passwords( $password ) ) { + $errors[] = __( 'Not a common password.', 'jetpack-account-protection' ); + } + + // TODO: Modify the method to return common or compromised. + // if ( $this->check_weak_passwords( $password ) ) { + // $errors[] = __( 'Not a leaked password.', 'jetpack-account-protection' ); + // } + + if ( $this->matches_user_data( $password, $user_id ) ) { + $errors[] = __( 'Doesn\'t match user data', 'jetpack-account-protection' ); + } + + if ( $this->is_recent_password( $password, $user_id ) ) { + $errors[] = __( 'Not used recently', 'jetpack-account-protection' ); + } + + return $errors; + } + + /** + * Check if the password contains a backslash. + * + * @param string $password The password to check. + * + * @return bool True if the password contains a backslash, false otherwise. + */ + private function contains_backslash( string $password ): bool { + return strpos( $password, '\\' ) !== false; + } + + /** + * Check if the password length is within the allowed range. + * + * @param string $password The password to check. + * + * @return bool True if the password is between 6 and 150 characters, false otherwise. + */ + private function check_length( string $password ): bool { + $length = strlen( $password ); + return $length >= 6 && $length <= 150; + } + + /** + * Check if the password matches any user data. + * + * @param string $password The password to check. + * @param int $user_id The user ID. + * + * @return bool True if the password matches any user data, false otherwise. + */ + private function matches_user_data( string $password, int $user_id ): bool { + $user = get_userdata( $user_id ); + + if ( ! $user ) { + return false; + } + + $user_data = array( + $user->user_login, + $user->user_nicename, + $user->display_name, + $user->first_name, + $user->last_name, + $user->user_email, + explode( '@', $user->user_email )[0], // Email username + $user->nickname, + ); + + $password_lower = strtolower( $password ); + + foreach ( $user_data as $data ) { + if ( ! empty( $data ) && strpos( $password_lower, strtolower( $data ) ) !== false ) { + return true; + } + } + + return false; + } + /** * Check if the password is in the list of compromised/common passwords. * @@ -60,4 +185,28 @@ public function check_weak_passwords( string $password ): bool { return false; } + + /** + * Check if the password has been used recently by the user. + * + * @param string $password The password to check. + * @param int $user_id The user ID. + * + * @return bool True if the password was recently used, false otherwise. + */ + private function is_recent_password( string $password, int $user_id ): bool { + $recent_passwords = get_user_meta( $user_id, 'recent_passwords', true ); + + if ( empty( $recent_passwords ) || ! is_array( $recent_passwords ) ) { + return false; + } + + foreach ( $recent_passwords as $old_hashed_password ) { + if ( wp_check_password( $password, $old_hashed_password ) ) { + return true; + } + } + + return false; + } } From 38a2d1566d7d33b7b2abcabec81ccde979c8f1f7 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 13:39:45 -0800 Subject: [PATCH 087/120] Update weak password check returns --- .../src/class-password-detection.php | 3 +- .../src/class-validation-service.php | 46 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 21ac671d72f94..8627a7dff2c15 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -52,7 +52,8 @@ public function login_form_password_detection( $user, string $password ) { // TODO: Do we want to do this for every user, every time? $this->validation_service->save_recent_password( $user->ID, $password ); - if ( $this->validation_service->check_weak_passwords( $password ) ) { + $weak_password_status = $this->validation_service->check_weak_passwords( $password ); + if ( $weak_password_status['compromised'] || $weak_password_status['common'] ) { // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 5c6aa4c0e5c52..f666f9cdfa754 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -59,14 +59,15 @@ public function validate_password( string $password, int $user_id ): array { $errors[] = __( 'Between 6 and 150 characters', 'jetpack-account-protection' ); } - if ( $this->check_weak_passwords( $password ) ) { + $weak_password_status = $this->check_weak_passwords( $password ); + + if ( ! $weak_password_status['common'] ) { $errors[] = __( 'Not a common password.', 'jetpack-account-protection' ); } - // TODO: Modify the method to return common or compromised. - // if ( $this->check_weak_passwords( $password ) ) { - // $errors[] = __( 'Not a leaked password.', 'jetpack-account-protection' ); - // } + if ( ! $weak_password_status['compromised'] ) { + $errors[] = __( 'Not a leaked password.', 'jetpack-account-protection' ); + } if ( $this->matches_user_data( $password, $user_id ) ) { $errors[] = __( 'Doesn\'t match user data', 'jetpack-account-protection' ); @@ -140,18 +141,25 @@ private function matches_user_data( string $password, int $user_id ): bool { } /** - * Check if the password is in the list of compromised/common passwords. + * Check if the password is in the list of compromised or common passwords. * * @param string $password The password to check. * - * @return bool True if the password is in the list of compromised/common passwords, false otherwise. + * @return array An associative array with: + * - 'compromised' => true if the password is found in a known data breach, false otherwise. + * - 'common' => true if the password is commonly used, false otherwise. + * - 'error' => true if an issue occurred while checking the password, false otherwise. */ - public function check_weak_passwords( string $password ): bool { + public function check_weak_passwords( string $password ): array { $api_url = '/jetpack-protect-weak-password'; $is_connected = ( new Connection_Manager() )->is_connected(); if ( ! $is_connected ) { - return new \WP_Error( 'site_not_connected' ); + return array( + 'error' => true, + 'compromised' => false, + 'common' => false, + ); } $hashed_password = sha1( $password ); @@ -168,22 +176,22 @@ public function check_weak_passwords( string $password ): bool { $response_code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { - return false; - // TODO: Return false or log error? + return array( + 'error' => true, + 'compromised' => false, + 'common' => false, + ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $password_suffix = substr( $hashed_password, 5 ); - if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { - return true; - } - if ( in_array( $password_suffix, $body['common'] ?? array(), true ) ) { - return true; - } - - return false; + return array( + 'error' => false, + 'compromised' => in_array( $password_suffix, $body['compromised'] ?? array(), true ), + 'common' => in_array( $password_suffix, $body['common'] ?? array(), true ), + ); } /** From 9838e09c54a957e51e52c7f6ec3068ba83620258 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 13:53:53 -0800 Subject: [PATCH 088/120] Fix phan errors --- .../src/class-password-detection.php | 17 +++++++++++------ .../src/class-validation-service.php | 3 +-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index d9534a26f7975..5e53a283fb6ef 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -87,9 +87,9 @@ public function handle_password_detection_validation_error( string $username, \W /** * Render password detection page. * - * @return void + * @return never */ - public function render_page(): void { + public function render_page() { if ( is_user_logged_in() ) { wp_safe_redirect( admin_url() ); exit; @@ -103,7 +103,7 @@ public function render_page(): void { $user_id = $transient_data['user_id'] ?? null; $user = $user_id ? get_user_by( 'ID', $user_id ) : null; - if ( ! $user ) { + if ( ! $user instanceof \WP_User ) { $this->redirect_to_login(); } @@ -143,7 +143,12 @@ public function render_page(): void { } } - $this->render_content( $user, $token ); + if ( $user instanceof \WP_User ) { + $this->render_content( $user, $token ); + } else { + $this->redirect_to_login(); + } + exit; } @@ -265,9 +270,9 @@ private function generate_and_store_transient_data( int $user_id ): array { /** * Redirect to the login page. * - * @return void + * @return never */ - private function redirect_to_login(): void { + private function redirect_to_login() { wp_safe_redirect( wp_login_url() ); exit; } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 2446746c3ff58..9f1e4d8fef9ed 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -26,7 +26,7 @@ public function check_weak_passwords( string $password ): bool { $is_connected = ( new Connection_Manager() )->is_connected(); if ( ! $is_connected ) { - return new \WP_Error( 'site_not_connected' ); + return false; } $hashed_password = sha1( $password ); @@ -44,7 +44,6 @@ public function check_weak_passwords( string $password ): bool { if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { return false; - // TODO: Return false or log error? } $body = json_decode( wp_remote_retrieve_body( $response ), true ); From 7d4b46ff3d33b8025c2a2e44e078208cd2d3c1a1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 13:54:57 -0800 Subject: [PATCH 089/120] Revert prior change --- .../account-protection/src/class-password-detection.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 5e53a283fb6ef..17a5ce1592d18 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -143,12 +143,7 @@ public function render_page() { } } - if ( $user instanceof \WP_User ) { - $this->render_content( $user, $token ); - } else { - $this->redirect_to_login(); - } - + $this->render_content( $user, $token ); exit; } From 6c522611d7fdc8b1be3fc000f40100297ec2aaf2 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 29 Jan 2025 14:19:03 -0800 Subject: [PATCH 090/120] Fix meta key --- .../account-protection/src/class-validation-service.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index f666f9cdfa754..0bf40664981ad 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -37,7 +37,7 @@ public function save_recent_password( int $user_id, string $password ) { array_unshift( $recent_passwords, $hashed_password ); $recent_passwords = array_slice( $recent_passwords, 0, 5 ); - update_user_meta( $user_id, 'recent_passwords', $recent_passwords ); + update_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', $recent_passwords ); } /** @@ -203,7 +203,7 @@ public function check_weak_passwords( string $password ): array { * @return bool True if the password was recently used, false otherwise. */ private function is_recent_password( string $password, int $user_id ): bool { - $recent_passwords = get_user_meta( $user_id, 'recent_passwords', true ); + $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); if ( empty( $recent_passwords ) || ! is_array( $recent_passwords ) ) { return false; From 06f60083321a280d8338851da516a79caed59c86 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 30 Jan 2025 08:55:03 -0800 Subject: [PATCH 091/120] Add process for add/updating recent pass list --- .../src/class-account-protection.php | 21 ++++- .../src/class-password-detection.php | 3 - .../src/class-validation-service.php | 81 ++++++++++++++----- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index 72900f144154f..f83409faa404d 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -30,15 +30,24 @@ class Account_Protection { */ private $password_detection; + /** + * Validation service instance + * + * @var Validation_Service + */ + private $validation_service; + /** * Account_Protection constructor. * * @param ?Modules $modules Modules instance. * @param ?Password_Detection $password_detection Password detection instance. + * @param ?Validation_Service $validation_service Validation service instance. */ - public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null ) { + public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Validation_Service $validation_service = null ) { $this->modules = $modules ?? new Modules(); $this->password_detection = $password_detection ?? new Password_Detection(); + $this->validation_service = $validation_service ?? new Validation_Service(); } /** @@ -83,6 +92,16 @@ private function register_runtime_hooks(): void { // Add password detection flow add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 ); + + // Add password validation + add_action( 'registration_errors', array( $this->validation_service, 'validate_user_register' ), 10, 2 ); + add_action( 'user_profile_update_errors', array( $this->validation_service, 'validate_profile_update' ), 10, 2 ); + add_action( 'validate_password_reset', array( $this->validation_service, 'validate_after_password_reset' ), 10, 2 ); + + // Update recent passwords list + // user_register + // profile_update + // after_password_reset } /** diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index f66a61e6e9232..6d3e66a6d3873 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -49,9 +49,6 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - // TODO: Do we want to do this for every user, every time? - $this->validation_service->save_recent_password( $user->ID, $password ); - $weak_password_status = $this->validation_service->check_weak_passwords( $password ); if ( $weak_password_status['compromised'] || $weak_password_status['common'] ) { // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 0bf40664981ad..7dd52c4409ba3 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -14,30 +14,50 @@ * Class Validation_Service */ class Validation_Service { - /** - * Save the new password to the user's recent passwords list. - * - * @param int $user_id The user ID. - * @param string $password The password to store. - */ - public function save_recent_password( int $user_id, string $password ) { - // TODO: Use a timestamp for last used password and remove old ones - beyond 1 year? - $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); + public function validate_profile_update( $errors, $update, $user ) { + if ( ! $this->verify_password_update_nonce( 'update-user_' . $user->ID ) ) { + $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + return; + } - if ( ! is_array( $recent_passwords ) ) { - $recent_passwords = array(); + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + // $password = sanitize_text_field($_POST['pass1']); + + $errors->add( 'password_error', __( 'Your new password does not meet the required criteria.', 'jetpack-account-protection' ) ); } - $hashed_password = wp_hash_password( $password ); - if ( in_array( $hashed_password, $recent_passwords, true ) ) { - return; + // TODO: Check current password, run validation, update list + error_log( 'validate_profile_update' ); + + return $errors; + } + + public function validation_user_register( $errors, $sanitized_user_login, $user_email ) { + if ( ! $this->verify_password_update_nonce( 'user-registration' ) ) { + $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + return $errors; } - // Add the new hashed password and keep only the last 5 - array_unshift( $recent_passwords, $hashed_password ); - $recent_passwords = array_slice( $recent_passwords, 0, 5 ); + // TODO: Check current password, run validation, update list + error_log( 'validation_user_register' ); + } - update_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', $recent_passwords ); + public function validate_after_password_reset( $errors, $user ) { + if ( ! $this->verify_password_update_nonce( 'resetpassword_' . $user->ID ) ) { + $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + return $errors; + } + + // TODO: Check current password, run validation, update list + error_log( 'validate_after_password_reset' ); + } + + private function verify_password_update_nonce( $key ) { + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $key ) ) { + return false; + } + + return true; } /** @@ -217,4 +237,29 @@ private function is_recent_password( string $password, int $user_id ): bool { return false; } + + /** + * Save the new password to the user's recent passwords list. + * + * @param int $user_id The user ID. + * @param string $password The password to store. + */ + public function save_recent_password( int $user_id, string $password ) { + $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); + + if ( ! is_array( $recent_passwords ) ) { + $recent_passwords = array(); + } + + $hashed_password = wp_hash_password( $password ); + if ( in_array( $hashed_password, $recent_passwords, true ) ) { + return; + } + + // Add the new hashed password and keep only the last 10 + array_unshift( $recent_passwords, $hashed_password ); + $recent_passwords = array_slice( $recent_passwords, 0, 10 ); + + update_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', $recent_passwords ); + } } From 730407a5ddcd24ae8da03d595b3f2a3a9094f5e1 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 30 Jan 2025 09:07:50 -0800 Subject: [PATCH 092/120] Send auth code via wpcom only --- .../src/class-email-service.php | 62 ++----------------- .../src/class-password-detection.php | 4 +- 2 files changed, 6 insertions(+), 60 deletions(-) diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index 6c3df30ef8483..8f99596746788 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -15,61 +15,6 @@ * Class Email_Service */ class Email_Service { - /** - * Send auth email. - * - * @param \WP_User $user The user. - * @param string $auth_code The authentication code. - * - * @return bool True if the email was sent successfully, false otherwise. - */ - public function send_auth_email( \WP_User $user, string $auth_code ): bool { - $wp_send = $this->wp_send_auth_email( $user, $auth_code ); - - if ( ! $wp_send ) { - return $this->api_send_auth_email( $user, $auth_code ); - } - - return true; - } - - /** - * Send the email using wp_mail(). - * - * @param \WP_User $user The user. - * @param string $auth_code The authentication code. - * - * @return bool True if the email was sent successfully, false otherwise. - */ - private function wp_send_auth_email( \WP_User $user, string $auth_code ): bool { - $blog_name = esc_html( get_bloginfo( 'name' ) ); - $blog_url = esc_url( get_bloginfo( 'url' ) ); - - $subject = esc_html__( 'Verify your identity at Jetpack', 'jetpack-account-protection' ); - $message = sprintf( - /* translators: 1: User login, 2: User URL, 3: Blog name, 4: Authentication code, 5: User URL, 6: Blog name */ - __( - '

Hi %1$s,

-

Your current password for %3$s was found in a public leak, which means your account might be at risk.

-

To help protect your account, please enter this code at the login prompt:

-

%4$s

-

If you didn\'t just log into %5$s, please do so now and change your password.

-

Stay secure,
Jetpack

', - 'jetpack-account-protection' - ), - esc_html( $user->user_login ), - $blog_url, - $blog_name, - esc_html( $auth_code ), - $blog_url, - $blog_name - ); - - $headers = array( 'Content-Type: text/html; charset=UTF-8' ); - - return wp_mail( $user->user_email, $subject, $message, $headers ); - } - /** * Send the email using the API. * @@ -78,7 +23,7 @@ private function wp_send_auth_email( \WP_User $user, string $auth_code ): bool { * * @return bool True if the email was sent successfully, false otherwise. */ - private function api_send_auth_email( \WP_User $user, string $auth_code ): bool { + public function api_send_auth_email( \WP_User $user, string $auth_code ): bool { $blog_id = Jetpack_Options::get_option( 'id' ); $is_connected = ( new Connection_Manager() )->is_connected(); @@ -108,7 +53,8 @@ private function api_send_auth_email( \WP_User $user, string $auth_code ): bool } $body = json_decode( wp_remote_retrieve_body( $response ), true ); - return $body['success'] ?? false; + + return $body['email_sent'] ?? false; } /** @@ -128,7 +74,7 @@ public function resend_auth_email( \WP_User $user, array $transient_data, string $auth_code = $this->generate_auth_code(); $transient_data['auth_code'] = $auth_code; - if ( ! $this->send_auth_email( $user, $auth_code ) ) { + if ( ! $this->api_send_auth_email( $user, $auth_code ) ) { return false; } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 17a5ce1592d18..6f7a0958a627c 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -53,7 +53,7 @@ public function login_form_password_detection( $user, string $password ) { // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); - $email_sent = $this->email_service->send_auth_email( $user, $transient['auth_code'] ); + $email_sent = $this->email_service->api_send_auth_email( $user, $transient['auth_code'] ); if ( ! $email_sent ) { $this->set_transient_error( $user->ID, __( 'Failed to send authentication email. Please try again.', 'jetpack-account-protection' ) ); } @@ -248,7 +248,7 @@ private function generate_and_store_transient_data( int $user_id ): array { $data = array( 'user_id' => $user_id, 'auth_code' => $auth_code, - 'resend_attempts' => 1, + 'resend_attempts' => 0, ); $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); From 39cd9958ae9f91500525b05501f088c94195a669 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Thu, 30 Jan 2025 09:17:37 -0800 Subject: [PATCH 093/120] Update method name --- .../account-protection/src/class-password-detection.php | 2 +- .../account-protection/src/class-validation-service.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 6f7a0958a627c..53e035098582e 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -49,7 +49,7 @@ public function login_form_password_detection( $user, string $password ) { return $user; } - if ( $this->validation_service->check_weak_passwords( $password ) ) { + if ( $this->validation_service->is_weak_password( $password ) ) { // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 9f1e4d8fef9ed..bb8d5ef5e1692 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -21,7 +21,7 @@ class Validation_Service { * * @return bool True if the password is in the list of compromised/common passwords, false otherwise. */ - public function check_weak_passwords( string $password ): bool { + public function is_weak_password( string $password ): bool { $api_url = '/jetpack-protect-weak-password'; $is_connected = ( new Connection_Manager() )->is_connected(); From 56ee7aae79da7c6dc21cda35f2ad77ad50d126ba Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 31 Jan 2025 09:56:52 -0800 Subject: [PATCH 094/120] Optimize validation --- .../src/class-account-protection.php | 58 ++++- .../account-protection/src/class-config.php | 12 +- .../src/class-email-service.php | 4 +- .../src/class-password-detection.php | 18 +- .../src/class-password-manager.php | 210 ++++++++++++++++++ .../src/class-validation-service.php | 149 ++++++------- 6 files changed, 338 insertions(+), 113 deletions(-) create mode 100644 projects/packages/account-protection/src/class-password-manager.php diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index f83409faa404d..dc8f232bec107 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -16,6 +16,13 @@ class Account_Protection { const PACKAGE_VERSION = '0.1.0-alpha'; const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection'; + /** + * Flag to track if hooks have been registered. + * + * @var bool + */ + private static $hooks_registered = false; + /** * Modules instance. * @@ -31,23 +38,23 @@ class Account_Protection { private $password_detection; /** - * Validation service instance + * Password manager instance * - * @var Validation_Service + * @var Password_Manager */ - private $validation_service; + private $password_manager; /** * Account_Protection constructor. * * @param ?Modules $modules Modules instance. * @param ?Password_Detection $password_detection Password detection instance. - * @param ?Validation_Service $validation_service Validation service instance. + * @param ?Password_Manager $password_manager Validation service instance. */ - public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Validation_Service $validation_service = null ) { + public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Password_Manager $password_manager = null ) { $this->modules = $modules ?? new Modules(); $this->password_detection = $password_detection ?? new Password_Detection(); - $this->validation_service = $validation_service ?? new Validation_Service(); + $this->password_manager = $password_manager ?? new Password_Manager(); } /** @@ -56,11 +63,17 @@ public function __construct( ?Modules $modules = null, ?Password_Detection $pass * @return void */ public function init(): void { + if ( self::$hooks_registered ) { + return; + } + $this->register_hooks(); if ( $this->is_enabled() ) { $this->register_runtime_hooks(); } + + self::$hooks_registered = true; } /** @@ -92,16 +105,37 @@ private function register_runtime_hooks(): void { // Add password detection flow add_action( 'login_form_password-detection', array( $this->password_detection, 'render_page' ), 10, 2 ); + add_action( 'wp_enqueue_scripts', array( $this->password_detection, 'enqueue_styles' ) ); // Add password validation - add_action( 'registration_errors', array( $this->validation_service, 'validate_user_register' ), 10, 2 ); - add_action( 'user_profile_update_errors', array( $this->validation_service, 'validate_profile_update' ), 10, 2 ); - add_action( 'validate_password_reset', array( $this->validation_service, 'validate_after_password_reset' ), 10, 2 ); + add_action( + 'user_new_form', + function () { + wp_nonce_field( 'add-new-user', '_new_user_nonce' ); + } + ); + add_action( 'user_profile_update_errors', array( $this->password_manager, 'validate_profile_update' ), 10, 3 ); + add_action( 'validate_password_reset', array( $this->password_manager, 'validate_password_reset' ), 10, 2 ); // Update recent passwords list - // user_register - // profile_update - // after_password_reset + add_action( 'profile_update', array( $this->password_manager, 'on_profile_update' ), 10, 3 ); + add_action( 'after_password_reset', array( $this->password_manager, 'on_password_reset' ), 10, 2 ); + + // TESTING + add_filter( + 'retrieve_password_message', + function ( $message, $key, $user_login, $user_data ) { + + $reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $user_login ) . "&key=$key&action=rp", 'login' ); + + // Log or store the reset link for debugging + error_log( 'Generated Reset Link: ' . $reset_link ); + + return $message; // Keep the original email message intact + }, + 10, + 4 + ); } /** diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index 99d461441752a..13f6526033ccb 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -11,9 +11,11 @@ * Class Config */ class Config { - public const TRANSIENT_PREFIX = 'password_detection'; - public const ERROR_CODE = 'password_detection_validation_error'; - public const ERROR_MESSAGE = 'Password validation failed.'; - public const EMAIL_SENT_EXPIRATION = 600; // 10 minutes - public const MAX_RESEND_ATTEMPTS = 3; + public const PASSWORD_DETECTION_TRANSIENT_PREFIX = 'password_detection'; + public const PASSWORD_DETECTION_ERROR_CODE = 'password_detection_validation_error'; + public const PASSWORD_DETECTION_ERROR_MESSAGE = 'Password validation failed.'; + public const PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION = 600; // 10 minutes + public const PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS = 3; + + public const VALIDATION_SERVICE_USER_META_KEY = 'password_detection_validation_service'; } diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index 8f99596746788..1760919cc7292 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -67,7 +67,7 @@ public function api_send_auth_email( \WP_User $user, string $auth_code ): bool { * @return bool True if the email was resent successfully, false otherwise. */ public function resend_auth_email( \WP_User $user, array $transient_data, string $token ): bool { - if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { + if ( $transient_data['resend_attempts'] >= Config::PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS ) { return false; } @@ -80,7 +80,7 @@ public function resend_auth_email( \WP_User $user, array $transient_data, string ++$transient_data['resend_attempts']; - if ( ! set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::EMAIL_SENT_EXPIRATION ) ) { + if ( ! set_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_{$token}", $transient_data, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION ) ) { return false; } diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index 53e035098582e..1c6e0ce28a30d 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -59,8 +59,8 @@ public function login_form_password_detection( $user, string $password ) { } return new \WP_Error( - Config::ERROR_CODE, - Config::ERROR_MESSAGE, + Config::PASSWORD_DETECTION_ERROR_CODE, + Config::PASSWORD_DETECTION_ERROR_MESSAGE, array( 'token' => $transient['token'] ) ); } @@ -96,7 +96,7 @@ public function render_page() { } $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : null; - $transient_data = get_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); + $transient_data = get_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_{$token}" ); if ( ! $transient_data ) { $this->redirect_to_login(); } @@ -107,8 +107,6 @@ public function render_page() { $this->redirect_to_login(); } - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); - // Handle resend email request if ( isset( $_GET['resend_email'] ) && $_GET['resend_email'] === '1' ) { if ( isset( $_GET['_wpnonce'] ) @@ -118,7 +116,7 @@ public function render_page() { if ( ! $email_resent ) { $message = __( 'Failed to resend authentication email. Please try again.', 'jetpack-account-protection' ); - if ( $transient_data['resend_attempts'] >= Config::MAX_RESEND_ATTEMPTS ) { + if ( $transient_data['resend_attempts'] >= Config::PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS ) { $message = __( 'Resend limit exceeded. Please try again later.', 'jetpack-account-protection' ); } @@ -156,7 +154,7 @@ public function render_page() { * @return void */ public function render_content( \WP_User $user, string $token ): void { - $transient_key = Config::TRANSIENT_PREFIX . "_error_{$user->ID}"; + $transient_key = Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_error_{$user->ID}"; $error_message = get_transient( $transient_key ); delete_transient( $transient_key ); @@ -251,7 +249,7 @@ private function generate_and_store_transient_data( int $user_id ): array { 'resend_attempts' => 0, ); - $transient_set = set_transient( Config::TRANSIENT_PREFIX . "_{$token}", $data, Config::EMAIL_SENT_EXPIRATION ); + $transient_set = set_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_{$token}", $data, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION ); if ( ! $transient_set ) { $this->set_transient_error( $user_id, __( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ) ); } @@ -296,7 +294,7 @@ private function get_redirect_url( string $token ): string { private function handle_auth_form_submission( \WP_User $user, string $token, string $auth_code, string $user_input ): void { if ( $auth_code && $auth_code === $user_input ) { // TODO: Ensure all transient are also removed on module and/or plugin deactivation - delete_transient( Config::TRANSIENT_PREFIX . "_{$token}" ); + delete_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_{$token}" ); wp_set_auth_cookie( $user->ID, true ); // TODO: Notify user to update their password/redirect to password update page wp_safe_redirect( admin_url() ); @@ -316,7 +314,7 @@ private function handle_auth_form_submission( \WP_User $user, string $token, str * @return void */ private function set_transient_error( int $user_id, string $message, int $expiration = 60 ): void { - set_transient( Config::TRANSIENT_PREFIX . "_error_{$user_id}", esc_html( $message ), $expiration ); + set_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_error_{$user_id}", esc_html( $message ), $expiration ); } /** diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php new file mode 100644 index 0000000000000..928c2a846b886 --- /dev/null +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -0,0 +1,210 @@ +validation_service = $validation_service ?? new Validation_Service(); + } + + /** + * Verify the nonce for password update. + * + * @param string $key The nonce key. + * + * @return bool True if the nonce is valid, false otherwise. + */ + private function verify_password_update_nonce( $key ) { + if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $key ) ) { + return false; + } + + return true; + } + + /** + * Verify the nonce for profile update. + * + * @param int $user_id The user ID. + * + * @return bool True if the nonce is valid, false otherwise. + */ + private function verify_profile_update_nonce( $user_id ) { + return $this->verify_password_update_nonce( 'update-user_' . $user_id ); + } + + // /** + // * Verify the nonce for password reset. + // * + // * @param int $user_id The user ID. + // * + // * @return bool True if the nonce is valid, false otherwise. + // */ + // private function verify_password_reset_nonce( $user_id ) { + // return $this->verify_password_update_nonce( 'resetpassword_' . $user_id ); + // } + + /** + * Validate the profile update. + * + * @param \WP_Error $errors The error object. + * @param bool $update Whether the user is being updated. + * @param \stdClass $user A copy of the new user object. + * + * @return void + */ + public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { + if ( ! $update ) { + // This is a new user (wp-admin/user-new.php) + if ( ! isset( $_POST['_new_user_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_new_user_nonce'] ) ), 'add-new-user' ) ) { + $errors->add( 'nonce_error', __( 'Error: Nonce verification failed for new user creation.', 'jetpack-account-protection' ) ); + return; + } + // This is an existing user update (wp-admin/profile.php or user-edit.php) + } elseif ( ! $this->verify_profile_update_nonce( $user->ID ) ) { + $errors->add( 'nonce_error', __( 'Error: Nonce verification failed for profile update.', 'jetpack-account-protection' ) ); + return; + } + + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); + + if ( $update ) { + $old_user_data = get_userdata( $user->ID ); + if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { + $errors->add( 'password_error', __( 'Error: Password was used recently.', 'jetpack-account-protection' ) ); + return; + } + } + + $error = $this->validation_service->return_first_validation_error( $user, $password, 'profile' ); + if ( ! empty( $error ) ) { + $errors->add( 'password_error', $error ); + return; + } + } + + // TODO: Run this even if JS validation passes? + } + + /** + * Validate the password reset. + * + * @param \WP_Error $errors The error object. + * @param \WP_User|\WP_Error $user The user object. + * + * @return void + */ + public function validate_password_reset( \WP_Error $errors, $user ): void { + // TODO: Does not appear possible to verify the nonce or reset key here, unclear how to approach this safely + // Maybe its fine because we are only handling existing data that has already been screened and erroring or allowing a pass? + // If necessary, we could use the same logic to verify as wp-login.php case 'resetpass'/case 'rp' + // if ( ! $this->verify_password_reset_nonce( $user->ID ) ) { + // $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); + // return; + // } + + if ( is_wp_error( $user ) ) { + return; + } + + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); + if ( $this->validation_service->is_current_password( $user, $password ) ) { + $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); + return; + } + + $error = $this->validation_service->return_first_validation_error( $user, $password, 'reset' ); + if ( ! empty( $error ) ) { + $errors->add( 'password_error', $error ); + return; + } + } + + // TODO: Run this even if JS validation passes? + } + + /** + * Handle the profile update. + * + * @param int $user_id The user ID. + * @param \WP_User $old_user_data The old user data. + * @param array $userdata The user data. + * + * @return void + */ + public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { + if ( ! $this->verify_profile_update_nonce( $user_id ) ) { + error_log( "Nonce verification failed for profile update: User ID {$user_id}" ); + return; + } + + $this->save_recent_password( $user_id, $old_user_data->user_pass ); + + // TODO: Do something if save fails? + } + + /** + * Handle the password reset. + * + * @param \WP_User $user The user. + * @param string $new_password The new password. + */ + public function on_password_reset( $user, $new_password ) { + // TODO: Does not appear possible to verify the nonce or reset key here, unclear how to approach this safely + // if ( ! $this->verify_password_reset_nonce( $user->ID ) ) { + // error_log( "Nonce verification failed for password reset: User ID {$user->ID}" ); + // return; + // } + + error_log( 'on_password_reset' ); + + $this->save_recent_password( $user->ID, $user->user_pass ); + // TODO: Do something if save fails? + } + + /** + * Save the new password hash to the user's recent passwords list. + * + * @param int $user_id The user ID. + * @param string $password_hash The password hash to store. + */ + public function save_recent_password( int $user_id, string $password_hash ) { + $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); + + if ( ! is_array( $recent_passwords ) ) { + $recent_passwords = array(); + } + + if ( in_array( $password_hash, $recent_passwords, true ) ) { + return; + } + + // Add the new hashed password and keep only the last 10 + array_unshift( $recent_passwords, $password_hash ); + $recent_passwords = array_slice( $recent_passwords, 0, 10 ); + + update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, $recent_passwords ); + } +} diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 6f418d3b35605..c31974e9a466c 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -14,84 +14,79 @@ * Class Validation_Service */ class Validation_Service { - public function validate_profile_update( $errors, $update, $user ) { - if ( ! $this->verify_password_update_nonce( 'update-user_' . $user->ID ) ) { - $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); - return; - } - - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - // $password = sanitize_text_field($_POST['pass1']); + /** + * Validate password against all security conditions. + * + * @param \WP_User|\stdClass $user The user object or a copy. + * @param string $password The password to check. + * + * @return array An array of validation errors (if any). + */ + public function return_all_validation_errors( $user, string $password ): array { + $errors = array(); - $errors->add( 'password_error', __( 'Your new password does not meet the required criteria.', 'jetpack-account-protection' ) ); + if ( $this->contains_backslash( $password ) ) { + $errors[] = __( 'Doesn\'t contain a backslash (\\) character', 'jetpack-account-protection' ); } - // TODO: Check current password, run validation, update list - error_log( 'validate_profile_update' ); - - return $errors; - } - - public function validation_user_register( $errors, $sanitized_user_login, $user_email ) { - if ( ! $this->verify_password_update_nonce( 'user-registration' ) ) { - $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); - return $errors; + if ( ! $this->check_length( $password ) ) { + $errors[] = __( 'Between 6 and 150 characters', 'jetpack-account-protection' ); } - // TODO: Check current password, run validation, update list - error_log( 'validation_user_register' ); - } - - public function validate_after_password_reset( $errors, $user ) { - if ( ! $this->verify_password_update_nonce( 'resetpassword_' . $user->ID ) ) { - $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); - return $errors; + if ( $this->matches_user_data( $user, $password ) ) { + $errors[] = __( 'Doesn\'t match user data', 'jetpack-account-protection' ); } - // TODO: Check current password, run validation, update list - error_log( 'validate_after_password_reset' ); - } + if ( $this->is_recent_password( $user->ID, $password ) ) { + $errors[] = __( 'Not used recently', 'jetpack-account-protection' ); + } - private function verify_password_update_nonce( $key ) { - if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], $key ) ) { - return false; + if ( $this->is_weak_password( $password ) ) { + $errors[] = __( 'Not a leaked password.', 'jetpack-account-protection' ); } - return true; + return $errors; } /** - * Validate password against security conditions. + * Validate password against first security conditions. * - * @param string $password The password to check. - * @param int $user_id The user ID. + * @param \WP_User|\stdClass $user The user object or a copy. + * @param string $password The password to check. + * @param 'profile'|'reset' $context The context the validation is run in. * - * @return array An array of validation errors (if any). + * @return string The first validation errors (if any). */ - public function validate_password( string $password, int $user_id ): array { - $errors = array(); + public function return_first_validation_error( $user, string $password, $context ): string { + if ( 'profile' === $context ) { + if ( empty( $password ) ) { + return __( 'Error: The password cannot be a space or all spaces.', 'jetpack-account-protection' ); + } + } - if ( $this->contains_backslash( $password ) ) { - $errors[] = __( 'Doesn\'t contain a backslash (\\) character', 'jetpack-account-protection' ); + if ( 'reset' === $context ) { + if ( $this->contains_backslash( $password ) ) { + return __( 'Error: The password cannot contain a backslash (\\) character.', 'jetpack-account-protection' ); + } } if ( ! $this->check_length( $password ) ) { - $errors[] = __( 'Between 6 and 150 characters', 'jetpack-account-protection' ); + return __( 'Error: The password must be between 6 and 150 characters.', 'jetpack-account-protection' ); } - if ( $this->is_weak_password( $password ) ) { - $errors[] = __( 'Not a leaked password.', 'jetpack-account-protection' ); + if ( $this->matches_user_data( $user, $password ) ) { + return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); } - if ( $this->matches_user_data( $password, $user_id ) ) { - $errors[] = __( 'Doesn\'t match user data', 'jetpack-account-protection' ); + if ( $this->is_recent_password( $user->ID, $password ) ) { + return __( 'Error: The password was used recently.', 'jetpack-account-protection' ); } - if ( $this->is_recent_password( $password, $user_id ) ) { - $errors[] = __( 'Not used recently', 'jetpack-account-protection' ); + if ( $this->is_weak_password( $password ) ) { + return __( 'Error: The password was found in a public leak.', 'jetpack-account-protection' ); } - return $errors; + return ''; } /** @@ -120,14 +115,12 @@ private function check_length( string $password ): bool { /** * Check if the password matches any user data. * - * @param string $password The password to check. - * @param int $user_id The user ID. + * @param \WP_User|\stdClass $user The user. + * @param string $password The password to check. * * @return bool True if the password matches any user data, false otherwise. */ - private function matches_user_data( string $password, int $user_id ): bool { - $user = get_userdata( $user_id ); - + private function matches_user_data( $user, string $password ): bool { if ( ! $user ) { return false; } @@ -140,6 +133,7 @@ private function matches_user_data( string $password, int $user_id ): bool { $user->last_name, $user->user_email, explode( '@', $user->user_email )[0], // Email username + explode( '@', $user->user_email )[1], // Email domain $user->nickname, ); @@ -198,16 +192,28 @@ public function is_weak_password( string $password ): bool { return false; } + /** + * Check if the password is the current password for the user. + * + * @param \WP_User $user The user. + * @param string $password The password to check. + * + * @return bool True if the password is the current password, false otherwise. + */ + public function is_current_password( \WP_User $user, string $password ): bool { + return wp_check_password( $password, $user->user_pass, $user->ID ); + } + /** * Check if the password has been used recently by the user. * + * @param int $user_id The user ID. * @param string $password The password to check. - * @param int $user_id The user ID. * - * @return bool True if the password was recently used, false otherwise. + * @return bool True if the password hash was recently used, false otherwise. */ - private function is_recent_password( string $password, int $user_id ): bool { - $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); + public function is_recent_password( int $user_id, string $password ): bool { + $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); if ( empty( $recent_passwords ) || ! is_array( $recent_passwords ) ) { return false; @@ -221,29 +227,4 @@ private function is_recent_password( string $password, int $user_id ): bool { return false; } - - /** - * Save the new password to the user's recent passwords list. - * - * @param int $user_id The user ID. - * @param string $password The password to store. - */ - public function save_recent_password( int $user_id, string $password ) { - $recent_passwords = get_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', true ); - - if ( ! is_array( $recent_passwords ) ) { - $recent_passwords = array(); - } - - $hashed_password = wp_hash_password( $password ); - if ( in_array( $hashed_password, $recent_passwords, true ) ) { - return; - } - - // Add the new hashed password and keep only the last 10 - array_unshift( $recent_passwords, $hashed_password ); - $recent_passwords = array_slice( $recent_passwords, 0, 10 ); - - update_user_meta( $user_id, 'jetpack_acccount_protection_recent_passwords', $recent_passwords ); - } } From c5e658ed4586dcdf394f01165b05ea31ace2eafa Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 31 Jan 2025 10:05:49 -0800 Subject: [PATCH 095/120] Fix key, remove testing code --- .../src/class-account-protection.php | 16 ---------------- .../account-protection/src/class-config.php | 2 +- .../src/class-password-manager.php | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index dc8f232bec107..6f8000ac55790 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -120,22 +120,6 @@ function () { // Update recent passwords list add_action( 'profile_update', array( $this->password_manager, 'on_profile_update' ), 10, 3 ); add_action( 'after_password_reset', array( $this->password_manager, 'on_password_reset' ), 10, 2 ); - - // TESTING - add_filter( - 'retrieve_password_message', - function ( $message, $key, $user_login, $user_data ) { - - $reset_link = network_site_url( 'wp-login.php?login=' . rawurlencode( $user_login ) . "&key=$key&action=rp", 'login' ); - - // Log or store the reset link for debugging - error_log( 'Generated Reset Link: ' . $reset_link ); - - return $message; // Keep the original email message intact - }, - 10, - 4 - ); } /** diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index 13f6526033ccb..b4aacb57387ad 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -17,5 +17,5 @@ class Config { public const PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION = 600; // 10 minutes public const PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS = 3; - public const VALIDATION_SERVICE_USER_META_KEY = 'password_detection_validation_service'; + public const VALIDATION_SERVICE_USER_META_KEY = 'jetpack_account_protection_recent_password_hashes'; } diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 928c2a846b886..403ca5a430185 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -92,7 +92,7 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl if ( $update ) { $old_user_data = get_userdata( $user->ID ); if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { - $errors->add( 'password_error', __( 'Error: Password was used recently.', 'jetpack-account-protection' ) ); + $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); return; } } From 9637bd3d4fa4aab55c07fddc6a11603ee48b0104 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 31 Jan 2025 10:08:28 -0800 Subject: [PATCH 096/120] Fix docs --- .../account-protection/src/class-validation-service.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index c31974e9a466c..59be954ad47cc 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -15,7 +15,7 @@ */ class Validation_Service { /** - * Validate password against all security conditions. + * Return all validation errors. * * @param \WP_User|\stdClass $user The user object or a copy. * @param string $password The password to check. @@ -49,7 +49,7 @@ public function return_all_validation_errors( $user, string $password ): array { } /** - * Validate password against first security conditions. + * Return first validation error. * * @param \WP_User|\stdClass $user The user object or a copy. * @param string $password The password to check. From 81a2325205e6a0e4b7252258ee0fb2854b9c7f18 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 31 Jan 2025 15:26:28 -0800 Subject: [PATCH 097/120] Fix tests --- .../tests/php/test-email-service.php | 2 +- .../tests/php/test-password-detection.php | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/projects/packages/account-protection/tests/php/test-email-service.php b/projects/packages/account-protection/tests/php/test-email-service.php index a71d02fafa8c4..667fccae881a9 100644 --- a/projects/packages/account-protection/tests/php/test-email-service.php +++ b/projects/packages/account-protection/tests/php/test-email-service.php @@ -66,7 +66,7 @@ public function test_resend_auth_mail_sends_mail_and_remembers_2fa_token_success $this->assertTrue( $result, 'Resending auth mail should return true as success indicator.' ); // Verify the transient has the expected data - $new_transient = get_transient( Config::TRANSIENT_PREFIX . "_{$my_token}" ); + $new_transient = get_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_{$my_token}" ); $this->assertSame( 1, $new_transient['resend_attempts'], 'Resend attempts should be 1.' ); $this->assertMatchesRegularExpression( '/^[0-9]{6}$/', $new_transient['auth_code'], 'Auth code should be 6 digits.' ); } diff --git a/projects/packages/account-protection/tests/php/test-password-detection.php b/projects/packages/account-protection/tests/php/test-password-detection.php index fc9021d53e4f0..01197dc57c5ae 100644 --- a/projects/packages/account-protection/tests/php/test-password-detection.php +++ b/projects/packages/account-protection/tests/php/test-password-detection.php @@ -10,7 +10,7 @@ class Password_Detection_Test extends BaseTestCase { public function test_handle_password_detection_validation_error_redirects_to_login(): void { - $error = new \WP_Error( Config::ERROR_CODE, Config::ERROR_MESSAGE, array( 'token' => 'my-token' ) ); + $error = new \WP_Error( Config::PASSWORD_DETECTION_ERROR_CODE, Config::PASSWORD_DETECTION_ERROR_MESSAGE, array( 'token' => 'my-token' ) ); $sut = $this->createPartialMock( Password_Detection::class, array( 'redirect_and_exit' ) ); $sut->expects( $this->once() ) @@ -96,8 +96,8 @@ public function test_login_form_password_detection_sends_email_and_returns_error $error = $sut->login_form_password_detection( $user, 'pw' ); $this->assertInstanceOf( \WP_Error::class, $error, 'Should return a WP_Error object.' ); - $this->assertSame( Config::ERROR_MESSAGE, $error->get_error_message( Config::ERROR_CODE ), 'Should return the correct error message.' ); - $token = $error->get_error_data( Config::ERROR_CODE )['token']; + $this->assertSame( Config::PASSWORD_DETECTION_ERROR_MESSAGE, $error->get_error_message( Config::PASSWORD_DETECTION_ERROR_CODE ), 'Should return the correct error message.' ); + $token = $error->get_error_data( Config::PASSWORD_DETECTION_ERROR_CODE )['token']; $this->assertSame( 32, strlen( $token ), 'Token should be 32 characters long.' ); remove_filter( 'check_password', '__return_true' ); @@ -129,7 +129,7 @@ public function test_login_form_password_detection_sets_transient_error_if_unabl $sut->login_form_password_detection( $user, 'pw' ); - $transient_data = get_transient( Config::TRANSIENT_PREFIX . "_error_{$user->ID}" ); + $transient_data = get_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . "_error_{$user->ID}" ); $this->assertSame( 'Failed to send authentication email. Please try again.', $transient_data, 'Should have set the correct error message.' ); remove_filter( 'check_password', '__return_true' ); @@ -166,7 +166,7 @@ public function test_render_page_redirects_to_login_if_transient_data_is_not_ava public function test_render_page_redirects_to_login_if_user_with_id_from_transient_does_not_exist(): void { $_GET['token'] = 'my_cool_token'; - set_transient( Config::TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123 ) ); + set_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123 ) ); $sut = $this->createPartialMock( Password_Detection::class, array( 'redirect_and_exit', 'load_user' ) ); $sut->expects( $this->once() ) @@ -189,7 +189,7 @@ public function test_render_page_checks_2fa_code_successfully(): void { $_POST['_wpnonce_verify'] = wp_create_nonce( 'verify_action' ); set_transient( - Config::TRANSIENT_PREFIX . '_my_cool_token', + Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123, 'auth_code' => '123456', @@ -237,7 +237,7 @@ public function test_render_page_sets_transient_error_if_2fa_code_is_wrong(): vo $_POST['_wpnonce_verify'] = wp_create_nonce( 'verify_action' ); set_transient( - Config::TRANSIENT_PREFIX . '_my_cool_token', + Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123, 'auth_code' => '123456', @@ -257,7 +257,7 @@ public function test_render_page_sets_transient_error_if_2fa_code_is_wrong(): vo $sut->render_page(); - $error = get_transient( Config::TRANSIENT_PREFIX . '_error_123' ); + $error = get_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_error_123' ); $this->assertSame( 'Authentication code verification failed. Please try again.', $error, 'Error message is not as expected.' ); @@ -273,7 +273,7 @@ public function test_render_page_sets_transient_error_if_2fa_nonce_is_wrong(): v $_POST['_wpnonce_verify'] = 'wrong nonce'; // intentionally wrong set_transient( - Config::TRANSIENT_PREFIX . '_my_cool_token', + Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123, 'auth_code' => '123456', @@ -296,7 +296,7 @@ public function test_render_page_sets_transient_error_if_2fa_nonce_is_wrong(): v $sut->render_page(); - $error = get_transient( Config::TRANSIENT_PREFIX . '_error_123' ); + $error = get_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_error_123' ); $this->assertSame( 'Verify nonce verification failed. Please try again.', $error, 'Error message is not as expected.' ); @@ -311,7 +311,7 @@ public function test_render_page_resends_mail_successfully(): void { $_GET['_wpnonce'] = wp_create_nonce( 'resend_email_nonce' ); set_transient( - Config::TRANSIENT_PREFIX . '_my_cool_token', + Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_my_cool_token', array( 'user_id' => 123, 'auth_code' => '123456', @@ -379,7 +379,7 @@ public function test_render_content_explains_the_2fa_form(): void { public function test_render_content_shows_transient_error_if_set(): void { $error_message = 'This is a error message to test things with.'; - set_transient( Config::TRANSIENT_PREFIX . '_error_123', $error_message ); + set_transient( Config::PASSWORD_DETECTION_TRANSIENT_PREFIX . '_error_123', $error_message ); $user = new \WP_User(); $user->ID = 123; From 5e96389ba2f8a64ea767e86bf8e1ad083d36df9e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 10:23:50 -0800 Subject: [PATCH 098/120] Improve matches user data logic --- .../src/class-validation-service.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 69f74598e7d42..a8aa54c0b4bb7 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -160,6 +160,11 @@ private function matches_user_data( $user, string $password ): bool { return false; } + $email_parts = explode( '@', $user->user_email ); // test@example.com + $email_username = $email_parts[0]; // 'test' + $email_domain = $email_parts[1]; // 'example.com' + $email_provider = explode( '.', $email_domain )[0]; // 'example' + $user_data = array( $user->user_login, $user->user_nicename, @@ -167,14 +172,19 @@ private function matches_user_data( $user, string $password ): bool { $user->first_name, $user->last_name, $user->user_email, - explode( '@', $user->user_email )[0], // Email username - explode( '@', $user->user_email )[1], // Email domain + $email_username, + $email_provider, $user->nickname, ); $password_lower = strtolower( $password ); foreach ( $user_data as $data ) { + // Skip if $data is 3 characters or less. + if ( strlen( $data ) <= 3 ) { + continue; + } + if ( ! empty( $data ) && strpos( $password_lower, strtolower( $data ) ) !== false ) { return true; } From c5c8acd72442893fe94483ed786c23642bd21fdd Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 11:05:14 -0800 Subject: [PATCH 099/120] Remove password reset nonce verification code --- .../src/class-password-manager.php | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 403ca5a430185..a155ccb5ccb5c 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -53,17 +53,6 @@ private function verify_profile_update_nonce( $user_id ) { return $this->verify_password_update_nonce( 'update-user_' . $user_id ); } - // /** - // * Verify the nonce for password reset. - // * - // * @param int $user_id The user ID. - // * - // * @return bool True if the nonce is valid, false otherwise. - // */ - // private function verify_password_reset_nonce( $user_id ) { - // return $this->verify_password_update_nonce( 'resetpassword_' . $user_id ); - // } - /** * Validate the profile update. * @@ -116,18 +105,12 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl * @return void */ public function validate_password_reset( \WP_Error $errors, $user ): void { - // TODO: Does not appear possible to verify the nonce or reset key here, unclear how to approach this safely - // Maybe its fine because we are only handling existing data that has already been screened and erroring or allowing a pass? - // If necessary, we could use the same logic to verify as wp-login.php case 'resetpass'/case 'rp' - // if ( ! $this->verify_password_reset_nonce( $user->ID ) ) { - // $errors->add( 'nonce_error', __( 'Nonce verification failed. Please try again.', 'jetpack-account-protection' ) ); - // return; - // } - if ( is_wp_error( $user ) ) { return; } + // No nonce verification necessary as the actions hook in after a robust verification process + // phpcs:disable WordPress.Security.NonceVerification if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); if ( $this->validation_service->is_current_password( $user, $password ) ) { @@ -155,6 +138,7 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { * @return void */ public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { + // TODO: Need to verify this is working... seems to happen on reset link send! if ( ! $this->verify_profile_update_nonce( $user_id ) ) { error_log( "Nonce verification failed for profile update: User ID {$user_id}" ); return; @@ -172,12 +156,7 @@ public function on_profile_update( int $user_id, \WP_User $old_user_data, array * @param string $new_password The new password. */ public function on_password_reset( $user, $new_password ) { - // TODO: Does not appear possible to verify the nonce or reset key here, unclear how to approach this safely - // if ( ! $this->verify_password_reset_nonce( $user->ID ) ) { - // error_log( "Nonce verification failed for password reset: User ID {$user->ID}" ); - // return; - // } - + // TODO: Need to verify this is working... error_log( 'on_password_reset' ); $this->save_recent_password( $user->ID, $user->user_pass ); From 8f8f934261fc017023ecaa5faab9e30fd3dfe8bc Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 13:07:34 -0800 Subject: [PATCH 100/120] Updates and fixes --- .../src/class-password-detection.php | 1 - .../src/class-password-manager.php | 20 +++++++------------ .../src/class-validation-service.php | 1 - 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index fa0ee91cd949a..b1774897a0e01 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -50,7 +50,6 @@ public function login_form_password_detection( $user, string $password ) { } if ( $this->validation_service->is_weak_password( $password ) ) { - // TODO: Every time the user logs in we generate a new token based transient. This might not be ideal. $transient = $this->generate_and_store_transient_data( $user->ID ); $email_sent = $this->email_service->api_send_auth_email( $user, $transient['auth_code'] ); diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index a155ccb5ccb5c..315f653931bef 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -109,7 +109,7 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { return; } - // No nonce verification necessary as the actions hook in after a robust verification process + // No nonce verification necessary as the action hooks in after a robust verification process // phpcs:disable WordPress.Security.NonceVerification if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); @@ -138,15 +138,13 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { * @return void */ public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { - // TODO: Need to verify this is working... seems to happen on reset link send! - if ( ! $this->verify_profile_update_nonce( $user_id ) ) { - error_log( "Nonce verification failed for profile update: User ID {$user_id}" ); - return; + if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) { + if ( $this->verify_profile_update_nonce( $user_id ) ) { + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + $this->save_recent_password( $user_id, $old_user_data->user_pass ); + } + } } - - $this->save_recent_password( $user_id, $old_user_data->user_pass ); - - // TODO: Do something if save fails? } /** @@ -156,11 +154,7 @@ public function on_profile_update( int $user_id, \WP_User $old_user_data, array * @param string $new_password The new password. */ public function on_password_reset( $user, $new_password ) { - // TODO: Need to verify this is working... - error_log( 'on_password_reset' ); - $this->save_recent_password( $user->ID, $user->user_pass ); - // TODO: Do something if save fails? } /** diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index a8aa54c0b4bb7..778f9d568f2f3 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -167,7 +167,6 @@ private function matches_user_data( $user, string $password ): bool { $user_data = array( $user->user_login, - $user->user_nicename, $user->display_name, $user->first_name, $user->last_name, From 8388caace0812d6dd675f9edc6a4247c037acf7e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 14:05:03 -0800 Subject: [PATCH 101/120] Include tests for new validation methods --- .../src/class-validation-service.php | 14 ++-- .../tests/php/test-account-protection.php | 5 ++ .../tests/php/test-validation-service.php | 69 ++++++++++++++++++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 778f9d568f2f3..1204b10761116 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -64,7 +64,7 @@ public function return_all_validation_errors( $user, string $password ): array { $errors[] = __( 'Doesn\'t contain a backslash (\\) character', 'jetpack-account-protection' ); } - if ( ! $this->check_length( $password ) ) { + if ( $this->is_invalid_length( $password ) ) { $errors[] = __( 'Between 6 and 150 characters', 'jetpack-account-protection' ); } @@ -105,7 +105,7 @@ public function return_first_validation_error( $user, string $password, $context } } - if ( ! $this->check_length( $password ) ) { + if ( $this->is_invalid_length( $password ) ) { return __( 'Error: The password must be between 6 and 150 characters.', 'jetpack-account-protection' ); } @@ -131,7 +131,7 @@ public function return_first_validation_error( $user, string $password, $context * * @return bool True if the password contains a backslash, false otherwise. */ - private function contains_backslash( string $password ): bool { + public function contains_backslash( string $password ): bool { return strpos( $password, '\\' ) !== false; } @@ -142,9 +142,9 @@ private function contains_backslash( string $password ): bool { * * @return bool True if the password is between 6 and 150 characters, false otherwise. */ - private function check_length( string $password ): bool { + public function is_invalid_length( string $password ): bool { $length = strlen( $password ); - return $length >= 6 && $length <= 150; + return $length < 6 || $length > 150; } /** @@ -155,7 +155,7 @@ private function check_length( string $password ): bool { * * @return bool True if the password matches any user data, false otherwise. */ - private function matches_user_data( $user, string $password ): bool { + public function matches_user_data( $user, string $password ): bool { if ( ! $user ) { return false; } @@ -221,9 +221,11 @@ public function is_weak_password( string $password ): bool { if ( in_array( $password_suffix, $body['compromised'] ?? array(), true ) ) { return true; } + if ( in_array( $password_suffix, $body['common'] ?? array(), true ) ) { return true; } + return false; } diff --git a/projects/packages/account-protection/tests/php/test-account-protection.php b/projects/packages/account-protection/tests/php/test-account-protection.php index 0bbb8c831d5ce..fbb481b26023b 100644 --- a/projects/packages/account-protection/tests/php/test-account-protection.php +++ b/projects/packages/account-protection/tests/php/test-account-protection.php @@ -37,6 +37,11 @@ public function test_init_registers_hooks_and_runtime_hooks_if_module_enabled(): } public function test_init_registers_hooks_but_not_runtime_hooks_if_module_disabled(): void { + $reflection = new \ReflectionClass( Account_Protection::class ); + $property = $reflection->getProperty( 'hooks_registered' ); + $property->setAccessible( true ); + $property->setValue( false ); + $sut = $this->createPartialMock( Account_Protection::class, array( 'is_enabled', 'register_hooks', 'register_runtime_hooks' ) ); $sut->expects( $this->once() ) ->method( 'is_enabled' ) diff --git a/projects/packages/account-protection/tests/php/test-validation-service.php b/projects/packages/account-protection/tests/php/test-validation-service.php index 24ffbb99b0166..e757b9f99d705 100644 --- a/projects/packages/account-protection/tests/php/test-validation-service.php +++ b/projects/packages/account-protection/tests/php/test-validation-service.php @@ -22,11 +22,17 @@ public function test_returns_false_if_not_connected() { $this->assertFalse( $validation_service->is_weak_password( 'somepassword' ) ); } - private function get_connected_connection_manager() { + private function get_connection_manager() { $connection = $this->getMockBuilder( 'Automattic\Jetpack\Connection\Manager' ) ->disableOriginalConstructor() ->getMock(); + return $connection; + } + + private function get_connected_connection_manager() { + $connection = $this->get_connection_manager(); + $connection->expects( $this->once() ) ->method( 'is_connected' ) ->willReturn( true ); @@ -160,4 +166,65 @@ public function test_returns_false_if_password_is_not_weak() { $this->assertFalse( $validation_service->is_weak_password( 'somepassword' ) ); } + + public function test_returns_true_if_password_is_current_password() { + $user = new \WP_User(); + $user->user_pass = wp_hash_password( 'somepassword' ); + $user->ID = 1; + + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->is_current_password( $user, 'somepassword' ) ); + } + + public function test_returns_false_if_password_is_not_current_password() { + $user = new \WP_User(); + $user->user_pass = wp_hash_password( 'somepassword' ); + $user->ID = 1; + + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertFalse( $validation_service->is_current_password( $user, 'anotherpassword' ) ); + } + + public function test_returns_true_if_password_was_recently_used() { + $user_id = 1; + $password_hash = wp_hash_password( 'somepassword' ); + + update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, array( $password_hash ) ); + + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->is_recent_password( $user_id, 'somepassword' ) ); + } + + public function test_returns_false_if_password_was_not_recently_used() { + $user_id = 1; + $password_hash = wp_hash_password( 'somepassword' ); + + update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, array( $password_hash ) ); + + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertFalse( $validation_service->is_recent_password( $user_id, 'anotherpassword' ) ); + } + + public function test_returns_true_if_password_matches_user_data() { + $user = new \WP_User(); + $user->user_email = 'example@wordpress.com'; + + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->matches_user_data( $user, 'wordpress' ) ); + } + + public function test_returns_false_if_password_is_too_short() { + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->is_invalid_length( 'short' ) ); + } + + public function test_returns_false_if_password_is_too_long() { + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->is_invalid_length( $string = str_repeat( 'a', 151 ) ) ); + } + + public function test_returns_true_if_password_contains_backslash() { + $validation_service = new Validation_Service( $this->get_connection_manager() ); + $this->assertTrue( $validation_service->contains_backslash( 'password\\' ) ); + } } From 2c59a84aa9a68a1dbc65442ae4d939684059a711 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 14:05:12 -0800 Subject: [PATCH 102/120] Include tests for new validation methods --- .../tests/php/test-validation-service.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/projects/packages/account-protection/tests/php/test-validation-service.php b/projects/packages/account-protection/tests/php/test-validation-service.php index e757b9f99d705..bf22f78739404 100644 --- a/projects/packages/account-protection/tests/php/test-validation-service.php +++ b/projects/packages/account-protection/tests/php/test-validation-service.php @@ -210,17 +210,21 @@ public function test_returns_true_if_password_matches_user_data() { $user->user_email = 'example@wordpress.com'; $validation_service = new Validation_Service( $this->get_connection_manager() ); - $this->assertTrue( $validation_service->matches_user_data( $user, 'wordpress' ) ); + $this->assertTrue( $validation_service->matches_user_data( $user, 'WordPress' ) ); } public function test_returns_false_if_password_is_too_short() { + $short_password = 'short'; + $validation_service = new Validation_Service( $this->get_connection_manager() ); - $this->assertTrue( $validation_service->is_invalid_length( 'short' ) ); + $this->assertTrue( $validation_service->is_invalid_length( $short_password ) ); } public function test_returns_false_if_password_is_too_long() { + $long_password = str_repeat( 'a', 151 ); + $validation_service = new Validation_Service( $this->get_connection_manager() ); - $this->assertTrue( $validation_service->is_invalid_length( $string = str_repeat( 'a', 151 ) ) ); + $this->assertTrue( $validation_service->is_invalid_length( $long_password ) ); } public function test_returns_true_if_password_contains_backslash() { From 7fb0b5a09edf15e7de6ce18f64a7508db2d847c0 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Mon, 3 Feb 2025 19:07:44 -0800 Subject: [PATCH 103/120] Add password manager class tests --- .../src/class-password-manager.php | 25 ++- .../tests/php/test-email-service.php | 2 +- .../tests/php/test-password-manager.php | 159 ++++++++++++++++++ 3 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 projects/packages/account-protection/tests/php/test-password-manager.php diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 315f653931bef..259e872ca4070 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -49,7 +49,7 @@ private function verify_password_update_nonce( $key ) { * * @return bool True if the nonce is valid, false otherwise. */ - private function verify_profile_update_nonce( $user_id ) { + public function verify_profile_update_nonce( $user_id ) { return $this->verify_password_update_nonce( 'update-user_' . $user_id ); } @@ -79,7 +79,7 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); if ( $update ) { - $old_user_data = get_userdata( $user->ID ); + $old_user_data = $this->get_old_user_data( $user->ID ); if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); return; @@ -96,6 +96,17 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl // TODO: Run this even if JS validation passes? } + /** + * Get the old user data. + * + * @param int $user_id The user ID. + * + * @return \WP_User|false The old user data, or false if the user does not exist. + */ + public function get_old_user_data( $user_id ) { + return get_userdata( $user_id ); + } + /** * Validate the password reset. * @@ -137,7 +148,7 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { * * @return void */ - public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { + public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) { if ( $this->verify_profile_update_nonce( $user_id ) ) { if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { @@ -152,8 +163,10 @@ public function on_profile_update( int $user_id, \WP_User $old_user_data, array * * @param \WP_User $user The user. * @param string $new_password The new password. + * + * @return void */ - public function on_password_reset( $user, $new_password ) { + public function on_password_reset( $user, $new_password ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $this->save_recent_password( $user->ID, $user->user_pass ); } @@ -162,8 +175,10 @@ public function on_password_reset( $user, $new_password ) { * * @param int $user_id The user ID. * @param string $password_hash The password hash to store. + * + * @return void */ - public function save_recent_password( int $user_id, string $password_hash ) { + public function save_recent_password( int $user_id, string $password_hash ): void { $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); if ( ! is_array( $recent_passwords ) ) { diff --git a/projects/packages/account-protection/tests/php/test-email-service.php b/projects/packages/account-protection/tests/php/test-email-service.php index 667fccae881a9..795f3aceb6582 100644 --- a/projects/packages/account-protection/tests/php/test-email-service.php +++ b/projects/packages/account-protection/tests/php/test-email-service.php @@ -6,7 +6,7 @@ use WorDBless\BaseTestCase; /** - * Tests for the Account_Protection class. + * Tests for the Email_Service class. */ class Email_Service_Test extends BaseTestCase { diff --git a/projects/packages/account-protection/tests/php/test-password-manager.php b/projects/packages/account-protection/tests/php/test-password-manager.php new file mode 100644 index 0000000000000..5cb25e18df2cd --- /dev/null +++ b/projects/packages/account-protection/tests/php/test-password-manager.php @@ -0,0 +1,159 @@ + 1 ); + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $password_manager_mock = new Password_Manager( $validation_service_mock ); + + $password_manager_mock->validate_profile_update( $errors, true, $user ); + + $this->assertTrue( $errors->has_errors() ); + $this->assertArrayHasKey( 'nonce_error', $errors->errors ); + } + + public function test_validate_profile_update_success() { + $_POST['_wpnonce'] = 'update-user_1'; + $_POST['pass1'] = 'newpassword'; + + $errors = new \WP_Error(); + $user = (object) array( 'ID' => 1 ); + + $fake_user = new \WP_User(); + $fake_user->ID = 1; + $fake_user->user_pass = wp_hash_password( 'oldpassword' ); + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $validation_service_mock->expects( $this->once() ) + ->method( 'return_first_validation_error' ) + ->willReturn( '' ); + + $password_manager_mock = $this->getMockBuilder( Password_Manager::class ) + ->setConstructorArgs( array( $validation_service_mock ) ) + ->onlyMethods( array( 'verify_profile_update_nonce', 'get_old_user_data' ) ) + ->getMock(); + + $password_manager_mock->expects( $this->once() ) + ->method( 'verify_profile_update_nonce' ) + ->willReturn( true ); + + $password_manager_mock->expects( $this->once() ) + ->method( 'get_old_user_data' ) + ->willReturn( $fake_user ); + + $password_manager_mock->validate_profile_update( $errors, true, $user ); + + $this->assertFalse( $errors->has_errors() ); + } + + public function test_validate_password_reset_with_invalid_user() { + $errors = new \WP_Error(); + $user = new \WP_Error( 'invalid_user', 'Invalid user.' ); + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $password_manager_mock = new Password_Manager( $validation_service_mock ); + + $password_manager_mock->validate_password_reset( $errors, $user ); + + $this->assertFalse( $errors->has_errors() ); + } + + public function test_validate_password_reset_with_valid_user() { + $_POST['pass1'] = 'securepassword'; + + $errors = new \WP_Error(); + $user = new \WP_User(); + $user->ID = 1; + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $validation_service_mock->expects( $this->once() ) + ->method( 'return_first_validation_error' ) + ->willReturn( '' ); + + $password_manager_mock = new Password_Manager( $validation_service_mock ); + $password_manager_mock->validate_password_reset( $errors, $user ); + + $this->assertFalse( $errors->has_errors() ); + } + + public function test_on_profile_update_with_valid_nonce() { + $_POST['action'] = 'update'; + $_POST['_wpnonce'] = 'valid_nonce'; + $_POST['pass1'] = 'newpassword'; + + $user_id = 1; + $old_user_data = new \WP_User(); + $old_user_data->user_pass = 'oldhashedpassword'; + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $password_manager_mock = $this->getMockBuilder( Password_Manager::class ) + ->setConstructorArgs( array( $validation_service_mock ) ) + ->onlyMethods( array( 'save_recent_password', 'verify_profile_update_nonce' ) ) + ->getMock(); + + $password_manager_mock->expects( $this->once() ) + ->method( 'verify_profile_update_nonce' ) + ->willReturn( true ); + + $password_manager_mock->expects( $this->once() ) + ->method( 'save_recent_password' ) + ->with( $user_id, 'oldhashedpassword' ); + + $password_manager_mock->on_profile_update( $user_id, $old_user_data, array() ); + } + + public function test_on_password_reset_saves_recent_password() { + $user = new \WP_User(); + $user->ID = 1; + $user->user_pass = 'hashedpassword'; + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $password_manager_mock = $this->getMockBuilder( Password_Manager::class ) + ->setConstructorArgs( array( $validation_service_mock ) ) + ->onlyMethods( array( 'save_recent_password' ) ) + ->getMock(); + + $password_manager_mock->expects( $this->once() ) + ->method( 'save_recent_password' ) + ->with( $user->ID, 'hashedpassword' ); + + $password_manager_mock->on_password_reset( $user, 'newpassword' ); + } + + public function test_save_recent_password_stores_last_10_passwords() { + $user_id = 1; + $password_hashes = array( + 'hash1', + 'hash2', + 'hash3', + 'hash4', + 'hash5', + 'hash6', + 'hash7', + 'hash8', + 'hash9', + 'hash10', + ); + + update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, $password_hashes ); + + $validation_service_mock = $this->createMock( Validation_Service::class ); + $password_manager_mock = new Password_Manager( $validation_service_mock ); + $password_manager_mock->save_recent_password( $user_id, 'new_hash' ); + + $stored_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); + $this->assertCount( 10, $stored_passwords ); + $this->assertEquals( 'new_hash', $stored_passwords[0] ); + } +} From b651b2b6a06271044f7c03fc1766472d65cc54e7 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 11:56:41 -0800 Subject: [PATCH 104/120] Remove custom nonce, add core create-user nonce check --- .../account-protection/src/class-account-protection.php | 7 +------ .../account-protection/src/class-password-manager.php | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index b819d45dc50e4..c8d4d776a30e7 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -108,12 +108,7 @@ protected function register_runtime_hooks(): void { add_action( 'wp_enqueue_scripts', array( $this->password_detection, 'enqueue_styles' ) ); // Add password validation - add_action( - 'user_new_form', - function () { - wp_nonce_field( 'add-new-user', '_new_user_nonce' ); - } - ); + add_action( 'user_profile_update_errors', array( $this->password_manager, 'validate_profile_update' ), 10, 3 ); add_action( 'validate_password_reset', array( $this->password_manager, 'validate_password_reset' ), 10, 2 ); diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 259e872ca4070..044fec2520751 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -65,7 +65,7 @@ public function verify_profile_update_nonce( $user_id ) { public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { if ( ! $update ) { // This is a new user (wp-admin/user-new.php) - if ( ! isset( $_POST['_new_user_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_new_user_nonce'] ) ), 'add-new-user' ) ) { + if ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) { $errors->add( 'nonce_error', __( 'Error: Nonce verification failed for new user creation.', 'jetpack-account-protection' ) ); return; } From 16d54a6ff03ca1cbb08e798c97dcf7e670f0969f Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 11:59:04 -0800 Subject: [PATCH 105/120] Remove todos - always run server side validation --- .../account-protection/src/class-password-manager.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 044fec2520751..2105d87e44497 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -92,8 +92,6 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl return; } } - - // TODO: Run this even if JS validation passes? } /** @@ -135,8 +133,6 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { return; } } - - // TODO: Run this even if JS validation passes? } /** From c9f5d32ddc5bfef69748aaf0a44c2d3718367e46 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 12:03:01 -0800 Subject: [PATCH 106/120] Update constant naming --- projects/packages/account-protection/src/class-config.php | 2 +- .../account-protection/src/class-password-manager.php | 4 ++-- .../account-protection/src/class-validation-service.php | 2 +- .../account-protection/tests/php/test-password-manager.php | 4 ++-- .../account-protection/tests/php/test-validation-service.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index b4aacb57387ad..f44dfe18f4d17 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -17,5 +17,5 @@ class Config { public const PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION = 600; // 10 minutes public const PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS = 3; - public const VALIDATION_SERVICE_USER_META_KEY = 'jetpack_account_protection_recent_password_hashes'; + public const VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY = 'jetpack_account_protection_recent_password_hashes'; } diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 2105d87e44497..d4c9e80c67d0d 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -175,7 +175,7 @@ public function on_password_reset( $user, $new_password ): void { // phpcs:ignor * @return void */ public function save_recent_password( int $user_id, string $password_hash ): void { - $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); + $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, true ); if ( ! is_array( $recent_passwords ) ) { $recent_passwords = array(); @@ -189,6 +189,6 @@ public function save_recent_password( int $user_id, string $password_hash ): voi array_unshift( $recent_passwords, $password_hash ); $recent_passwords = array_slice( $recent_passwords, 0, 10 ); - update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, $recent_passwords ); + update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords ); } } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 1204b10761116..95d384c157eea 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -250,7 +250,7 @@ public function is_current_password( \WP_User $user, string $password ): bool { * @return bool True if the password hash was recently used, false otherwise. */ public function is_recent_password( int $user_id, string $password ): bool { - $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); + $recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, true ); if ( empty( $recent_passwords ) || ! is_array( $recent_passwords ) ) { return false; diff --git a/projects/packages/account-protection/tests/php/test-password-manager.php b/projects/packages/account-protection/tests/php/test-password-manager.php index 5cb25e18df2cd..9bbad7ec649bf 100644 --- a/projects/packages/account-protection/tests/php/test-password-manager.php +++ b/projects/packages/account-protection/tests/php/test-password-manager.php @@ -146,13 +146,13 @@ public function test_save_recent_password_stores_last_10_passwords() { 'hash10', ); - update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, $password_hashes ); + update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, $password_hashes ); $validation_service_mock = $this->createMock( Validation_Service::class ); $password_manager_mock = new Password_Manager( $validation_service_mock ); $password_manager_mock->save_recent_password( $user_id, 'new_hash' ); - $stored_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, true ); + $stored_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, true ); $this->assertCount( 10, $stored_passwords ); $this->assertEquals( 'new_hash', $stored_passwords[0] ); } diff --git a/projects/packages/account-protection/tests/php/test-validation-service.php b/projects/packages/account-protection/tests/php/test-validation-service.php index bf22f78739404..96caf3702eac3 100644 --- a/projects/packages/account-protection/tests/php/test-validation-service.php +++ b/projects/packages/account-protection/tests/php/test-validation-service.php @@ -189,7 +189,7 @@ public function test_returns_true_if_password_was_recently_used() { $user_id = 1; $password_hash = wp_hash_password( 'somepassword' ); - update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, array( $password_hash ) ); + update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, array( $password_hash ) ); $validation_service = new Validation_Service( $this->get_connection_manager() ); $this->assertTrue( $validation_service->is_recent_password( $user_id, 'somepassword' ) ); @@ -199,7 +199,7 @@ public function test_returns_false_if_password_was_not_recently_used() { $user_id = 1; $password_hash = wp_hash_password( 'somepassword' ); - update_user_meta( $user_id, Config::VALIDATION_SERVICE_USER_META_KEY, array( $password_hash ) ); + update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, array( $password_hash ) ); $validation_service = new Validation_Service( $this->get_connection_manager() ); $this->assertFalse( $validation_service->is_recent_password( $user_id, 'anotherpassword' ) ); From 5559bce0f9f75522bfecb924a3cf53dcee87cc68 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 12:07:15 -0800 Subject: [PATCH 107/120] Translate error message --- projects/packages/account-protection/src/class-config.php | 1 - .../account-protection/src/class-password-detection.php | 2 +- .../account-protection/tests/php/test-password-detection.php | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/projects/packages/account-protection/src/class-config.php b/projects/packages/account-protection/src/class-config.php index f44dfe18f4d17..97020daac1f90 100644 --- a/projects/packages/account-protection/src/class-config.php +++ b/projects/packages/account-protection/src/class-config.php @@ -13,7 +13,6 @@ class Config { public const PASSWORD_DETECTION_TRANSIENT_PREFIX = 'password_detection'; public const PASSWORD_DETECTION_ERROR_CODE = 'password_detection_validation_error'; - public const PASSWORD_DETECTION_ERROR_MESSAGE = 'Password validation failed.'; public const PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION = 600; // 10 minutes public const PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS = 3; diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index b1774897a0e01..ac73491d07847 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -59,7 +59,7 @@ public function login_form_password_detection( $user, string $password ) { return new \WP_Error( Config::PASSWORD_DETECTION_ERROR_CODE, - Config::PASSWORD_DETECTION_ERROR_MESSAGE, + __( 'Password validation failed.', 'jetpack-account-protection' ), array( 'token' => $transient['token'] ) ); } diff --git a/projects/packages/account-protection/tests/php/test-password-detection.php b/projects/packages/account-protection/tests/php/test-password-detection.php index 01197dc57c5ae..62aff859e6b7d 100644 --- a/projects/packages/account-protection/tests/php/test-password-detection.php +++ b/projects/packages/account-protection/tests/php/test-password-detection.php @@ -10,7 +10,7 @@ class Password_Detection_Test extends BaseTestCase { public function test_handle_password_detection_validation_error_redirects_to_login(): void { - $error = new \WP_Error( Config::PASSWORD_DETECTION_ERROR_CODE, Config::PASSWORD_DETECTION_ERROR_MESSAGE, array( 'token' => 'my-token' ) ); + $error = new \WP_Error( Config::PASSWORD_DETECTION_ERROR_CODE, 'Password validation failed.', array( 'token' => 'my-token' ) ); $sut = $this->createPartialMock( Password_Detection::class, array( 'redirect_and_exit' ) ); $sut->expects( $this->once() ) @@ -96,7 +96,7 @@ public function test_login_form_password_detection_sends_email_and_returns_error $error = $sut->login_form_password_detection( $user, 'pw' ); $this->assertInstanceOf( \WP_Error::class, $error, 'Should return a WP_Error object.' ); - $this->assertSame( Config::PASSWORD_DETECTION_ERROR_MESSAGE, $error->get_error_message( Config::PASSWORD_DETECTION_ERROR_CODE ), 'Should return the correct error message.' ); + $this->assertSame( 'Password validation failed.', $error->get_error_message( Config::PASSWORD_DETECTION_ERROR_CODE ), 'Should return the correct error message.' ); $token = $error->get_error_data( Config::PASSWORD_DETECTION_ERROR_CODE )['token']; $this->assertSame( 32, strlen( $token ), 'Token should be 32 characters long.' ); From b716b6f3989b9d3fcf92c669680d15932aefcd2a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 12:48:34 -0800 Subject: [PATCH 108/120] Ensure styles are enqueued when viewing the password detection page --- .../src/class-password-detection.php | 18 ++- .../src/class-password-manager.php | 2 +- .../src/class-password-strength-meter.php | 114 ++++++++++++++++++ 3 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 projects/packages/account-protection/src/class-password-strength-meter.php diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index ac73491d07847..a77c92640f3bc 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -356,11 +356,17 @@ private function set_transient_error( int $user_id, string $message, int $expira * @return void */ public function enqueue_styles(): void { - wp_enqueue_style( - 'password-detection-styles', - plugin_dir_url( __FILE__ ) . 'css/password-detection.css', - array(), - Account_Protection::PACKAGE_VERSION - ); + // No nonce verification necessary - reading only + // phpcs:disable WordPress.Security.NonceVerification + if ( isset( $_GET['action'] ) && $_GET['action'] === 'password-detection' ) { + if ( ! wp_style_is( 'password-detection-styles', 'enqueued' ) ) { + wp_enqueue_style( + 'password-detection-styles', + plugin_dir_url( __FILE__ ) . 'css/password-detection.css', + array(), + Account_Protection::PACKAGE_VERSION + ); + } + } } } diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index d4c9e80c67d0d..cb7efb8f2b674 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -118,7 +118,7 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { return; } - // No nonce verification necessary as the action hooks in after a robust verification process + // No nonce verification necessary - action hooks in after a robust verification process // phpcs:disable WordPress.Security.NonceVerification if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); diff --git a/projects/packages/account-protection/src/class-password-strength-meter.php b/projects/packages/account-protection/src/class-password-strength-meter.php new file mode 100644 index 0000000000000..df3d6b2e63099 --- /dev/null +++ b/projects/packages/account-protection/src/class-password-strength-meter.php @@ -0,0 +1,114 @@ +validation_service = $validation_service ?? new Validation_Service(); + } + + /** + * AJAX endpoint for password validation. + * + * @return void + */ + public function validate_password_ajax(): void { + if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'validate_password_nonce' ) ) { + wp_send_json_error( array( 'message' => 'Invalid nonce.' ) ); + } + + if ( ! isset( $_POST['password'] ) ) { + wp_send_json_error( array( 'message' => 'No password provided.' ) ); + } + + // TODO: May need to skip user specific validation in pass reset unless we can retreive the user object + + $password = sanitize_text_field( wp_unslash( $_POST['password'] ) ); + $state = $this->validation_service->get_validation_state( wp_get_current_user(), $password ); + + wp_send_json_success( array( 'status' => $state ) ); + } + + /** + * Enqueue the password strength meter script on the profile page. + * + * @return void + */ + public function enqueue_jetpack_password_strength_meter_profile_script(): void { + if ( ! wp_script_is( 'jetpack-password-strength-meter', 'enqueued' ) ) { + wp_enqueue_script( + 'jetpack-password-strength-meter', + plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js', + array( 'jquery' ), + Account_Protection::PACKAGE_VERSION, + true + ); + } + + $this->localize_jetpack_data(); + } + + /** + * Enqueue the password strength meter script on the reset password page. + * + * @return void + */ + public function enqueue_jetpack_password_strength_meter_reset_script(): void { + // No nonce verification necessary - action includes a robust verification process + // phpcs:disable WordPress.Security.NonceVerification + if ( isset( $_GET['action'] ) && ( $_GET['action'] === 'rp' || $_GET['action'] === 'resetpass' ) ) { + if ( ! wp_script_is( 'jetpack-password-strength-meter', 'enqueued' ) ) { + wp_enqueue_script( + 'jetpack-password-strength-meter', + plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js', + array( 'jquery' ), + Account_Protection::PACKAGE_VERSION, + true + ); + } + } + + $this->localize_jetpack_data(); + } + + /** + * Localize the Jetpack data for the password strength meter. + * + * @return void + */ + public function localize_jetpack_data(): void { + wp_localize_script( + 'jetpack-password-strength-meter', + 'jetpackData', + array( + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'validate_password_nonce' ), + 'logo' => plugin_dir_url( __FILE__ ) . 'assets/jetpack-logo.svg', + 'checkIcon' => plugin_dir_url( __FILE__ ) . 'assets/check.svg', + 'crossIcon' => plugin_dir_url( __FILE__ ) . 'assets/cross.svg', + 'loadingIcon' => plugin_dir_url( __FILE__ ) . 'assets/loading.svg', + 'validationInitialState' => $this->validation_service->get_validation_initial_state(), + ) + ); + } +} From d296b26b8c9f2cf70e150e498bbf1c13cd289aab Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 12:57:57 -0800 Subject: [PATCH 109/120] Use global page now and action check to enqueue styles --- .../src/class-password-detection.php | 4 +--- .../src/class-validation-service.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-detection.php b/projects/packages/account-protection/src/class-password-detection.php index a77c92640f3bc..3e5457191b28f 100644 --- a/projects/packages/account-protection/src/class-password-detection.php +++ b/projects/packages/account-protection/src/class-password-detection.php @@ -358,15 +358,13 @@ private function set_transient_error( int $user_id, string $message, int $expira public function enqueue_styles(): void { // No nonce verification necessary - reading only // phpcs:disable WordPress.Security.NonceVerification - if ( isset( $_GET['action'] ) && $_GET['action'] === 'password-detection' ) { - if ( ! wp_style_is( 'password-detection-styles', 'enqueued' ) ) { + if ( ( isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php' ) && ( isset( $_GET['action'] ) && $_GET['action'] === 'password-detection' ) ) { wp_enqueue_style( 'password-detection-styles', plugin_dir_url( __FILE__ ) . 'css/password-detection.css', array(), Account_Protection::PACKAGE_VERSION ); - } } } } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 95d384c157eea..87245f26b87f0 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -166,14 +166,14 @@ public function matches_user_data( $user, string $password ): bool { $email_provider = explode( '.', $email_domain )[0]; // 'example' $user_data = array( - $user->user_login, - $user->display_name, - $user->first_name, - $user->last_name, - $user->user_email, - $email_username, - $email_provider, - $user->nickname, + $user->user_login ?? '', + $user->display_name ?? '', + $user->first_name ?? '', + $user->last_name ?? '', + $user->user_email ?? '', + $email_username ?? '', + $email_provider ?? '', + $user->nickname ?? '', ); $password_lower = strtolower( $password ); From 5515183d63fde339daef688b4b18d57a25fffa27 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 14:55:25 -0800 Subject: [PATCH 110/120] Skip recent password checks during create user action --- .../src/class-password-manager.php | 40 +++++++++---------- .../src/class-validation-service.php | 14 ++++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index cb7efb8f2b674..38c709b94b52a 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -63,35 +63,33 @@ public function verify_profile_update_nonce( $user_id ) { * @return void */ public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { - if ( ! $update ) { - // This is a new user (wp-admin/user-new.php) - if ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) { - $errors->add( 'nonce_error', __( 'Error: Nonce verification failed for new user creation.', 'jetpack-account-protection' ) ); - return; - } - // This is an existing user update (wp-admin/profile.php or user-edit.php) - } elseif ( ! $this->verify_profile_update_nonce( $user->ID ) ) { - $errors->add( 'nonce_error', __( 'Error: Nonce verification failed for profile update.', 'jetpack-account-protection' ) ); + if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) + || ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) { + $errors->add( 'nonce_error', __( 'Error: Nonce verification failed.', 'jetpack-account-protection' ) ); return; } - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); + if ( empty( $_POST['pass1'] ) ) { + return; + } - if ( $update ) { - $old_user_data = $this->get_old_user_data( $user->ID ); - if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { - $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); - return; - } - } + $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); - $error = $this->validation_service->return_first_validation_error( $user, $password, 'profile' ); - if ( ! empty( $error ) ) { - $errors->add( 'password_error', $error ); + if ( $update ) { + $old_user_data = $this->get_old_user_data( $user->ID ); + if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { + $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); return; } } + + $context = $update ? 'update' : 'create-user'; + $error = $this->validation_service->return_first_validation_error( $user, $password, $context ); + + if ( ! empty( $error ) ) { + $errors->add( 'password_error', $error ); + return; + } } /** diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 87245f26b87f0..bfaebb22d168c 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -86,14 +86,14 @@ public function return_all_validation_errors( $user, string $password ): array { /** * Return first validation error. * - * @param \WP_User|\stdClass $user The user object or a copy. - * @param string $password The password to check. - * @param 'profile'|'reset' $context The context the validation is run in. + * @param \WP_User|\stdClass $user The user object or a copy. + * @param string $password The password to check. + * @param 'create-user'|'update'|'reset' $context The context the validation is run in. * * @return string The first validation errors (if any). */ public function return_first_validation_error( $user, string $password, $context ): string { - if ( 'profile' === $context ) { + if ( 'reset' !== $context ) { if ( empty( $password ) ) { return __( 'Error: The password cannot be a space or all spaces.', 'jetpack-account-protection' ); } @@ -113,8 +113,10 @@ public function return_first_validation_error( $user, string $password, $context return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); } - if ( $this->is_recent_password( $user->ID, $password ) ) { - return __( 'Error: The password was used recently.', 'jetpack-account-protection' ); + if ( 'create-user' !== $context ) { + if ( $this->is_recent_password( $user->ID, $password ) ) { + return __( 'Error: The password was used recently.', 'jetpack-account-protection' ); + } } if ( $this->is_weak_password( $password ) ) { From a8e6b8d21211e3c277268254955f4a8627693d46 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 15:04:45 -0800 Subject: [PATCH 111/120] Additional skips, and comment clarification --- .../src/class-validation-service.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index bfaebb22d168c..e96d6fa08cb1b 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -93,12 +93,14 @@ public function return_all_validation_errors( $user, string $password ): array { * @return string The first validation errors (if any). */ public function return_first_validation_error( $user, string $password, $context ): string { + // Reset form includes this validation in core if ( 'reset' !== $context ) { if ( empty( $password ) ) { return __( 'Error: The password cannot be a space or all spaces.', 'jetpack-account-protection' ); } } + // Update and create-user forms include this validation in core if ( 'reset' === $context ) { if ( $this->contains_backslash( $password ) ) { return __( 'Error: The password cannot contain a backslash (\\) character.', 'jetpack-account-protection' ); @@ -109,11 +111,15 @@ public function return_first_validation_error( $user, string $password, $context return __( 'Error: The password must be between 6 and 150 characters.', 'jetpack-account-protection' ); } - if ( $this->matches_user_data( $user, $password ) ) { - return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); + // Unable to retrieve user data during reset form validation + if ( 'reset' !== $context ) { + if ( $this->matches_user_data( $user, $password ) ) { + return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); + } } - if ( 'create-user' !== $context ) { + // Unable to retrieve user data during reset form validation, not relevant to create-user form + if ( 'update' === $context ) { if ( $this->is_recent_password( $user->ID, $password ) ) { return __( 'Error: The password was used recently.', 'jetpack-account-protection' ); } From b5699a64ad67f7460a11e77a74807d29e30b06f0 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 15:10:52 -0800 Subject: [PATCH 112/120] Revert skips of user specific reset form validation, hook provides access to this --- .../src/class-validation-service.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index e96d6fa08cb1b..67e5dbee0ec95 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -111,15 +111,11 @@ public function return_first_validation_error( $user, string $password, $context return __( 'Error: The password must be between 6 and 150 characters.', 'jetpack-account-protection' ); } - // Unable to retrieve user data during reset form validation - if ( 'reset' !== $context ) { - if ( $this->matches_user_data( $user, $password ) ) { - return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); - } + if ( $this->matches_user_data( $user, $password ) ) { + return __( 'Error: The password matches user data.', 'jetpack-account-protection' ); } - // Unable to retrieve user data during reset form validation, not relevant to create-user form - if ( 'update' === $context ) { + if ( 'create-user' !== $context ) { if ( $this->is_recent_password( $user->ID, $password ) ) { return __( 'Error: The password was used recently.', 'jetpack-account-protection' ); } From 556fc849003dadbe00a44c7edba18ebea731799e Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 15:23:43 -0800 Subject: [PATCH 113/120] Revert unintended additions --- .../src/class-password-strength-meter.php | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 projects/packages/account-protection/src/class-password-strength-meter.php diff --git a/projects/packages/account-protection/src/class-password-strength-meter.php b/projects/packages/account-protection/src/class-password-strength-meter.php deleted file mode 100644 index df3d6b2e63099..0000000000000 --- a/projects/packages/account-protection/src/class-password-strength-meter.php +++ /dev/null @@ -1,114 +0,0 @@ -validation_service = $validation_service ?? new Validation_Service(); - } - - /** - * AJAX endpoint for password validation. - * - * @return void - */ - public function validate_password_ajax(): void { - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'validate_password_nonce' ) ) { - wp_send_json_error( array( 'message' => 'Invalid nonce.' ) ); - } - - if ( ! isset( $_POST['password'] ) ) { - wp_send_json_error( array( 'message' => 'No password provided.' ) ); - } - - // TODO: May need to skip user specific validation in pass reset unless we can retreive the user object - - $password = sanitize_text_field( wp_unslash( $_POST['password'] ) ); - $state = $this->validation_service->get_validation_state( wp_get_current_user(), $password ); - - wp_send_json_success( array( 'status' => $state ) ); - } - - /** - * Enqueue the password strength meter script on the profile page. - * - * @return void - */ - public function enqueue_jetpack_password_strength_meter_profile_script(): void { - if ( ! wp_script_is( 'jetpack-password-strength-meter', 'enqueued' ) ) { - wp_enqueue_script( - 'jetpack-password-strength-meter', - plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js', - array( 'jquery' ), - Account_Protection::PACKAGE_VERSION, - true - ); - } - - $this->localize_jetpack_data(); - } - - /** - * Enqueue the password strength meter script on the reset password page. - * - * @return void - */ - public function enqueue_jetpack_password_strength_meter_reset_script(): void { - // No nonce verification necessary - action includes a robust verification process - // phpcs:disable WordPress.Security.NonceVerification - if ( isset( $_GET['action'] ) && ( $_GET['action'] === 'rp' || $_GET['action'] === 'resetpass' ) ) { - if ( ! wp_script_is( 'jetpack-password-strength-meter', 'enqueued' ) ) { - wp_enqueue_script( - 'jetpack-password-strength-meter', - plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js', - array( 'jquery' ), - Account_Protection::PACKAGE_VERSION, - true - ); - } - } - - $this->localize_jetpack_data(); - } - - /** - * Localize the Jetpack data for the password strength meter. - * - * @return void - */ - public function localize_jetpack_data(): void { - wp_localize_script( - 'jetpack-password-strength-meter', - 'jetpackData', - array( - 'ajaxurl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'validate_password_nonce' ), - 'logo' => plugin_dir_url( __FILE__ ) . 'assets/jetpack-logo.svg', - 'checkIcon' => plugin_dir_url( __FILE__ ) . 'assets/check.svg', - 'crossIcon' => plugin_dir_url( __FILE__ ) . 'assets/cross.svg', - 'loadingIcon' => plugin_dir_url( __FILE__ ) . 'assets/loading.svg', - 'validationInitialState' => $this->validation_service->get_validation_initial_state(), - ) - ); - } -} From 6e725cb415e7720c9b021106cb41eff36c5765f9 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 18:33:20 -0800 Subject: [PATCH 114/120] Return early if update is irrelevant --- .../account-protection/src/class-password-manager.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 38c709b94b52a..1adc69915400f 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -63,13 +63,13 @@ public function verify_profile_update_nonce( $user_id ) { * @return void */ public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { - if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) - || ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) { - $errors->add( 'nonce_error', __( 'Error: Nonce verification failed.', 'jetpack-account-protection' ) ); + if ( empty( $_POST['pass1'] ) ) { return; } - if ( empty( $_POST['pass1'] ) ) { + if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) + || ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) { + $errors->add( 'nonce_error', __( 'Error: Nonce verification failed.', 'jetpack-account-protection' ) ); return; } From afb4621e431e1857e1d4c450bd901617627005a3 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 18:35:31 -0800 Subject: [PATCH 115/120] Only verify nonce if pass is set --- .../account-protection/src/class-password-manager.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 1adc69915400f..6981fd532c038 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -144,9 +144,9 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { */ public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) { - if ( $this->verify_profile_update_nonce( $user_id ) ) { - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $this->save_recent_password( $user_id, $old_user_data->user_pass ); + if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { + if ( $this->verify_profile_update_nonce( $user_id ) ) { + $this->save_recent_password( $user_id, $old_user_data->user_pass ); } } } From 62d30c290c700aa6b59a00c0f0218203a0648510 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 5 Feb 2025 18:47:37 -0800 Subject: [PATCH 116/120] Skip validation if bypass enabled --- .../src/class-password-manager.php | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 6981fd532c038..0e94c43034c8f 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -67,6 +67,11 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl return; } + // If bypass is enabled, do not validate the password + if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { + return; + } + if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) || ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) { $errors->add( 'nonce_error', __( 'Error: Nonce verification failed.', 'jetpack-account-protection' ) ); @@ -106,6 +111,8 @@ public function get_old_user_data( $user_id ) { /** * Validate the password reset. * + * No nonce verification necessary - action hooks in after a robust verification process + * * @param \WP_Error $errors The error object. * @param \WP_User|\WP_Error $user The user object. * @@ -116,20 +123,27 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { return; } - // No nonce verification necessary - action hooks in after a robust verification process // phpcs:disable WordPress.Security.NonceVerification - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); - if ( $this->validation_service->is_current_password( $user, $password ) ) { - $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); - return; - } + if ( empty( $_POST['pass1'] ) ) { + return; + } - $error = $this->validation_service->return_first_validation_error( $user, $password, 'reset' ); - if ( ! empty( $error ) ) { - $errors->add( 'password_error', $error ); - return; - } + // If bypass is enabled, do not validate the password + // phpcs:disable WordPress.Security.NonceVerification + if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { + return; + } + + $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); + if ( $this->validation_service->is_current_password( $user, $password ) ) { + $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); + return; + } + + $error = $this->validation_service->return_first_validation_error( $user, $password, 'reset' ); + if ( ! empty( $error ) ) { + $errors->add( 'password_error', $error ); + return; } } From 3a2993c305c2b9502259443f5c1de96f4a6f89db Mon Sep 17 00:00:00 2001 From: dkmyta Date: Tue, 11 Feb 2025 18:50:13 -0800 Subject: [PATCH 117/120] Fix test --- .../account-protection/tests/php/test-password-manager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/packages/account-protection/tests/php/test-password-manager.php b/projects/packages/account-protection/tests/php/test-password-manager.php index 9bbad7ec649bf..71cf09dd05a57 100644 --- a/projects/packages/account-protection/tests/php/test-password-manager.php +++ b/projects/packages/account-protection/tests/php/test-password-manager.php @@ -10,6 +10,7 @@ class Password_Manager_Test extends BaseTestCase { public function test_validate_profile_update_nonce_failure() { $_POST['_wpnonce'] = 'invalid_nonce'; + $_POST['pass1'] = 'newpassword'; $errors = new \WP_Error(); $user = (object) array( 'ID' => 1 ); From 7c5733c43717075b619fcc34c49a6858d5b01986 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 12 Feb 2025 10:49:34 -0800 Subject: [PATCH 118/120] Update methods, removes nonce checks, fix tests --- .../src/class-account-protection.php | 4 +- .../src/class-password-manager.php | 75 ++++--------------- .../src/class-validation-service.php | 11 ++- .../tests/php/test-password-manager.php | 70 ++++++----------- .../tests/php/test-validation-service.php | 22 ++++-- 5 files changed, 62 insertions(+), 120 deletions(-) diff --git a/projects/packages/account-protection/src/class-account-protection.php b/projects/packages/account-protection/src/class-account-protection.php index c8d4d776a30e7..9e028ee993853 100644 --- a/projects/packages/account-protection/src/class-account-protection.php +++ b/projects/packages/account-protection/src/class-account-protection.php @@ -113,8 +113,8 @@ protected function register_runtime_hooks(): void { add_action( 'validate_password_reset', array( $this->password_manager, 'validate_password_reset' ), 10, 2 ); // Update recent passwords list - add_action( 'profile_update', array( $this->password_manager, 'on_profile_update' ), 10, 3 ); - add_action( 'after_password_reset', array( $this->password_manager, 'on_password_reset' ), 10, 2 ); + add_action( 'profile_update', array( $this->password_manager, 'on_profile_update' ), 10, 2 ); + add_action( 'after_password_reset', array( $this->password_manager, 'on_password_reset' ), 10, 1 ); } /** diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 0e94c43034c8f..514fc295efa04 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -27,32 +27,6 @@ public function __construct( ?Validation_Service $validation_service = null ) { $this->validation_service = $validation_service ?? new Validation_Service(); } - /** - * Verify the nonce for password update. - * - * @param string $key The nonce key. - * - * @return bool True if the nonce is valid, false otherwise. - */ - private function verify_password_update_nonce( $key ) { - if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $key ) ) { - return false; - } - - return true; - } - - /** - * Verify the nonce for profile update. - * - * @param int $user_id The user ID. - * - * @return bool True if the nonce is valid, false otherwise. - */ - public function verify_profile_update_nonce( $user_id ) { - return $this->verify_password_update_nonce( 'update-user_' . $user_id ); - } - /** * Validate the profile update. * @@ -63,33 +37,25 @@ public function verify_profile_update_nonce( $user_id ) { * @return void */ public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { - if ( empty( $_POST['pass1'] ) ) { + if ( empty( $user->user_pass ) ) { return; } // If bypass is enabled, do not validate the password + // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { return; } - if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) - || ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) { - $errors->add( 'nonce_error', __( 'Error: Nonce verification failed.', 'jetpack-account-protection' ) ); - return; - } - - $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); - if ( $update ) { - $old_user_data = $this->get_old_user_data( $user->ID ); - if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) { + if ( $this->validation_service->is_current_password( $user->ID, $user->user_pass ) ) { $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); return; } } $context = $update ? 'update' : 'create-user'; - $error = $this->validation_service->return_first_validation_error( $user, $password, $context ); + $error = $this->validation_service->return_first_validation_error( $user, $user->user_pass, $context ); if ( ! empty( $error ) ) { $errors->add( 'password_error', $error ); @@ -97,17 +63,6 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl } } - /** - * Get the old user data. - * - * @param int $user_id The user ID. - * - * @return \WP_User|false The old user data, or false if the user does not exist. - */ - public function get_old_user_data( $user_id ) { - return get_userdata( $user_id ); - } - /** * Validate the password reset. * @@ -123,19 +78,20 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { return; } - // phpcs:disable WordPress.Security.NonceVerification + // phpcs:ignore WordPress.Security.NonceVerification if ( empty( $_POST['pass1'] ) ) { return; } // If bypass is enabled, do not validate the password - // phpcs:disable WordPress.Security.NonceVerification + // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { return; } + // phpcs:ignore WordPress.Security.NonceVerification $password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); - if ( $this->validation_service->is_current_password( $user, $password ) ) { + if ( $this->validation_service->is_current_password( $user->ID, $password ) ) { $errors->add( 'password_error', __( 'Error: The password was used recently.', 'jetpack-account-protection' ) ); return; } @@ -151,18 +107,14 @@ public function validate_password_reset( \WP_Error $errors, $user ): void { * Handle the profile update. * * @param int $user_id The user ID. - * @param \WP_User $old_user_data The old user data. - * @param array $userdata The user data. + * @param \WP_User $old_user_data Object containing user data prior to update. * * @return void */ - public function on_profile_update( int $user_id, \WP_User $old_user_data, array $userdata ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + public function on_profile_update( int $user_id, \WP_User $old_user_data ): void { + // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) { - if ( isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) { - if ( $this->verify_profile_update_nonce( $user_id ) ) { - $this->save_recent_password( $user_id, $old_user_data->user_pass ); - } - } + $this->save_recent_password( $user_id, $old_user_data->user_pass ); } } @@ -170,11 +122,10 @@ public function on_profile_update( int $user_id, \WP_User $old_user_data, array * Handle the password reset. * * @param \WP_User $user The user. - * @param string $new_password The new password. * * @return void */ - public function on_password_reset( $user, $new_password ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + public function on_password_reset( $user ): void { $this->save_recent_password( $user->ID, $user->user_pass ); } diff --git a/projects/packages/account-protection/src/class-validation-service.php b/projects/packages/account-protection/src/class-validation-service.php index 67e5dbee0ec95..a64b5065c4fff 100644 --- a/projects/packages/account-protection/src/class-validation-service.php +++ b/projects/packages/account-protection/src/class-validation-service.php @@ -236,12 +236,17 @@ public function is_weak_password( string $password ): bool { /** * Check if the password is the current password for the user. * - * @param \WP_User $user The user. - * @param string $password The password to check. + * @param int $user_id The user ID. + * @param string $password The password to check. * * @return bool True if the password is the current password, false otherwise. */ - public function is_current_password( \WP_User $user, string $password ): bool { + public function is_current_password( int $user_id, string $password ): bool { + $user = get_userdata( $user_id ); + if ( ! $user ) { + return false; + } + return wp_check_password( $password, $user->user_pass, $user->ID ); } diff --git a/projects/packages/account-protection/tests/php/test-password-manager.php b/projects/packages/account-protection/tests/php/test-password-manager.php index 71cf09dd05a57..fd734ace1a639 100644 --- a/projects/packages/account-protection/tests/php/test-password-manager.php +++ b/projects/packages/account-protection/tests/php/test-password-manager.php @@ -8,51 +8,28 @@ * Tests for the Password_Manager class. */ class Password_Manager_Test extends BaseTestCase { - public function test_validate_profile_update_nonce_failure() { - $_POST['_wpnonce'] = 'invalid_nonce'; - $_POST['pass1'] = 'newpassword'; - - $errors = new \WP_Error(); - $user = (object) array( 'ID' => 1 ); - - $validation_service_mock = $this->createMock( Validation_Service::class ); - $password_manager_mock = new Password_Manager( $validation_service_mock ); - - $password_manager_mock->validate_profile_update( $errors, true, $user ); - - $this->assertTrue( $errors->has_errors() ); - $this->assertArrayHasKey( 'nonce_error', $errors->errors ); - } - public function test_validate_profile_update_success() { - $_POST['_wpnonce'] = 'update-user_1'; - $_POST['pass1'] = 'newpassword'; + $user_id = wp_insert_user( + array( + 'user_login' => 'admin', + 'user_pass' => wp_hash_password( 'oldpassword' ), + 'user_email' => 'admin@admin.com', + 'role' => 'administrator', + ) + ); $errors = new \WP_Error(); - $user = (object) array( 'ID' => 1 ); - - $fake_user = new \WP_User(); - $fake_user->ID = 1; - $fake_user->user_pass = wp_hash_password( 'oldpassword' ); + $user = (object) array( + 'ID' => $user_id, + 'user_pass' => wp_hash_password( 'newpassword' ), + ); $validation_service_mock = $this->createMock( Validation_Service::class ); $validation_service_mock->expects( $this->once() ) ->method( 'return_first_validation_error' ) ->willReturn( '' ); - $password_manager_mock = $this->getMockBuilder( Password_Manager::class ) - ->setConstructorArgs( array( $validation_service_mock ) ) - ->onlyMethods( array( 'verify_profile_update_nonce', 'get_old_user_data' ) ) - ->getMock(); - - $password_manager_mock->expects( $this->once() ) - ->method( 'verify_profile_update_nonce' ) - ->willReturn( true ); - - $password_manager_mock->expects( $this->once() ) - ->method( 'get_old_user_data' ) - ->willReturn( $fake_user ); - + $password_manager_mock = new Password_Manager( $validation_service_mock ); $password_manager_mock->validate_profile_update( $errors, true, $user ); $this->assertFalse( $errors->has_errors() ); @@ -89,10 +66,6 @@ public function test_validate_password_reset_with_valid_user() { } public function test_on_profile_update_with_valid_nonce() { - $_POST['action'] = 'update'; - $_POST['_wpnonce'] = 'valid_nonce'; - $_POST['pass1'] = 'newpassword'; - $user_id = 1; $old_user_data = new \WP_User(); $old_user_data->user_pass = 'oldhashedpassword'; @@ -100,18 +73,21 @@ public function test_on_profile_update_with_valid_nonce() { $validation_service_mock = $this->createMock( Validation_Service::class ); $password_manager_mock = $this->getMockBuilder( Password_Manager::class ) ->setConstructorArgs( array( $validation_service_mock ) ) - ->onlyMethods( array( 'save_recent_password', 'verify_profile_update_nonce' ) ) + ->onlyMethods( array( 'save_recent_password' ) ) ->getMock(); - $password_manager_mock->expects( $this->once() ) - ->method( 'verify_profile_update_nonce' ) - ->willReturn( true ); - $password_manager_mock->expects( $this->once() ) ->method( 'save_recent_password' ) ->with( $user_id, 'oldhashedpassword' ); - $password_manager_mock->on_profile_update( $user_id, $old_user_data, array() ); + $password_manager_mock->on_profile_update( + $user_id, + $old_user_data, + array( + 'ID' => 1, + 'user_pass' => 'newpassword', + ) + ); } public function test_on_password_reset_saves_recent_password() { @@ -129,7 +105,7 @@ public function test_on_password_reset_saves_recent_password() { ->method( 'save_recent_password' ) ->with( $user->ID, 'hashedpassword' ); - $password_manager_mock->on_password_reset( $user, 'newpassword' ); + $password_manager_mock->on_password_reset( $user ); } public function test_save_recent_password_stores_last_10_passwords() { diff --git a/projects/packages/account-protection/tests/php/test-validation-service.php b/projects/packages/account-protection/tests/php/test-validation-service.php index 96caf3702eac3..b7b8696bbb87a 100644 --- a/projects/packages/account-protection/tests/php/test-validation-service.php +++ b/projects/packages/account-protection/tests/php/test-validation-service.php @@ -168,18 +168,28 @@ public function test_returns_false_if_password_is_not_weak() { } public function test_returns_true_if_password_is_current_password() { - $user = new \WP_User(); - $user->user_pass = wp_hash_password( 'somepassword' ); - $user->ID = 1; + $user = wp_insert_user( + array( + 'user_login' => 'admin', + 'user_pass' => 'somepassword', + 'user_email' => 'admin@admin.com', + 'role' => 'administrator', + ) + ); $validation_service = new Validation_Service( $this->get_connection_manager() ); $this->assertTrue( $validation_service->is_current_password( $user, 'somepassword' ) ); } public function test_returns_false_if_password_is_not_current_password() { - $user = new \WP_User(); - $user->user_pass = wp_hash_password( 'somepassword' ); - $user->ID = 1; + $user = wp_insert_user( + array( + 'user_login' => 'admin', + 'user_pass' => 'somepassword', + 'user_email' => 'admin@admin.com', + 'role' => 'administrator', + ) + ); $validation_service = new Validation_Service( $this->get_connection_manager() ); $this->assertFalse( $validation_service->is_current_password( $user, 'anotherpassword' ) ); From bd3e654a52c306af700340b01ba6bbc42f3b8122 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 12 Feb 2025 11:09:22 -0800 Subject: [PATCH 119/120] Fix test --- .../tests/php/test-password-manager.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/projects/packages/account-protection/tests/php/test-password-manager.php b/projects/packages/account-protection/tests/php/test-password-manager.php index fd734ace1a639..718e2d4c528d1 100644 --- a/projects/packages/account-protection/tests/php/test-password-manager.php +++ b/projects/packages/account-protection/tests/php/test-password-manager.php @@ -66,6 +66,8 @@ public function test_validate_password_reset_with_valid_user() { } public function test_on_profile_update_with_valid_nonce() { + $_POST['action'] = 'update'; + $user_id = 1; $old_user_data = new \WP_User(); $old_user_data->user_pass = 'oldhashedpassword'; @@ -82,11 +84,7 @@ public function test_on_profile_update_with_valid_nonce() { $password_manager_mock->on_profile_update( $user_id, - $old_user_data, - array( - 'ID' => 1, - 'user_pass' => 'newpassword', - ) + $old_user_data ); } From 647be75c69181706a7bf08ef1e5fb618d6ceb030 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 12 Feb 2025 11:12:06 -0800 Subject: [PATCH 120/120] Remove comment --- .../packages/account-protection/src/class-password-manager.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/packages/account-protection/src/class-password-manager.php b/projects/packages/account-protection/src/class-password-manager.php index 514fc295efa04..42fa92d615c36 100644 --- a/projects/packages/account-protection/src/class-password-manager.php +++ b/projects/packages/account-protection/src/class-password-manager.php @@ -66,8 +66,6 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl /** * Validate the password reset. * - * No nonce verification necessary - action hooks in after a robust verification process - * * @param \WP_Error $errors The error object. * @param \WP_User|\WP_Error $user The user object. *