pwned_api = 'https://api.pwnedpasswords.com/range/'; $this->model = wd_di()->get( Password_Reset::class ); $this->password_protection_model = wd_di()->get( Password_Protection_Settings::class ); } /** * Get the password that was submitted by user. * * @return string */ public function get_submitted_password() { $password = ''; foreach ( array( 'pwd', 'pass1', 'password', 'edd_user_pass', 'password_1', 'account_password' ) as $key ) { $submitted_pass = HTTP::post( $key ); if ( '' !== $submitted_pass ) { $password = $submitted_pass; break; } } return $password; } /** * Makes an API request to the remote server and checks if the password is pwned. * * @param string $password The password to check. * * @return WP_Error|bool */ public function check_pwned_password( $password ) { $hash = strtoupper( hash( 'sha1', $password ) ); $subhash = substr( $hash, 0, 5 ); $url = $this->pwned_api . $subhash; $args = array( 'method' => 'GET', 'headers' => array( 'user-agent' => defender_get_own_user_agent(), ), ); $request = wp_remote_request( $url, $args ); if ( is_wp_error( $request ) ) { return $request; } $pwned_count = 0; $body = wp_remote_retrieve_body( $request ); foreach ( array_map( 'trim', explode( "\n", trim( $body ) ) ) as $row ) { if ( $subhash . substr( strtoupper( $row ), 0, 35 ) === $hash ) { $pwned_count = substr( $row, 36 ); break; } } // Found Pwned password or not. return $pwned_count > 0; } /** * Check if the specified user role is enabled. * * @param WP_User $user The user to check. * @param array $selected_user_roles The roles to check against. * * @return bool */ public function is_enabled_by_user_role( $user, $selected_user_roles ) { // No for super admin. if ( is_multisite() && is_super_admin( $user->ID ) ) { return false; } if ( ! isset( $user->roles ) || array() === $user->roles ) { // WP_User does have data about roles sometimes, e.g. on the Profile page, so we get it from userdata. $user_id = $user->ID; if ( ! is_multisite() ) { $user_meta = get_userdata( $user_id ); if ( ! isset( $user_meta->roles ) || array() === $user_meta->roles ) { // User should have roles. $this->log( sprintf( "User ID: %d doesn't have roles.", $user_id ), self::PASSWORD_LOG ); return false; } $user_roles = $user_meta->roles; } else { $arr_user_blogs = get_blogs_of_user( $user_id ); if ( ! is_array( $arr_user_blogs ) || array() === $arr_user_blogs ) { // User should be associated with some site. $this->log( sprintf( 'User ID: %d is not associated with any site.', $user_id ), self::PASSWORD_LOG ); return false; } $user_blog_id = array_key_first( $arr_user_blogs ); $user = new WP_User( $user_id, '', $user_blog_id ); if ( ! isset( $user->roles ) || array() === $user->roles ) { $this->log( sprintf( "User ID: %d doesn't have roles on MU.", $user->ID ), self::PASSWORD_LOG ); return false; } $user_roles = $user->roles; } } else { $user_roles = $user->roles; } $common_roles = array_intersect( $selected_user_roles, $user_roles ); return array() !== $common_roles; } /** * Get reset password redirect URL. * * @param WP_User $user The user for whom to generate the URL. * * @return string */ public function get_reset_password_redirect_url( $user ): string { $url = ''; $key = get_password_reset_key( $user ); if ( ! is_wp_error( $key ) ) { if ( wd_di()->get( Woocommerce::class )->is_wc_login_context() && function_exists( 'wc_lostpassword_url' ) ) { $url = add_query_arg( array( 'key' => $key, 'login' => $user->user_login, ), wc_lostpassword_url() ); } else { $url = add_query_arg( array( 'action' => 'rp', 'key' => $key, 'login' => $user->user_login, ), wp_login_url() ); } $this->hosting_compatibility( $key, $user->user_login ); } return $url; } /** * Sets a cookie notice. * * @param string $name The name of the cookie. * @param string $value The value of the cookie. * @param int $time The expiration time of the cookie. */ public function set_cookie_notice( $name, $value, $time ) { if ( ! isset( $_COOKIE[ $name ] ) ) { setcookie( $name, $value, array( 'expires' => $time, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); } } /** * Remove cookie notice. * Setting the expiration time in the past indicates to the browser that the cookie has expired and should be * removed. * * @param string $name The name of the cookie. * * @return void * @since 4.3.0 Removed the $time parameter. */ public function remove_cookie_notice( string $name ): void { if ( isset( $_COOKIE[ $name ] ) && ! headers_sent() ) { setcookie( $name, '', array( 'expires' => time() - YEAR_IN_SECONDS, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); } $this->remove_extra_cookies(); } /** * Gets the last time the user's password was changed. * * @param WP_User|int $user The user to check. * * @return int The timestamp of the last password change. */ protected function password_last_changed( $user ) { if ( ! $user ) { return 0; } $changed = (int) get_user_meta( $user->ID, 'wd_last_password_change', true ); if ( ! $changed ) { $changed = strtotime( $user->user_registered ); $changed = ! is_int( $changed ) ? (int) $changed : $changed; } return $changed; } /** * Checks if the user's password is expired. * * @param WP_User $user The user to check. * * @return bool True if the password is expired, false otherwise. */ public function check_expired_password( $user ) { return isset( $this->model->force_time ) && $this->model->force_time >= $this->password_last_changed( $user ); } /** * Updates the last password change time when a user's password is updated. * * @param WP_User $user The user whose password was updated. */ public function handle_password_updated( $user ) { update_user_meta( $user->ID, 'wd_last_password_change', time() ); } /** * Handles compatibility with different hosting environments when resetting passwords. * * @param string $key The reset key. * @param string $user_login The user's login name. */ protected function hosting_compatibility( $key, $user_login ) { $mask_login = new \WP_Defender\Model\Setting\Mask_Login(); if ( ! isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && $mask_login->is_active() && 'flywheel' === Server::get_current_server() ) { $value = sprintf( '%s:%s', $user_login, $key ); setcookie( 'wp-resetpass-' . COOKIEHASH, $value, 0, $mask_login->get_new_login_url(), COOKIE_DOMAIN, is_ssl(), true ); } } /** * Removes extra cookies set during the password reset process. */ protected function remove_extra_cookies() { if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 'flywheel' === Server::get_current_server() && ! headers_sent() ) { setcookie( 'wp-resetpass-' . COOKIEHASH, '', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); } } /** * Force reset password. * * @param WP_User|WP_Error $user WP_User object or WP_Error. * @param string $password Password plain string. * * @return WP_User|WP_Error Return user object or error object. */ public function do_force_reset( $user, $password ) { // The check for $user and $password was performed before the method was called. if ( wp_check_password( $password, $user->user_pass, $user->ID ) && $this->is_force_reset( $user ) ) { $action = 'password_reset'; $cookie_name = 'display_reset_password_warning'; $this->trigger_redirect( $user, $action, $cookie_name ); } return $user; } /** * Reset weak password. * * @param WP_User|WP_Error $user WP_User object or WP_Error. * @param string $password Password plain string. * * @return WP_User|WP_Error Return user object or error object. */ public function do_weak_reset( $user, $password ) { // The check for $user and $password was performed before the method was called. if ( wp_check_password( $password, $user->user_pass, $user->ID ) && $this->is_weak_password( $user, $password ) ) { $action = 'password_protection'; $cookie_name = 'display_pwned_password_warning'; $this->trigger_redirect( $user, $action, $cookie_name ); } return $user; } /** * Redirect to reset password with error message. * * @param WP_User|WP_Error $user WP_User object or WP_Error. * @param string $action Action query string name. * @param string $cookie_name Cookie name. */ public function trigger_redirect( $user, $action, $cookie_name ) { // Set cookie to check and display the warning notice on reset password page. $this->set_cookie_notice( $cookie_name, true, time() + MINUTE_IN_SECONDS * 2 ); // Get the reset password URL. $url = $this->get_reset_password_redirect_url( $user ); /** * Fires before redirecting to the password reset page. * * @param string $url * @param string $action * * @since 2.5.6 */ do_action( 'wd_forced_reset_password_url', $url, $action ); // Redirect to the reset password page. if ( '' !== $url ) { $url = esc_url_raw( $url ); header( 'Cache-Control: no-store, no-cache' ); wp_safe_redirect( $url ); exit(); } } /** * Add WooCommerce error message for password reset warnings. * * @param string $wc_message WooCommerce default error message. * @return string WooCommerce error message. */ public function add_woocommerce_error_message( $wc_message ) { if ( isset( $_COOKIE['display_pwned_password_warning'] ) && function_exists( 'wc_print_notice' ) ) { wc_print_notice( $this->password_protection_model->pwned_actions['force_change_message'], 'error' ); $this->remove_cookie_notice( 'display_pwned_password_warning' ); } if ( isset( $_COOKIE['display_reset_password_warning'] ) && function_exists( 'wc_print_notice' ) ) { wc_print_notice( $this->model->message, 'error' ); $this->remove_cookie_notice( 'display_reset_password_warning' ); } return $wc_message; } /** * Verify if password is weak. * Is user role enabled for weak password verification and is the password weak? * If so then return true else password is strong therefore return false. * * @param WP_User $user WP_User object. * @param string $password Plain password string. * * @return bool If password weak then true else false. * @since 2.6.1 */ public function is_weak_password( $user, $password ) { $user_roles = $this->password_protection_model->user_roles; $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles ); $is_pwned = $this->check_pwned_password( $password ); $is_weak_password = $this->password_protection_model->is_active() && $is_enabled_by_user && ! is_wp_error( $is_pwned ) && $is_pwned; return $is_weak_password; } /** * Verify if password is need to be force reset. * Is user role enabled for force password reset and is the password expired? * If so then return true else password is non expired therefore return false. * * @param WP_User $user WP_User object. * * @return bool If password expired then true else false. * @since 2.6.1 */ public function is_force_reset( $user ) { $user_roles = $this->model->user_roles; $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles ); $is_expired = $this->check_expired_password( $user ); $is_expired_password = $this->model->is_active() && $is_enabled_by_user && $is_expired; return $is_expired_password; } }