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