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 / Admin / class-profile-section-renderer.php
wp-2fa / includes / classes / Admin Last commit date
AdminSettings 1 day ago Controllers 1 day ago Fly-Out 1 day ago Helpers 1 day ago Methods 1 day ago Settings 1 day ago SettingsPages 1 day ago Views 1 day ago class-about-us.php 1 day ago class-docs-and-support.php 1 day ago class-free-support-notice.php 1 day ago class-help-contact-us.php 1 day ago class-license-page.php 1 day ago class-new-interface-notice.php 1 day ago class-plugin-updated-notice.php 1 day ago class-premium-features.php 1 day ago class-profile-section-renderer.php 1 day ago class-settings-page.php 1 day ago class-setup-wizard.php 1 day ago class-top-bar-banner.php 1 day ago class-user-listing.php 1 day ago class-user-notices.php 1 day ago class-user-profile.php 1 day ago class-user-registered.php 1 day ago class-wizard-integration.php 1 day ago index.php 1 day ago
class-profile-section-renderer.php
543 lines
1 <?php
2 /**
3 * WP 2FA - Profile Section Renderer
4 *
5 * Assembles all data needed for the profile templates and renders them.
6 * Replaces the old inline HTML approach with a template-based system.
7 *
8 * Works in both admin (profile page) and frontend (shortcode) contexts.
9 *
10 * @package wp2fa
11 * @subpackage profile
12 * @since 4.0.0
13 * @copyright 2026 Melapress
14 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
15 */
16
17 declare( strict_types=1 );
18
19 namespace WP2FA\Admin;
20
21 use WP2FA\WP2FA;
22 use WP2FA\Methods\TOTP;
23 use WP2FA\Utils\User_Utils;
24 use WP2FA\Extensions_Loader;
25 use WP2FA\Methods\Backup_Codes;
26 use WP2FA\Utils\Settings_Utils;
27 use WP2FA\Admin\Helpers\WP_Helper;
28 use WP2FA\Freemius\User_Licensing;
29 use WP2FA\Admin\Controllers\Methods;
30 use WP2FA\Admin\Helpers\User_Helper;
31 use WP2FA\Admin\Controllers\Settings;
32 use WP2FA\Extensions\Zero_Setup_Email\Zero_Setup_Email;
33
34 defined( 'ABSPATH' ) || exit;
35
36 if ( ! class_exists( '\WP2FA\Admin\Profile_Section_Renderer' ) ) {
37
38 /**
39 * Renders the new card-based profile 2FA section.
40 *
41 * @since 4.0.0
42 */
43 class Profile_Section_Renderer {
44
45 /**
46 * Script handle for the profile section JS.
47 */
48 const SCRIPT_HANDLE = 'wp2fa-profile-section';
49
50 /**
51 * Style handle for the profile section CSS.
52 */
53 const STYLE_HANDLE = 'wp2fa-profile-section';
54
55 /**
56 * Template directory path (with trailing separator).
57 *
58 * @var string
59 */
60 private static $template_dir = '';
61
62 /**
63 * Get the template directory path.
64 *
65 * @return string
66 */
67 private static function get_template_dir(): string {
68 if ( empty( self::$template_dir ) ) {
69 self::$template_dir = WP_2FA_PATH . 'templates' . DIRECTORY_SEPARATOR . 'profile' . DIRECTORY_SEPARATOR;
70 }
71
72 return self::$template_dir;
73 }
74
75 /**
76 * Enqueue CSS and JS for the profile section.
77 *
78 * Can be called from admin hooks or frontend shortcode context.
79 *
80 * @param bool $is_frontend Whether this is a frontend (shortcode) context.
81 *
82 * @return void
83 */
84 public static function enqueue_assets( bool $is_frontend = false ): void {
85 $plugin_url = WP_2FA_URL;
86 $version = WP_2FA_VERSION;
87
88 // CSS.
89 \wp_enqueue_style(
90 self::STYLE_HANDLE,
91 $plugin_url . 'css/wp2fa-profile.css',
92 array(),
93 $version
94 );
95
96 // JS.
97 \wp_enqueue_script(
98 self::SCRIPT_HANDLE,
99 $plugin_url . 'js/profile/wp2fa-profile.js',
100 array(),
101 $version,
102 true
103 );
104
105 // Backup codes profile JS.
106 \wp_enqueue_script(
107 'wp2fa-profile-backup-codes',
108 $plugin_url . 'js/profile/wp2fa-profile-backup-codes.js',
109 array( self::SCRIPT_HANDLE ),
110 $version,
111 true
112 );
113
114 // Localized data for the profile JS.
115 $user = \wp_get_current_user();
116
117 $localized = array(
118 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
119 'i18n' => array(
120 'showQrCode' => \esc_html__( 'Show QR code', 'wp-2fa' ),
121 'hideQrCode' => \esc_html__( 'Hide QR code', 'wp-2fa' ),
122 'copied' => \esc_html__( 'Copied!', 'wp-2fa' ),
123 'yourBackupCodes' => '', //\esc_html__( 'Your backup codes', 'wp-2fa' ),
124 'backupCodesGenerated' => WP2FA::get_wp2fa_white_label_setting( 'backup_codes_generated', true ) ?: \esc_html__( 'Store these codes somewhere safe. Each code can only be used once.', 'wp-2fa' ),
125 'backupCodesEmailSent' => \esc_html__( 'Codes sent to your email.', 'wp-2fa' ),
126 'backupCodesEmailError' => \esc_html__( 'Failed to send email.', 'wp-2fa' ),
127 'networkError' => \esc_html__( 'Network error. Please try again.', 'wp-2fa' ),
128 ),
129 );
130
131 // Auto-open the wizard if the user needs to configure 2FA instantly.
132 if ( $user && $user->ID ) {
133 $show_setup = false;
134 if ( isset( $_GET['show'] ) && 'wp-2fa-setup' === $_GET['show'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
135 $show_setup = true;
136 } elseif ( User_Helper::get_user_enforced_instantly( $user ) ) {
137 $show_setup = true;
138 }
139
140 $show_enable2fa = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting', true, $user->ID );
141 if ( $show_setup && $show_enable2fa ) {
142 $localized['autoOpenWizard'] = true;
143 }
144 }
145
146 /**
147 * Filter: wp_2fa_profile_localized_data
148 *
149 * Allows extensions to add data to the profile section JS.
150 *
151 * @param array $localized The localized data array.
152 *
153 * @since 4.0.0
154 */
155 $localized = \apply_filters( WP_2FA_PREFIX . 'profile_localized_data', $localized );
156
157 \wp_localize_script( self::SCRIPT_HANDLE, 'wp2faProfileData', $localized );
158
159 /**
160 * Action: wp_2fa_profile_enqueue_assets
161 *
162 * Allows extensions to enqueue their own profile section assets.
163 *
164 * @param string $plugin_url The plugin root URL.
165 * @param string $version The plugin version.
166 * @param bool $is_frontend Whether this is a frontend context.
167 *
168 * @since 4.0.0
169 */
170 \do_action( WP_2FA_PREFIX . 'profile_enqueue_assets', $plugin_url, $version, $is_frontend );
171 }
172
173 /**
174 * Render the profile section.
175 *
176 * This is the main entry point. It assembles the template data and includes
177 * the main template file.
178 *
179 * @param \WP_User $user The user whose profile is being displayed.
180 * @param array $additional_args Extra arguments (is_shortcode, show_preamble, options).
181 *
182 * @return void
183 */
184 public static function render( \WP_User $user, array $additional_args = array() ): void {
185
186 // Ensure we have something in the settings — but only bail if the plugin
187 // was never configured (no settings hash). If settings got wiped after
188 // initial configuration, the init() fallback restores them.
189 if ( empty( Settings_Utils::get_option( WP_2FA_POLICY_SETTINGS_NAME ) ) && ! Settings_Utils::get_option( WP_2FA_PREFIX . 'settings_hash' ) ) {
190 return;
191 }
192
193 $show_preamble = true;
194 if ( isset( $additional_args['show_preamble'] ) ) {
195 $show_preamble = \filter_var( $additional_args['show_preamble'], FILTER_VALIDATE_BOOLEAN );
196 }
197
198 $user_type = User_Utils::determine_user_2fa_status( $user );
199
200 // Orphan user override.
201 if ( in_array( 'orphan_user', $user_type, true ) ) {
202 $additional_args['is_shortcode'] = true;
203 }
204
205 $data = self::build_template_data( $user, $user_type, $additional_args, $show_preamble );
206
207 /**
208 * Filter: wp_2fa_profile_template_data
209 *
210 * Allows extensions to modify the template data before rendering.
211 *
212 * @param array $data The template data array.
213 * @param \WP_User $user The target user.
214 * @param array $user_type The user's 2FA status array.
215 *
216 * @since 4.0.0
217 */
218 $data = \apply_filters( WP_2FA_PREFIX . 'profile_template_data', $data, $user, $user_type );
219
220 $template_file = self::get_template_dir() . 'main.php';
221
222 /**
223 * Filter: wp_2fa_profile_template_file
224 *
225 * Allows overriding the main profile template file path.
226 *
227 * @param string $template_file Absolute path to the main template.
228 *
229 * @since 4.0.0
230 */
231 $template_file = \apply_filters( WP_2FA_PREFIX . 'profile_template_file', $template_file );
232
233 if ( \file_exists( $template_file ) ) {
234 include $template_file;
235 }
236 }
237
238 /**
239 * Build the complete template data array.
240 *
241 * Consolidates all the logic from the old user_2fa_options method.
242 *
243 * @param \WP_User $user The target user.
244 * @param array $user_type User 2FA status flags.
245 * @param array $additional_args Extra arguments.
246 * @param bool $show_preamble Whether to show heading/description.
247 *
248 * @return array<string,mixed>
249 */
250 private static function build_template_data( \WP_User $user, array $user_type, array $additional_args, bool $show_preamble ): array {
251
252 $description = WP2FA::get_wp2fa_white_label_setting( 'user-profile-form-preamble-desc', true );
253 $is_shortcode = isset( $additional_args['is_shortcode'] ) && $additional_args['is_shortcode'];
254 $viewing_own = in_array( 'viewing_own_profile', $user_type, true );
255 $is_admin = in_array( 'can_manage_options', $user_type, true );
256 $is_excluded = in_array( 'user_is_excluded', $user_type, true );
257
258 // Orphan user messages.
259 if ( in_array( 'orphan_user', $user_type, true ) ) {
260 if ( User_Utils::in_array_all( array( 'user_needs_to_setup_2fa', 'can_manage_options' ), $user_type ) ) {
261 $description = \esc_html__( 'This user is required to setup 2FA but has not yet done so.', 'wp-2fa' );
262 }
263 if ( User_Utils::in_array_all( array( 'user_is_excluded', 'can_manage_options' ), $user_type ) ) {
264 $description = \esc_html__( 'This user is excluded from configuring 2FA.', 'wp-2fa' );
265 }
266 }
267
268 // Excluded user: filter for extra content.
269 $excluded_content = '';
270 if ( $is_excluded || \current_user_can( 'manage_options' ) ) {
271 /** This filter is documented in class-user-profile.php */
272 $excluded_content = \esc_html__( 'This user is excluded from configuring 2FA.', 'wp-2fa' );
273 }
274
275 // Determine enabled methods and labels.
276 $enabled_method = User_Helper::get_enabled_method_for_user( $user );
277 $has_enabled_method = ! empty( $enabled_method );
278 $primary_label = \esc_html__( 'No enabled primary method', 'wp-2fa' );
279
280 if ( $has_enabled_method ) {
281 $translated_methods = Settings::get_providers_translate_names();
282 $primary_label = isset( $translated_methods[ $enabled_method ] ) ? $translated_methods[ $enabled_method ] : $primary_label;
283 }
284
285 // Backup methods.
286 $enabled_backup_methods = User_Helper::get_enabled_backup_methods_for_user( $user );
287 $backup_methods_label = \esc_html__( 'No enabled backup methods', 'wp-2fa' );
288 if ( ! empty( $enabled_backup_methods ) ) {
289 $backup_methods_label = \implode( ', ', $enabled_backup_methods );
290 }
291
292 // Show "Currently configured" summary?
293 $show_enabled = true;
294 if ( isset( $additional_args['options']['do_not_show_enabled'] ) && false !== (bool) $additional_args['options']['do_not_show_enabled'] ) {
295 $show_enabled = false;
296 }
297
298 // Build specific card data.
299 $setup_card_data = self::build_setup_card_data( $user, $user_type, $additional_args );
300 $backup_card_data = self::build_backup_card_data( $user, $user_type );
301 $admin_data = self::build_admin_actions_data( $user, $user_type );
302 $totp_data = self::build_totp_data( $user, $user_type, $enabled_method );
303
304 // Extra content from filters.
305 $form_content = '';
306 /** This filter is documented in class-user-profile.php */
307 $extra_content = \apply_filters( WP_2FA_PREFIX . 'append_to_profile_form_content', $form_content, $user );
308
309 // Show cards?
310 $show_setup_card = $setup_card_data['show_change_btn'] || $setup_card_data['show_configure_btn'] || $setup_card_data['show_remove_btn'];
311 $show_backup_card = $viewing_own && $has_enabled_method && ( $backup_card_data['show_generate_codes'] || ! empty( $backup_card_data['extra_buttons'] ) );
312
313 // Premium enforcement check.
314 if ( ! $has_enabled_method && class_exists( '\WP2FA\Extensions\Zero_Setup_Email\Zero_Setup_Email' ) && Zero_Setup_Email::is_enforced() ) {
315 User_Helper::run_user_enforcement_check( $user );
316 }
317
318 return array(
319 'user' => $user,
320 'user_id' => $user->ID,
321 'show_preamble' => $show_preamble,
322 'preamble_title' => WP2FA::get_wp2fa_white_label_setting( 'user-profile-form-preamble-title', true ),
323 'preamble_desc' => $description,
324 'is_excluded' => $is_excluded,
325 'excluded_content' => $excluded_content,
326 'has_enabled_methods' => $has_enabled_method,
327 'primary_label' => $primary_label,
328 'backup_methods_label' => $backup_methods_label,
329 'show_enabled' => $show_enabled,
330 'show_setup_card' => $show_setup_card,
331 'show_backup_card' => $show_backup_card,
332 'show_admin_actions' => ! empty( $admin_data['reset_url'] ) || ! empty( $admin_data['temp_remove_url'] ) || ! empty( $admin_data['unlock_url'] ),
333 'setup_card_data' => $setup_card_data,
334 'backup_card_data' => $backup_card_data,
335 'admin_actions_data' => $admin_data,
336 'totp_data' => $totp_data,
337 'extra_content' => $extra_content,
338 'user_type' => $user_type,
339 'styling_class' => ( empty( WP2FA::get_wp2fa_white_label_setting( 'enable_wizard_styling' ) ) ) ? 'default_styling' : 'enable_styling',
340 );
341 }
342
343 /**
344 * Build data for the 2FA Setup card.
345 *
346 * @param \WP_User $user The target user.
347 * @param array $user_type User 2FA status flags.
348 * @param array $additional_args Extra arguments.
349 *
350 * @return array
351 */
352 private static function build_setup_card_data( \WP_User $user, array $user_type, array $additional_args ): array {
353 $show_change_btn = false;
354 $show_configure_btn = false;
355 $show_remove_btn = false;
356 $change_label = \esc_html__( 'Change 2FA Settings', 'wp-2fa' );
357 $configure_label = \esc_html__( 'Configure 2FA', 'wp-2fa' );
358
359 $viewing_own = in_array( 'viewing_own_profile', $user_type, true );
360
361 /** This filter is documented in class-user-profile.php */
362 $show_enable2fa = \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting', true, $user->ID );
363
364 /** This filter is documented in class-user-profile.php */
365 \apply_filters( WP_2FA_PREFIX . 'enable_2fa_user_setting_description', '' );
366
367 // Premium: license/enforcement overrides.
368 // When quota is exceeded or no license is present, fall back to free
369 // version behaviour — all users can still configure free methods.
370 if ( class_exists( '\WP2FA\Extensions_Loader' ) && Extensions_Loader::use_proxytron() ) {
371 if ( class_exists( '\WP2FA\Freemius\User_Licensing' ) && User_Licensing::quota_check() ) {
372 $show_enable2fa = true;
373 }
374 }
375
376 if ( $viewing_own ) {
377 // User has an enabled method - show "Change" and optionally "Remove".
378 if (
379 User_Utils::in_array_all( array( 'has_enabled_methods' ), $user_type ) ||
380 User_Utils::in_array_all( array( 'no_required_has_enabled' ), $user_type )
381 ) {
382 if ( $show_enable2fa ) {
383 $show_change_btn = true;
384 }
385 if ( User_Profile::can_user_remove_2fa( $user->ID ) ) {
386 $show_remove_btn = true;
387 }
388 }
389
390 // User needs to setup 2FA - show "Configure".
391 $show_if_user_is_not_in = array(
392 'user_is_excluded',
393 'has_enabled_methods',
394 'no_required_has_enabled',
395 );
396
397 if (
398 User_Utils::in_array_all( array( 'user_needs_to_setup_2fa' ), $user_type ) ||
399 User_Utils::role_is_not( $show_if_user_is_not_in, $user_type )
400 ) {
401 if ( $show_enable2fa ) {
402 $show_configure_btn = true;
403 }
404 }
405 }
406
407 return array(
408 'show_change_btn' => $show_change_btn,
409 'show_configure_btn' => $show_configure_btn,
410 'show_remove_btn' => $show_remove_btn,
411 'change_label' => $change_label,
412 'configure_label' => $configure_label,
413 );
414 }
415
416 /**
417 * Build data for the Backup 2FA Method card.
418 *
419 * @param \WP_User $user The target user.
420 * @param array $user_type User 2FA status flags.
421 *
422 * @return array
423 */
424 private static function build_backup_card_data( \WP_User $user, array $user_type ): array {
425 $show_generate_codes = false;
426 $codes_remaining = -1;
427 $backup_codes_nonce = '';
428 $extra_buttons = '';
429
430 $role = User_Helper::get_user_role( $user );
431
432 if ( class_exists( '\WP2FA\Methods\Backup_Codes' ) && Backup_Codes::are_backup_codes_enabled_for_role( $role ) ) {
433 $show_generate_codes = true;
434 $codes_remaining = Backup_Codes::codes_remaining_for_user( $user );
435 $backup_codes_nonce = \wp_create_nonce( 'wp-2fa-backup-codes-generate-json-' . $user->ID );
436 }
437
438 /**
439 * Add an option for external providers to add their own form buttons.
440 *
441 * @since 2.0.0
442 */
443 $extra_buttons = \apply_filters( WP_2FA_PREFIX . 'additional_form_buttons', $extra_buttons );
444
445 return array(
446 'show_generate_codes' => $show_generate_codes,
447 'codes_remaining' => $codes_remaining,
448 'backup_codes_nonce' => $backup_codes_nonce,
449 'extra_buttons' => $extra_buttons,
450 );
451 }
452
453 /**
454 * Build data for admin-only actions section.
455 *
456 * @param \WP_User $user The target user.
457 * @param array $user_type User 2FA status flags.
458 *
459 * @return array
460 */
461 private static function build_admin_actions_data( \WP_User $user, array $user_type ): array {
462 $reset_url = '';
463 $temp_remove_url = '';
464 $unlock_url = '';
465 $description = '';
466
467 $viewing_own = in_array( 'viewing_own_profile', $user_type, true );
468 $is_admin = in_array( 'can_manage_options', $user_type, true );
469
470 // Admin viewing another user's profile with 2FA configured.
471 if ( $is_admin && in_array( 'has_enabled_methods', $user_type, true ) && ! $viewing_own ) {
472 $description = \esc_html__( 'The user has already configured 2FA. When you reset the user\'s current 2FA configuration, the user can log back in with just the username and password.', 'wp-2fa' );
473
474 $reset_url = \add_query_arg(
475 array(
476 'action' => 'remove_user_2fa',
477 'user_id' => $user->ID,
478 'wp_2fa_nonce' => \wp_create_nonce( 'wp-2fa-remove-user-2fa-nonce' ),
479 'admin_reset' => 'yes',
480 ),
481 \admin_url( 'user-edit.php' )
482 );
483 }
484
485 // Premium: temporary removal.
486 if ( class_exists( '\WP2FA\Admin\Helpers\User_Helper' ) ) {
487 if ( method_exists( '\WP2FA\Admin\Helpers\User_Helper', 'is_2fa_for_user_temporary_removed' ) && ! User_Helper::is_2fa_for_user_temporary_removed() && ! $viewing_own && ! empty( User_Helper::get_enabled_method_for_user( $user ) ) ) {
488 $temp_remove_url = \wp_nonce_url( 'users.php?action=remove-2fa-temporary&users=' . $user->ID, 'bulk-users' );
489 }
490 }
491
492 // Admin viewing user whose grace period has expired.
493 if ( User_Utils::in_array_all( array( 'can_manage_options', 'grace_has_expired' ), $user_type ) ) {
494 $unlock_url = \add_query_arg(
495 array(
496 'action' => 'unlock_account',
497 'user_id' => $user->ID,
498 'wp_2fa_nonce' => \wp_create_nonce( 'wp-2fa-unlock-account-nonce' ),
499 ),
500 \admin_url( 'user-edit.php' )
501 );
502 }
503
504 return array(
505 'description' => $description,
506 'reset_url' => $reset_url,
507 'temp_remove_url' => $temp_remove_url,
508 'unlock_url' => $unlock_url,
509 );
510 }
511
512 /**
513 * Build TOTP-specific data (QR code, key).
514 *
515 * @param \WP_User $user The target user.
516 * @param array $user_type User 2FA status flags.
517 * @param string $enabled_method The user's currently enabled method.
518 *
519 * @return array
520 */
521 private static function build_totp_data( \WP_User $user, array $user_type, string $enabled_method ): array {
522 $viewing_own = in_array( 'viewing_own_profile', $user_type, true );
523 $is_admin = in_array( 'can_manage_options', $user_type, true );
524 $has_enabled = in_array( 'has_enabled_methods', $user_type, true );
525
526 if ( ( $viewing_own || $is_admin ) && $has_enabled && class_exists( '\WP2FA\Methods\TOTP' ) && TOTP::METHOD_NAME === $enabled_method ) {
527 // Ensure TOTP methods resolve against the target user (important when admin views another user).
528 User_Helper::set_user( $user );
529
530 return array(
531 'show_qr' => true,
532 'qr_code_url' => TOTP::get_qr_code(),
533 'totp_key' => TOTP::get_totp_decrypted(),
534 );
535 }
536
537 return array(
538 'show_qr' => false,
539 );
540 }
541 }
542 }
543