PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Utils / class-user-utils.php
wp-2fa / includes / classes / Utils Last commit date
class-abstract-migration.php 15 hours ago class-date-time-utils.php 15 hours ago class-debugging.php 15 hours ago class-generate-modal.php 15 hours ago class-migration.php 15 hours ago class-request-utils.php 15 hours ago class-settings-utils.php 15 hours ago class-upgrade-guard.php 15 hours ago class-user-utils.php 15 hours ago class-validator.php 15 hours ago class-white-label.php 15 hours ago index.php 15 hours ago
class-user-utils.php
378 lines
1 <?php
2 /**
3 * Responsible for different user's manipulations.
4 *
5 * @package wp2fa
6 * @subpackage user-utils
7 *
8 * @copyright 2026 Melapress
9 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
10 *
11 * @see https://wordpress.org/plugins/wp-2fa/
12 */
13
14 declare(strict_types=1);
15
16 namespace WP2FA\Utils;
17
18 defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
19
20 use WP2FA\Methods\Backup_Codes;
21 use WP2FA\Admin\Helpers\User_Helper;
22
23 if ( ! class_exists( '\WP2FA\Utils\User_Utils' ) ) {
24 /**
25 * Utility class for creating modal popup markup.
26 *
27 * @package WP2FA\Utils
28 *
29 * @since 1.4.2
30 */
31 class User_Utils {
32 /**
33 * Holds map with human readable 2FA statuses.
34 *
35 * @var array
36 *
37 * @since 2.2.0
38 */
39 private static $statuses;
40
41 /**
42 * Determines the proper 2FA status of the given user.
43 *
44 * @param \WP_User $user - The user to check.
45 *
46 * @return array
47 *
48 * @since 2.2.0
49 */
50 public static function determine_user_2fa_status( $user ) {
51 // Get current user, we going to need this regardless.
52 $current_user = \wp_get_current_user();
53
54 // Bail if we still don't have an object.
55 if ( ! is_a( $user, '\WP_User' ) || ! is_a( $current_user, '\WP_User' ) ) {
56 return array();
57 }
58
59 $roles = (array) $user->roles;
60
61 // Grab grace period UNIX time.
62 $grace_period_expired = User_Helper::get_grace_period( $user );
63 $is_user_excluded = User_Helper::is_excluded( $user->ID );
64 $is_user_enforced = User_Helper::is_enforced( $user->ID );
65 $is_user_locked = User_Helper::is_user_locked( $user->ID );
66 $user_last_login = User_Helper::get_login_date_for_user( $user->ID );
67
68 // First let's see if the user already has a token.
69 $enabled_methods = User_Helper::get_enabled_method_for_user( $user );
70
71 $enforcement_policy = Settings_Utils::get_setting_role( User_Helper::get_user_role( $user ), 'enforcement-policy' );
72 $no_enforced_methods = false;
73 if ( 'do-not-enforce' === $enforcement_policy || ( 'all-users' !== $enforcement_policy && ! User_Helper::is_user_enforced( $user ) && ! $is_user_excluded ) ) {
74 /**
75 * Filter that gives methods ability to make themselves enforced even the global enforcement is off.
76 *
77 * @param bool - at this point this is true.
78 *
79 * @since 3.0.0
80 */
81 $no_enforced_methods = \apply_filters( WP_2FA_PREFIX . 'is_method_enforced', true, $user );
82 }
83
84 $user_type = array();
85 // Order is important here - for speed optimizations see self::extract_statuses() function of that class - we probably need to redo the whole thing.
86
87 if ( $is_user_excluded ) {
88 $user_type[] = 'user_is_excluded';
89 }
90
91 if ( $is_user_locked ) {
92 $user_type[] = 'user_is_locked';
93 }
94
95 if ( $no_enforced_methods && ! empty( $enabled_methods ) ) {
96 $user_type[] = 'no_required_has_enabled';
97 }
98
99 if ( $no_enforced_methods && empty( $enabled_methods ) && ! $is_user_excluded ) {
100 if ( empty( $user_last_login ) ) {
101 $user_type[] = User_Helper::USER_UNDETERMINED_STATUS;
102 } else {
103 $user_type[] = 'no_required_not_enabled';
104 }
105 }
106
107 if ( ! $no_enforced_methods && empty( $enabled_methods ) && ! $is_user_excluded && $is_user_enforced ) {
108 $user_type[] = 'user_needs_to_setup_2fa';
109 }
110
111 if ( ! $no_enforced_methods && empty( $enabled_methods ) && ! $is_user_excluded && ! $is_user_enforced ) {
112 if ( empty( $user_last_login ) ) {
113 $user_type[] = User_Helper::USER_UNDETERMINED_STATUS;
114 } else {
115 $user_type[] = 'no_required_not_enabled';
116 }
117 }
118
119 if ( ! empty( $enabled_methods ) ) {
120 $user_type[] = 'has_enabled_methods';
121 }
122
123 $codes_remaining = Backup_Codes::codes_remaining_for_user( $user );
124 if ( 0 === $codes_remaining ) {
125 $user_type[] = 'user_needs_to_setup_backup_codes';
126 }
127
128 if ( empty( $roles ) ) {
129 $user_type[] = 'orphan_user'; // User has no role.
130 }
131
132 if ( \current_user_can( 'manage_options' ) ) {
133 $user_type[] = 'can_manage_options';
134 }
135
136 if ( \current_user_can( 'read' ) ) {
137 $user_type[] = 'can_read';
138 }
139
140 if ( $grace_period_expired ) {
141 $user_type[] = 'grace_has_expired';
142 }
143
144 if ( $current_user->ID === $user->ID ) {
145 $user_type[] = 'viewing_own_profile';
146 }
147
148 /*
149 * Gives the ability to alter the user types for the user.
150 *
151 * @param array $user_type - Type of the user.
152 * @param \WP_User $user - The WP user.
153 *
154 * @since 2.0.0
155 */
156 return \apply_filters( WP_2FA_PREFIX . 'additional_user_types', $user_type, $user );
157 }
158
159 /**
160 * Checks is all values exist in given array.
161 *
162 * @param array $needles - Which values to check.
163 * @param array $haystack - The array to check against.
164 *
165 * @return bool
166 *
167 * @since 2.2.0
168 */
169 public static function in_array_all( $needles, $haystack ) {
170 return empty( array_diff( $needles, $haystack ) );
171 }
172
173 /**
174 * Check if role is not in given array of roles.
175 *
176 * @param array $roles - All roles.
177 * @param array $user_roles - The User roles.
178 *
179 * @return bool
180 *
181 * @since 2.2.0
182 */
183 public static function role_is_not( $roles, $user_roles ) {
184 if ( empty( array_intersect( $roles, $user_roles ) ) ) {
185 return true;
186 }
187
188 return false;
189 }
190
191 /**
192 * Return all users, either by using a direct query or get_users.
193 *
194 * @param string $method Method to use.
195 * @param array $users_args Query arguments.
196 *
197 * @return mixed Array of IDs/Object of Users.
198 *
199 * @since 2.2.0
200 */
201 private static function get_all_users_data( $method, $users_args ) {
202 if ( 'get_users' === $method ) {
203 return get_users( $users_args );
204 }
205
206 // method is "query", let's build the SQL query ourselves using prepared statements.
207 global $wpdb;
208
209 $batch_size = isset( $users_args['batch_size'] ) ? (int) $users_args['batch_size'] : false;
210 $offset = isset( $users_args['count'] ) ? (int) $users_args['count'] * $batch_size : false;
211
212 // Base select.
213 $select = "SELECT ID, user_login FROM {$wpdb->users} u";
214 $params = array();
215
216 // If we want to grab users with a specific role.
217 if ( isset( $users_args['role__in'] ) && ! empty( $users_args['role__in'] ) ) {
218 $roles = (array) $users_args['role__in'];
219
220 $select .= " INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id WHERE um.meta_key LIKE %s AND (";
221 // meta_key pattern (base prefix + %capabilities).
222 $meta_key_pattern = $wpdb->base_prefix . '%capabilities';
223 $params[] = $meta_key_pattern;
224
225 $role_like_parts = array();
226 foreach ( $roles as $role ) {
227 $role_escaped = $wpdb->esc_like( (string) $role );
228 $role_like_parts[] = 'um.meta_value LIKE %s';
229 // pattern includes serialized quotes: %"role"%.
230 $params[] = '%"' . $role_escaped . '"%';
231 }
232
233 $select .= implode( ' OR ', $role_like_parts );
234 $select .= ' )';
235
236 $excluded_users = ( ! empty( $users_args['excluded_users'] ) ) ? (array) $users_args['excluded_users'] : array();
237
238 if ( ! empty( $excluded_users ) ) {
239 $placeholders = implode( ',', array_fill( 0, count( $excluded_users ), '%s' ) );
240 $select .= " AND user_login NOT IN ( {$placeholders} )";
241 foreach ( $excluded_users as $excluded_user ) {
242 $params[] = (string) $excluded_user;
243 }
244 }
245
246 $skip_existing_2fa_users = ( ! empty( $users_args['skip_existing_2fa_users'] ) ) ? (bool) $users_args['skip_existing_2fa_users'] : false;
247
248 if ( $skip_existing_2fa_users ) {
249 $select .= " AND u.ID NOT IN ( SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s )";
250 $params[] = 'wp_2fa_enabled_methods';
251 }
252 }
253
254 if ( $batch_size ) {
255 // append LIMIT/OFFSET using integer placeholders.
256 $select .= ' LIMIT %d OFFSET %d';
257 $params[] = $batch_size;
258 $params[] = $offset;
259 }
260
261 // If we have parameters, prepare the statement; otherwise run directly.
262 if ( ! empty( $params ) ) {
263 // Use splat operator to pass params array to prepare.
264 $sql = $wpdb->prepare( $select, ...$params );
265 return $wpdb->get_results( $sql );
266 }
267
268 return $wpdb->get_results( $select );
269 }
270
271 /**
272 * Retrieve string of comma separated IDs.
273 *
274 * @param string $method Method to use.
275 * @param array $users_args Query arguments.
276 *
277 * @return string List of IDs.
278 *
279 * @since 2.2.0
280 */
281 public static function get_all_user_ids( $method, $users_args ) {
282 $user_data = self::get_all_users_data( $method, $users_args );
283
284 $users = array_map(
285 function ( $user ) {
286 return (int) $user->ID;
287 },
288 $user_data
289 );
290
291 // Sanitize the IDs before returning them as a comma-separated string.
292 $sanitized_ids = array_map( 'intval', $users );
293
294 return implode( ',', $sanitized_ids );
295 }
296
297 /**
298 * Retrieve array if user IDs and login names.
299 *
300 * @param string $method Method to use.
301 * @param array $users_args Query arguments.
302 *
303 * @return array User details.
304 *
305 * @since 2.2.0
306 */
307 public static function get_all_user_ids_and_login_names( $method, $users_args ) {
308 $user_data = self::get_all_users_data( $method, $users_args );
309
310 $users = array_map(
311 function ( $user ) {
312 $user_item['ID'] = intval( $user->ID );
313 $user_item['user_login'] = sanitize_user( $user->user_login, true );
314
315 return $user_item;
316 },
317 $user_data
318 );
319
320 return $users;
321 }
322
323 /**
324 * Returns the array with human readable statuses of the WP 2FA.
325 *
326 * @since 1.6
327 *
328 * @return array
329 */
330 public static function get_human_readable_user_statuses() {
331 if ( null === self::$statuses ) {
332 self::$statuses = array(
333 'has_enabled_methods' => \esc_html__( 'Configured', 'wp-2fa' ),
334 'user_needs_to_setup_2fa' => \esc_html__( 'Required but not configured', 'wp-2fa' ),
335 'no_required_has_enabled' => \esc_html__( 'Configured (but not required)', 'wp-2fa' ),
336 'no_required_not_enabled' => \esc_html__( 'Not required & not configured', 'wp-2fa' ),
337 'user_is_excluded' => \esc_html__( 'Not allowed', 'wp-2fa' ),
338 'user_is_locked' => \esc_html__( 'Locked', 'wp-2fa' ),
339 User_Helper::USER_UNDETERMINED_STATUS => \esc_html__( 'User has not logged in yet, 2FA status is unknown', 'wp-2fa' ),
340 );
341 }
342
343 return self::$statuses;
344 }
345
346 /**
347 * Gets the user types extracted with @see User_Utils::determine_user_2fa_status,
348 * checks values and generates human readable 2FA status text.
349 *
350 * @param array $user_types - The types of the user.
351 *
352 * @return array An array with the id and label elements of user 2FA status. Empty in case there is not match.
353 *
354 * @since 1.7.0 Changed the function to return the id and label of the first match it finds instead of concatenated labels of all matched statuses.
355 */
356 public static function extract_statuses( array $user_types ) {
357 if ( null === self::$statuses ) {
358 self::get_human_readable_user_statuses();
359 }
360
361 if ( empty( $user_types ) ) {
362 return array();
363 }
364
365 $key_to_search = sanitize_key( reset( $user_types ) );
366
367 if ( isset( self::$statuses[ $key_to_search ] ) ) {
368 return array(
369 'id' => $key_to_search,
370 'label' => esc_html( self::$statuses[ $key_to_search ] ),
371 );
372 }
373
374 return array();
375 }
376 }
377 }
378