PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-account-protection / src / class-email-service.php

class-email-service.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3, at jetpack_vendor/automattic/jetpack-account-protection/src/class-email-service.php

207 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class used to define Email Service.
4 *
5 * @package automattic/jetpack-account-protection
6 */
7
8 namespace Automattic\Jetpack\Account_Protection;
9
10 use Automattic\Jetpack\Connection\Client;
11 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 use Jetpack_Options;
13
14 /**
15 * Class Email_Service
16 */
17 class Email_Service {
18 /**
19 * Connection manager dependency.
20 *
21 * @var Connection_Manager
22 */
23 private $connection_manager;
24
25 /**
26 * Constructor for dependency injection.
27 *
28 * @param Connection_Manager|null $connection_manager Connection manager dependency.
29 */
30 public function __construct(
31 ?Connection_Manager $connection_manager = null
32 ) {
33 $this->connection_manager = $connection_manager ?? new Connection_Manager();
34 }
35
36 /**
37 * Send the email using the API.
38 *
39 * @param int $user_id The user ID.
40 * @param string $auth_code The authentication code.
41 *
42 * @return true|\WP_Error True if the email was sent successfully, \WP_Error otherwise.
43 */
44 public function api_send_auth_email( int $user_id, string $auth_code ) {
45 $blog_id = Jetpack_Options::get_option( 'id' );
46
47 /**
48 * Filters whether the Account Protection verification email should be handled externally.
49 *
50 * When the filter returns a truthy value, the default WPCOM API email send is skipped,
51 * allowing sites to deliver the email locally (e.g. via `wp_mail()`).
52 *
53 * @since 0.3.0
54 *
55 * @param bool $handled Whether the email has been handled. Default false.
56 * @param int $user_id The user ID.
57 * @param string $auth_code The authentication code.
58 * @param int $blog_id The blog ID, or false if not available.
59 */
60 $handled = apply_filters( 'jetpack_account_protection_send_auth_email', false, $user_id, $auth_code, $blog_id );
61
62 if ( $handled ) {
63 return true;
64 }
65
66 if ( ! $blog_id || ! $this->connection_manager->is_connected() ) {
67 return new \WP_Error( 'jetpack_connection_error', __( 'Jetpack is not connected. Please connect and try again.', 'jetpack-account-protection' ) );
68 }
69
70 $body = array(
71 'user_id' => $user_id,
72 'code' => $auth_code,
73 );
74
75 $response = $this->send_email_request( (int) $blog_id, $body );
76 if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
77 return new \WP_Error( 'email_send_error', __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
78 }
79
80 $response_code = wp_remote_retrieve_response_code( $response );
81 $body = json_decode( wp_remote_retrieve_body( $response ), true );
82 if ( 200 !== $response_code ) {
83 return new \WP_Error( $body['code'] ?? 'email_send_error', $body['message'] ?? __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
84 }
85
86 if ( empty( $body['email_send_success'] ) ) {
87 return new \WP_Error( 'email_send_error', __( 'Failed to send authentication code. Please try again.', 'jetpack-account-protection' ) );
88 }
89
90 return true;
91 }
92
93 /**
94 * Dependency decoupling for the static call to the client.
95 *
96 * @param int $blog_id Blog ID.
97 * @param array $body The request body.
98 * @return array|\WP_Error Response data or error.
99 */
100 protected function send_email_request( int $blog_id, array $body ) {
101 return Client::wpcom_json_api_request_as_blog(
102 sprintf( '/sites/%d/jetpack-protect-send-verification-code', $blog_id ),
103 '2',
104 array(
105 'method' => 'POST',
106 ),
107 $body,
108 'wpcom'
109 );
110 }
111
112 /**
113 * Resend email attempts.
114 *
115 * @param int $user_id The user ID.
116 * @param array $transient_data The transient data.
117 * @param string $token The token.
118 *
119 * @return true|\WP_Error True if the email was resent successfully, \WP_Error otherwise.
120 */
121 public function resend_auth_email( int $user_id, array $transient_data, string $token ) {
122 if ( $transient_data['requests'] >= Config::PASSWORD_DETECTION_EMAIL_REQUEST_LIMIT || $this->user_email_limit_reached( $user_id ) ) {
123 return new \WP_Error( 'email_request_limit_exceeded', __( 'Email request limit exceeded. Please try again later.', 'jetpack-account-protection' ) );
124 }
125
126 $auth_code = $this->generate_auth_code();
127 $transient_data['auth_code'] = $auth_code;
128
129 $resend = $this->api_send_auth_email( $user_id, $auth_code );
130 if ( is_wp_error( $resend ) ) {
131 return $resend;
132 }
133
134 $this->count_user_email( $user_id );
135 ++$transient_data['requests'];
136
137 if ( ! set_transient( Config::PREFIX . "_{$token}", $transient_data, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION ) ) {
138 return new \WP_Error( 'transient_set_error', __( 'Failed to set transient data. Please try again.', 'jetpack-account-protection' ) );
139 }
140
141 return true;
142 }
143
144 /**
145 * Get the name of the transient counting the codes emailed to a user across the network.
146 *
147 * @param int $user_id The user ID.
148 *
149 * @return string
150 */
151 public static function get_user_email_count_key( int $user_id ): string {
152 return Config::PREFIX . "_email_requests_user_{$user_id}";
153 }
154
155 /**
156 * Whether the user has been emailed as many codes as the limit allows, counted across the network.
157 *
158 * @param int $user_id The user ID.
159 *
160 * @return bool
161 */
162 public function user_email_limit_reached( int $user_id ): bool {
163 return (int) get_site_transient( self::get_user_email_count_key( $user_id ) ) >= Config::PASSWORD_DETECTION_EMAIL_REQUEST_LIMIT;
164 }
165
166 /**
167 * Count a code emailed to a user.
168 *
169 * @param int $user_id The user ID.
170 *
171 * @return void
172 */
173 public function count_user_email( int $user_id ): void {
174 $key = self::get_user_email_count_key( $user_id );
175
176 set_site_transient( $key, (int) get_site_transient( $key ) + 1, Config::PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION );
177 }
178
179 /**
180 * Generate an auth code.
181 *
182 * @return string The generated auth code.
183 */
184 public function generate_auth_code(): string {
185 return (string) wp_rand( 100000, 999999 );
186 }
187
188 /**
189 * Mask an email address like d*****@g*****.com.
190 *
191 * @param string $email The email address to mask.
192 *
193 * @return string The masked email address.
194 */
195 public function mask_email_address( string $email ): string {
196 $parts = explode( '@', $email );
197 $name = substr( $parts[0], 0, 1 ) . str_repeat( '*', strlen( $parts[0] ) - 1 );
198 $domain_parts = explode( '.', $parts[1] );
199 $domain = substr( $domain_parts[0], 0, 1 ) . str_repeat( '*', strlen( $domain_parts[0] ) - 1 );
200
201 // Join all domain parts except the first one with dots
202 $tld = implode( '.', array_slice( $domain_parts, 1 ) );
203
204 return "{$name}@{$domain}.{$tld}";
205 }
206 }
207