PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / post / forgot-password.php

forgot-password.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/post/forgot-password.php

162 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Forgot-password / reset-password form handlers.
4 *
5 * Two `admin-post.php` actions, both available to logged-out visitors:
6 *
7 * - `storeengine/forgot_password/request_reset`
8 * Accept an email, hand off to WP core's retrieve_password() so the
9 * branded HTML reset email (PasswordReset class) goes out, then
10 * redirect back with a generic success flag. Generic on purpose —
11 * differentiating "email found" vs "email not found" would leak
12 * account existence.
13 *
14 * - `storeengine/forgot_password/set_password`
15 * Validate the {key, login} pair against WP core's
16 * check_password_reset_key(), then call reset_password() so the
17 * standard `password_reset` action chain fires for integrations.
18 *
19 * Both flows route through the existing AbstractPostHandler pipeline so
20 * we inherit its nonce verification, field sanitization, and
21 * allow_visitor_action plumbing without reimplementing it.
22 */
23
24 namespace StoreEngine\Post;
25
26 if ( ! defined( 'ABSPATH' ) ) {
27 exit;
28 }
29
30 use StoreEngine\Classes\AbstractPostHandler;
31 use StoreEngine\Utils\Helper;
32 use WP_Error;
33 use WP_User;
34
35 class ForgotPassword extends AbstractPostHandler {
36
37 public function __construct() {
38 $this->actions = [
39 'forgot_password/request_reset' => [
40 'callback' => [ $this, 'request_reset' ],
41 'capability' => '',
42 'allow_visitor_action' => true,
43 'fields' => [
44 'email' => 'string',
45 ],
46 ],
47 'forgot_password/set_password' => [
48 'callback' => [ $this, 'set_password' ],
49 'capability' => '',
50 'allow_visitor_action' => true,
51 'fields' => [
52 'key' => 'string',
53 'login' => 'string',
54 'pass1' => 'string',
55 'pass2' => 'string',
56 ],
57 ],
58 ];
59 }
60
61 /**
62 * Step 1: visitor enters email, we ask WP to send the reset email.
63 *
64 * We always redirect to the same success state regardless of whether
65 * the email matches a real account — leaking existence here is the
66 * classic account-enumeration mistake.
67 */
68 public function request_reset( array $payload ) {
69 $endpoint_url = Helper::get_account_endpoint_url( 'forgot-password' );
70
71 $email = isset( $payload['email'] ) ? sanitize_email( $payload['email'] ) : '';
72 if ( ! $email || ! is_email( $email ) ) {
73 wp_safe_redirect( add_query_arg( 'reset_error', 'invalid_email', $endpoint_url ) );
74 die();
75 }
76
77 // retrieve_password() validates that the account exists, generates
78 // a reset key, persists it via update_user_meta, and then fires
79 // `retrieve_password_notification_email` — which our PasswordReset
80 // email class hooks to send the branded HTML version.
81 //
82 // On unknown email it returns a WP_Error; we deliberately swallow it
83 // so the response is identical to the success path.
84 retrieve_password( $email );
85
86 wp_safe_redirect( add_query_arg( 'reset_sent', '1', $endpoint_url ) );
87 die();
88 }
89
90 /**
91 * Step 2: visitor returns via the email link with a {key, login} pair,
92 * picks a new password, submits.
93 */
94 public function set_password( array $payload ) {
95 $endpoint_url = Helper::get_account_endpoint_url( 'forgot-password' );
96
97 $key = isset( $payload['key'] ) ? sanitize_text_field( $payload['key'] ) : '';
98 $login = isset( $payload['login'] ) ? sanitize_text_field( $payload['login'] ) : '';
99 $pass1 = $payload['pass1'] ?? '';
100 $pass2 = $payload['pass2'] ?? '';
101
102 if ( ! $key || ! $login ) {
103 return new WP_Error(
104 'invalid_reset_link',
105 __( 'This reset link is missing required information. Please request a new one.', 'storeengine' ),
106 [ 'status' => 400, 'title' => __( 'Invalid reset link', 'storeengine' ) ]
107 );
108 }
109
110 if ( '' === $pass1 || '' === $pass2 ) {
111 wp_safe_redirect( add_query_arg(
112 [ 'key' => $key, 'login' => $login, 'reset_error' => 'empty' ],
113 $endpoint_url
114 ) );
115 die();
116 }
117
118 // Minimum-length policy. Filterable so a site can tighten it (e.g.
119 // require 12+ chars) without having to fork this handler. WP doesn't
120 // enforce a minimum at the reset_password() layer, so without this
121 // check `'a'` would be a valid new password.
122 $min_length = (int) apply_filters( 'storeengine/auth/min_password_length', 8 );
123 if ( strlen( $pass1 ) < $min_length ) {
124 wp_safe_redirect( add_query_arg(
125 [ 'key' => $key, 'login' => $login, 'reset_error' => 'too_short' ],
126 $endpoint_url
127 ) );
128 die();
129 }
130
131 if ( $pass1 !== $pass2 ) {
132 wp_safe_redirect( add_query_arg(
133 [ 'key' => $key, 'login' => $login, 'reset_error' => 'mismatch' ],
134 $endpoint_url
135 ) );
136 die();
137 }
138
139 $user = check_password_reset_key( $key, $login );
140 if ( is_wp_error( $user ) || ! ( $user instanceof WP_User ) ) {
141 return new WP_Error(
142 'expired_reset_link',
143 __( 'This reset link is invalid or has expired. Please request a new one.', 'storeengine' ),
144 [ 'status' => 400, 'title' => __( 'Invalid reset link', 'storeengine' ) ]
145 );
146 }
147
148 reset_password( $user, $pass1 );
149
150 // Land the customer on the StoreEngine dashboard, not wp-login.php.
151 // For logged-out visitors, the dashboard page's [storeengine_dashboard]
152 // shortcode renders [storeengine_login_form] — the branded sign-in
153 // form. Using storeengine_login_url() here was unsafe because it
154 // falls back to wp_login_url() whenever auth_redirect_type isn't
155 // exactly 'storeengine' or the dashboard_page setting is missing,
156 // dropping the customer on WP's default login screen at the end of
157 // what's supposed to be a fully branded flow.
158 wp_safe_redirect( add_query_arg( 'password_updated', '1', Helper::get_dashboard_url() ) );
159 die();
160 }
161 }
162