+ get_jp_emblem( true );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . esc_html__( 'risks of using weak passwords', 'jetpack-account-protection' ) . ''
+ );
+ ?>
+
+
+
+
+ email_service->mask_email_address( $user->user_email ) )
+ );
+ ?>
+
+
+
+
+
+
+ ' . esc_html__( 'reset your password', 'jetpack-account-protection' ) . ''
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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( \WP_User $user, string $password ): bool {
+ if ( ! user_can( $user, 'publish_posts' ) && ! user_can( $user, 'edit_published_posts' ) ) {
+ return false;
+ }
+
+ /**
+ * Filter which determines whether or not password detection should be applied for the provided user.
+ *
+ * @since 0.1.0
+ *
+ * @param bool $requires_protection Whether or not password detection should be applied.
+ * @param \WP_User $user The user object to apply the filter against.
+ */
+
+ $user_requires_protection = apply_filters( 'jetpack_account_protection_user_requires_protection', true, $user );
+
+ if ( ! $user_requires_protection ) {
+ return false;
+ }
+
+ return wp_check_password( $password, $user->user_pass, $user->ID );
+ }
+
+ /**
+ * Generate and store a consolidated transient for the user.
+ *
+ * @param int $user_id The user ID.
+ * @param string $auth_code The auth code.
+ *
+ * @return string The generated token associated with the new transient data.
+ */
+ private function generate_and_store_transient_data( int $user_id, string $auth_code ): string {
+ $token = wp_generate_password( 32, false, false );
+
+ $data = array(
+ 'user_id' => $user_id,
+ 'auth_code' => $auth_code,
+ 'requests' => 1,
+ );
+
+ $set_token_transient = set_transient( Config::PREFIX . "_{$token}", $data, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION );
+ $set_user_transient = set_transient( Config::PREFIX . "_last_valid_token_{$user_id}", $token, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION );
+ if ( ! $set_token_transient || ! $set_user_transient ) {
+ $this->set_transient_error(
+ $user_id,
+ array(
+ 'code' => 'transient_error',
+ 'message' => __( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ),
+ )
+ );
+ }
+
+ return $token;
+ }
+
+ /**
+ * Redirect to the login page.
+ *
+ * @return never
+ */
+ private function redirect_to_login() {
+ $this->redirect_and_exit( wp_login_url() );
+ }
+
+ /**
+ * Get redirect URL.
+ *
+ * @param string $token The token.
+ *
+ * @return string The redirect URL.
+ */
+ 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 $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( \WP_User $user, string $token, string $auth_code, string $user_input ): void {
+ if ( $auth_code && $auth_code === $user_input ) {
+ $this->set_transient_success(
+ $user->ID,
+ array(
+ 'code' => 'auth_code_success',
+ 'message' => __( 'Authentication code verified successfully.', 'jetpack-account-protection' ),
+ )
+ );
+
+ delete_transient( Config::PREFIX . "_{$token}" );
+ delete_transient( Config::PREFIX . "_last_valid_token_{$user->ID}" );
+ wp_set_auth_cookie( $user->ID, true );
+ wp_set_current_user( $user->ID );
+ } else {
+ $this->set_transient_error(
+ $user->ID,
+ array(
+ 'code' => 'auth_code_error',
+ 'message' => __( 'Authentication code verification failed. Please try again.', 'jetpack-account-protection' ),
+ )
+ );
+ }
+ }
+
+ /**
+ * Set a transient success message.
+ *
+ * @param int $user_id The user ID.
+ * @param array $success An array of the success code and message.
+ * @param int $expiration The expiration time in seconds.
+ *
+ * @return void
+ */
+ public function set_transient_success( int $user_id, array $success, int $expiration = 60 ): void {
+ set_transient( Config::PREFIX . "_success_{$user_id}", $success, $expiration );
+ }
+
+ /**
+ * Set a transient error message.
+ *
+ * @param int $user_id The user ID.
+ * @param array $error An array of the error code and message.
+ * @param int $expiration The expiration time in seconds.
+ *
+ * @return void
+ */
+ public function set_transient_error( int $user_id, array $error, int $expiration = 60 ): void {
+ set_transient( Config::PREFIX . "_error_{$user_id}", $error, $expiration );
+ }
+
+ /**
+ * Enqueue the password detection page styles.
+ *
+ * @return void
+ */
+ public function enqueue_styles(): void {
+ global $pagenow;
+ if ( ! isset( $pagenow ) || $pagenow !== 'wp-login.php' ) {
+ return;
+ }
+ // No nonce verification necessary - reading only
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( 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/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-manager.php b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-manager.php
new file mode 100644
index 00000000..c830c958
--- /dev/null
+++ b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-manager.php
@@ -0,0 +1,154 @@
+validation_service = $validation_service ?? new Validation_Service();
+ }
+
+ /**
+ * 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 ( 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;
+ }
+
+ $core_validation_errors = $errors->get_error_messages( 'pass' );
+ $jetpack_validation_errors = $this->validation_service->get_validation_errors( $user->user_pass, true, $user );
+ $validation_errors = array_diff( $jetpack_validation_errors, $core_validation_errors );
+
+ foreach ( $validation_errors as $validation_error ) {
+ $errors->add( 'pass', $validation_error, array( 'form-field' => 'pass1' ) );
+ }
+ }
+
+ /**
+ * 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 {
+ if ( is_wp_error( $user ) ) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( empty( $_POST['pass1'] ) ) {
+ 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;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $password = wp_unslash( $_POST['pass1'] );
+
+ $core_validation_errors = $errors->get_error_messages( 'pass' );
+ $jetpack_validation_errors = $this->validation_service->get_validation_errors( $password );
+ $validation_errors = array_diff( $jetpack_validation_errors, $core_validation_errors );
+
+ foreach ( $validation_errors as $validation_error ) {
+ $errors->add( 'pass', $validation_error, array( 'form-field' => 'pass1' ) );
+ }
+ }
+
+ /**
+ * Handle the profile update.
+ *
+ * @param int $user_id The user ID.
+ * @param \WP_User|\stdClass|null $old_user_data Object containing user data prior to update.
+ *
+ * @return void
+ */
+ public function on_profile_update( int $user_id, $old_user_data ): void {
+ if ( ! is_object( $old_user_data ) || empty( $old_user_data->user_pass ) ) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) {
+ $this->save_recent_password_hash( $user_id, $old_user_data->user_pass );
+ }
+ }
+
+ /**
+ * Handle the password reset.
+ *
+ * @param \WP_User|\stdClass|null $user The user object.
+ *
+ * @return void
+ */
+ public function on_password_reset( $user ): void {
+ if ( ! is_object( $user ) || ! isset( $user->ID ) || empty( $user->user_pass ) ) {
+ return;
+ }
+
+ $this->save_recent_password_hash( $user->ID, $user->user_pass );
+ }
+
+ /**
+ * 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.
+ *
+ * @return void
+ */
+ public function save_recent_password_hash( int $user_id, string $password_hash ): void {
+ $recent_passwords = get_user_meta( $user_id, Config::RECENT_PASSWORD_HASHES_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, Config::PASSWORD_MANAGER_RECENT_PASSWORDS_LIMIT );
+
+ update_user_meta( $user_id, Config::RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords );
+ }
+}
diff --git a/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-strength-meter.php b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-strength-meter.php
new file mode 100644
index 00000000..f4854b73
--- /dev/null
+++ b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-password-strength-meter.php
@@ -0,0 +1,183 @@
+validation_service = $validation_service ?? new Validation_Service();
+ }
+
+ /**
+ * Wrapper method for nonce verification.
+ *
+ * @param string $nonce Nonce value.
+ * @param string $action Nonce action.
+ *
+ * @return bool
+ */
+ protected function verify_nonce( string $nonce, string $action ): bool {
+ return wp_verify_nonce( $nonce, $action );
+ }
+
+ /**
+ * Wrapper method for sending a JSON error response.
+ *
+ * @param string $message The error message.
+ *
+ * @return void
+ */
+ protected function send_json_error( string $message ): void {
+ // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
+ wp_send_json_error( array( 'message' => $message ), null, JSON_UNESCAPED_SLASHES );
+ }
+
+ /**
+ * Wrapper method for sending a JSON success response.
+ *
+ * @param array $data The data to send.
+ *
+ * @return void
+ */
+ protected function send_json_success( array $data ): void {
+ // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
+ wp_send_json_success( $data, null, JSON_UNESCAPED_SLASHES );
+ }
+
+ /**
+ * AJAX endpoint for password validation.
+ *
+ * @return void
+ */
+ public function validate_password_ajax(): void {
+ // phpcs:disable WordPress.Security.NonceVerification
+ if ( ! isset( $_POST['password'] ) ) {
+ $this->send_json_error( __( 'No password provided.', 'jetpack-account-protection' ) );
+ return;
+ }
+
+ if ( ! isset( $_POST['nonce'] ) || ! $this->verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'validate_password_nonce' ) ) {
+ $this->send_json_error( __( 'Invalid nonce.', 'jetpack-account-protection' ) );
+ return;
+ }
+
+ $user_specific = false;
+ if ( isset( $_POST['user_specific'] ) ) {
+ $user_specific = filter_var( sanitize_text_field( wp_unslash( $_POST['user_specific'] ) ), FILTER_VALIDATE_BOOLEAN );
+ }
+
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ $password = wp_unslash( $_POST['password'] );
+ // phpcs:enable WordPress.Security.NonceVerification
+ $state = $this->validation_service->get_validation_state( $password, $user_specific );
+
+ $this->send_json_success( array( 'state' => $state ) );
+ }
+
+ /**
+ * Enqueue the password strength meter script on the profile page.
+ *
+ * @return void
+ */
+ public function enqueue_jetpack_password_strength_meter_profile_script(): void {
+ global $pagenow;
+
+ if ( ! isset( $pagenow ) || ! in_array( $pagenow, array( 'profile.php', 'user-new.php', 'user-edit.php' ), true ) ) {
+ return;
+ }
+
+ $this->enqueue_script();
+ $this->enqueue_styles();
+
+ // Only profile page should run user specific checks.
+ $this->localize_jetpack_data( 'profile.php' === $pagenow );
+ }
+
+ /**
+ * 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 as the action includes a robust verification process
+ // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $_GET['action'] ) && ( 'rp' === $_GET['action'] || 'resetpass' === $_GET['action'] ) ) {
+ $this->enqueue_script();
+ $this->enqueue_styles();
+ $this->localize_jetpack_data();
+ }
+ }
+
+ /**
+ * Localize the Jetpack data for the password strength meter.
+ *
+ * @param bool $user_specific Whether or not to run user specific checks.
+ *
+ * @return void
+ */
+ public function localize_jetpack_data( bool $user_specific = false ): void {
+ $jetpack_logo = new Jetpack_Logo();
+
+ wp_localize_script(
+ 'jetpack-password-strength-meter',
+ 'jetpackData',
+ array(
+ 'ajaxurl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'validate_password_nonce' ),
+ 'userSpecific' => $user_specific,
+ 'logo' => htmlspecialchars( $jetpack_logo->get_jp_emblem( true ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ),
+ 'validationInitialState' => $this->validation_service->get_validation_initial_state( $user_specific ),
+ )
+ );
+ }
+
+ /**
+ * Enqueue the password strength meter script.
+ *
+ * @return void
+ */
+ public function enqueue_script(): void {
+ wp_enqueue_script(
+ 'jetpack-password-strength-meter',
+ plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js',
+ array( 'jquery' ),
+ Account_Protection::PACKAGE_VERSION,
+ true
+ );
+ }
+
+ /**
+ * Enqueue the password strength meter styles.
+ *
+ * @return void
+ */
+ public function enqueue_styles(): void {
+ wp_enqueue_style(
+ 'strength-meter-styles',
+ plugin_dir_url( __FILE__ ) . 'css/strength-meter.css',
+ array(),
+ Account_Protection::PACKAGE_VERSION
+ );
+ }
+}
diff --git a/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-settings.php b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-settings.php
new file mode 100644
index 00000000..e7b2e809
--- /dev/null
+++ b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-settings.php
@@ -0,0 +1,44 @@
+account_protection = $account_protection ?? Account_Protection::instance();
+ }
+
+ /**
+ * Get account protection settings.
+ *
+ * @return array
+ */
+ public function get() {
+ $settings = array(
+ 'isEnabled' => $this->account_protection->is_enabled(),
+ 'isSupported' => $this->account_protection->is_supported_environment(),
+ 'hasUnsupportedJetpackVersion' => $this->account_protection->has_unsupported_jetpack_version(),
+ );
+
+ return $settings;
+ }
+}
diff --git a/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-validation-service.php b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-validation-service.php
new file mode 100644
index 00000000..6855dffe
--- /dev/null
+++ b/wp-content/plugins/jetpack-protect/jetpack_vendor/automattic/jetpack-account-protection/src/class-validation-service.php
@@ -0,0 +1,326 @@
+connection_manager = $connection_manager ?? new Connection_Manager();
+ }
+
+ /**
+ * Dependency decoupling so we can test this class.
+ *
+ * @param string $password_prefix The password prefix to be checked.
+ * @return array|\WP_Error
+ */
+ protected function request_suffixes( string $password_prefix ) {
+ return Client::wpcom_json_api_request_as_blog(
+ '/jetpack-protect-weak-password/' . $password_prefix,
+ '2',
+ array( 'method' => 'GET' ),
+ null,
+ 'wpcom'
+ );
+ }
+
+ /**
+ * Return validation initial state.
+ *
+ * @param bool $user_specific Whether or not to include user specific checks.
+ *
+ * @return array An array of all validation statuses and messages.
+ */
+ public function get_validation_initial_state( $user_specific ): array {
+ $base_conditions = array(
+ 'core' => array(
+ 'status' => null,
+ 'message' => __( 'Strong password', 'jetpack-account-protection' ),
+ 'info' => __( 'Passwords should meet WordPress core security requirements to enhance account protection.', 'jetpack-account-protection' ),
+ ),
+ 'contains_backslash' => array(
+ 'status' => null,
+ 'message' => __( "Doesn't contain a backslash (\\) character", 'jetpack-account-protection' ),
+ 'info' => null,
+ ),
+ 'invalid_length' => array(
+ 'status' => null,
+ 'message' => __( 'Between 6 and 150 characters', 'jetpack-account-protection' ),
+ 'info' => null,
+ ),
+ 'leaked' => array(
+ 'status' => null,
+ 'message' => __( 'Not a leaked password', 'jetpack-account-protection' ),
+ 'info' => __( 'If found in a public breach, this password may already be known to attackers.', 'jetpack-account-protection' ),
+ ),
+ );
+
+ if ( ! $user_specific ) {
+ return $base_conditions;
+ }
+
+ $user_specific_conditions = array(
+ 'matches_user_data' => array(
+ 'status' => null,
+ 'message' => __( "Doesn't match existing user data", 'jetpack-account-protection' ),
+ 'info' => __( 'Using a password similar to your username or email makes it easier to guess.', 'jetpack-account-protection' ),
+ ),
+ 'recent' => array(
+ 'status' => null,
+ 'message' => __( 'Not used recently', 'jetpack-account-protection' ),
+ 'info' => __( 'Reusing old passwords may increase security risks. A fresh password improves protection.', 'jetpack-account-protection' ),
+ ),
+ );
+
+ return array_merge( $base_conditions, $user_specific_conditions );
+ }
+
+ /**
+ * Return validation state - client-side.
+ *
+ * @param string $password The password to check.
+ * @param bool $user_specific Whether or not to run user specific checks.
+ *
+ * @return array An array of the status of each check.
+ */
+ public function get_validation_state( string $password, $user_specific ): array {
+ $validation_state = $this->get_validation_initial_state( $user_specific );
+
+ $validation_state['contains_backslash']['status'] = $this->contains_backslash( $password );
+ $validation_state['invalid_length']['status'] = $this->is_invalid_length( $password );
+ $validation_state['leaked']['status'] = $this->is_leaked_password( $password );
+
+ if ( ! $user_specific ) {
+ return $validation_state;
+ }
+
+ // Run checks on existing user data
+ $user = wp_get_current_user();
+ $validation_state['matches_user_data']['status'] = $this->matches_user_data( $user, $password );
+ $validation_state['recent']['status'] = $this->is_recent_password_hash( $user, $password );
+
+ return $validation_state;
+ }
+
+ /**
+ * Return all validation errors - server-side.
+ *
+ * @param string $password The password to check.
+ * @param bool $user_specific Whether or not to run user specific checks.
+ * @param \stdClass|null $user The user data or null.
+ *
+ * @return array The validation errors (if any).
+ */
+ public function get_validation_errors( string $password, $user_specific = false, $user = null ): array {
+ $errors = array();
+
+ if ( empty( $password ) ) {
+ $errors[] = __( '