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 / Helpers / class-wp-helper.php
wp-2fa / includes / classes / Admin / Helpers Last commit date
class-ajax-helper.php 1 day ago class-classes-helper.php 1 day ago class-email-templates.php 1 day ago class-file-writer.php 1 day ago class-methods-helper.php 1 day ago class-php-helper.php 1 day ago class-sms-templates.php 1 day ago class-user-helper.php 1 day ago class-wp-helper.php 1 day ago index.php 1 day ago
class-wp-helper.php
1097 lines
1 <?php
2 /**
3 * Responsible for the WP core functionalities.
4 *
5 * @package wp2fa
6 * @subpackage helpers
7 *
8 * @since 2.2.0
9 *
10 * @copyright 2026 Melapress
11 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
12 *
13 * @see https://wordpress.org/plugins/wp-2fa/
14 */
15
16 namespace WP2FA\Admin\Helpers;
17
18 use WP2FA\Utils\Settings_Utils;
19 use WP2FA\Utils\Abstract_Migration;
20 use WP2FA\Admin\Plugin_Updated_Notice;
21
22 defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
23
24 /*
25 * WP helper class
26 */
27 if ( ! class_exists( '\WP2FA\Admin\Helpers\WP_Helper' ) ) {
28 /**
29 * All the WP functionality must go trough this class.
30 *
31 * @since 2.2.0
32 */
33 class WP_Helper {
34
35 public const PLUGIN_PAGES = array(
36 'wp-2fa_page_wp-2fa-settings',
37 'wp-2fa_page_wp-2fa-settings-network',
38 'wp-2fa_page_wp-2fa-settings-new',
39 'wp-2fa_page_wp-2fa-settings-new-network',
40 'toplevel_page_wp-2fa-policies',
41 'toplevel_page_wp-2fa-policies-network',
42 'wp-2fa_page_wp-2fa-reports',
43 'wp-2fa_page_wp-2fa-reports-network',
44 'wp-2fa_page_wp-2fa-reports-locked',
45 'wp-2fa_page_wp-2fa-reports-locked-network',
46 'wp-2fa_page_wp-2fa-help-contact-us',
47 'wp-2fa_page_wp-2fa-help-contact-us-network',
48 'wp-2fa_page_wp-2fa-premium-features',
49 'wp-2fa_page_wp-2fa-premium-features-network',
50 'wp-2fa_page_wp-2fa-policies-account',
51 'wp-2fa_page_wp-2fa-policies-account-network',
52 'wp-2fa_page_wp-2fa-passkeys',
53 'wp-2fa_page_wp-2fa-passkeys-network',
54 'wp-2fa_page_wp-2fa-white-labeling',
55 'wp-2fa_page_wp-2fa-white-labeling-network',
56 'wp-2fa_page_wp-2fa-about-us',
57 'wp-2fa_page_wp-2fa-about-us-network',
58 );
59
60 /**
61 * Hold the user roles as array - Human readable is used for key of the array, and the internal role name is the value.
62 *
63 * @var array
64 *
65 * @since 2.2.0
66 */
67 private static $user_roles = array();
68
69 /**
70 * Hold the user roles as array - Internal role name is used for key of the array, and the human readable format is the value.
71 *
72 * @var array
73 *
74 * @since 2.2.0
75 */
76 private static $user_roles_wp = array();
77
78 /**
79 * Keeps the value of the multisite install of the WP.
80 *
81 * @var bool
82 *
83 * @since 2.2.0
84 */
85 private static $is_multisite = null;
86
87 /**
88 * Holds array with all the sites in multisite WP installation.
89 *
90 * @var array
91 */
92 private static $sites = array();
93
94 /**
95 * Inits the class, and fires all the necessarily methods.
96 *
97 * @return void
98 *
99 * @since 2.2.0
100 */
101 public static function init() {
102 // @free:start
103 $today_date = gmdate( 'Y-m-d' );
104 $today_date = gmdate( 'Y-m-d', strtotime( $today_date ) );
105
106 $event_date_begin = gmdate( 'Y-m-d', strtotime( '11/21/2025' ) );
107 $event_date_end = gmdate( 'Y-m-d', strtotime( '12/01/2025' ) );
108
109 $event_ending_date = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_end_date', false );
110
111 $extra_event_banner_dismissed = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed', false );
112 $extra_event_banner_super_dismissed = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_super_dismissed', false );
113
114 if ( gmdate( 'Y-m-d', strtotime( '11/28/2025' ) ) === $today_date && $extra_event_banner_dismissed && ! $extra_event_banner_super_dismissed ) {
115 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed' );
116 }
117
118 if ( ( $today_date >= $event_date_begin ) && ( $today_date <= $event_date_end ) && ( false === $event_ending_date || strtotime( $event_ending_date ) < strtotime( $today_date ) ) ) {
119 $extra_event_banner_dismissed = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed', false );
120 if ( ! $extra_event_banner_dismissed ) {
121 \update_site_option( WP_2FA_PREFIX . '_extra_event_banner', true );
122 \update_site_option( WP_2FA_PREFIX . '_extra_event_banner_end_date', strtotime( $event_date_end ) );
123 \update_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed', false );
124 }
125 } else {
126 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner' );
127 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_end_date' );
128 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed' );
129 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_super_dismissed' );
130 }
131 // @free:end
132
133 // @free:start
134 $install_date = \get_site_option( WP_2FA_PREFIX . '_install_date', false );
135 if ( ! $install_date ) {
136 \update_site_option( WP_2FA_PREFIX . '_install_date', time() );
137 }
138 // @free:end
139
140 if ( self::is_multisite() ) {
141 \add_action( 'network_admin_notices', array( __CLASS__, 'show_critical_admin_notice' ) );
142 // @free:start
143 \add_action( 'network_admin_notices', array( __CLASS__, 'show_2025_security_survey_admin_notice' ), 20 );
144 // \add_action( 'network_admin_notices', array( __CLASS__, 'show_survey_banner' ), 20 );
145 // @free:end
146 } else {
147 \add_action( 'admin_notices', array( __CLASS__, 'show_critical_admin_notice' ) );
148 // @free:start
149 \add_action( 'admin_notices', array( __CLASS__, 'show_2025_security_survey_admin_notice' ), 20 );
150 // \add_action( 'admin_notices', array( __CLASS__, 'show_survey_banner' ), 20 );
151 // @free:end
152 }
153 }
154
155 /**
156 * Sets the ajax hooks for the plugin.
157 *
158 * @return void
159 *
160 * @since 4.0.0
161 */
162 public static function set_ajax_hooks() {
163 // @free:start
164 \add_action( 'wp_ajax_wp_2fa_dismiss_extra_event_banner', array( __CLASS__, 'dismiss_extra_event_banner' ) );
165 \add_action( 'wp_ajax_wp_2fa_dismiss_survey_banner', array( __CLASS__, 'dismiss_survey_banner' ) );
166 \add_action( 'wp_ajax_wp_2fa_take_survey', array( __CLASS__, 'survey_taken' ) );
167 // @free:end
168
169 \add_action( 'wp_ajax_dismiss_survey_notice', array( __CLASS__, 'dismiss_survey_notice' ) );
170 }
171
172 /**
173 * Checks if specific role exists.
174 *
175 * @param string $role - The name of the role to check.
176 *
177 * @since 2.2.0
178 */
179 public static function is_role_exists( string $role ): bool {
180 self::set_roles();
181
182 if ( in_array( $role, self::$user_roles, true ) ) {
183 return true;
184 }
185
186 return false;
187 }
188
189 /**
190 * Returns the currently available WP roles - the Human readable format is the key.
191 *
192 * @return array
193 *
194 * @since 2.2.0
195 */
196 public static function get_roles() {
197 self::set_roles();
198
199 return self::$user_roles;
200 }
201
202 /**
203 * Returns the currently available WP roles.
204 *
205 * @return array
206 *
207 * @since 2.2.0
208 */
209 public static function get_roles_wp() {
210 if ( empty( self::$user_roles_wp ) ) {
211 self::set_roles();
212 self::$user_roles_wp = array_flip( self::$user_roles );
213 }
214
215 return self::$user_roles_wp;
216 }
217
218 /**
219 * Shows critical notices to the admin.
220 *
221 * @return void
222 *
223 * @since 2.2.0
224 */
225 public static function show_critical_admin_notice() {
226 if ( User_Helper::is_admin() ) {
227 /*
228 * Gives the ability to show notices to the admins
229 */
230 \do_action( WP_2FA_PREFIX . 'critical_notice' );
231 }
232 }
233
234 /**
235 * Shows critical notices to the admin.
236 *
237 * @return void
238 *
239 * @since 2.2.0
240 */
241 public static function show_2025_security_survey_admin_notice() {
242 $screen = \get_current_screen();
243 $show_extra_event_banner = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner', false );
244 $extra_event_banner_dismissed = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed', false );
245
246 if ( $show_extra_event_banner ) {
247 $event_ending_date = \get_site_option( WP_2FA_PREFIX . '_extra_event_banner_end_date', false );
248 if ( $event_ending_date && ( \time() > (int) ( $event_ending_date ) ) ) {
249 $show_extra_event_banner = false;
250 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner' );
251 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_end_date' );
252 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed' );
253 }
254 }
255 if ( in_array( $screen->base, self::PLUGIN_PAGES, true ) && $show_extra_event_banner && ! $extra_event_banner_dismissed ) {
256 \remove_action( 'admin_notices', array( Plugin_Updated_Notice::class, 'plugin_update_banner' ), 30 );
257 \remove_action( 'network_admin_notices', array( Plugin_Updated_Notice::class, 'plugin_update_banner' ), 30 );
258 ?>
259 <!-- Copy START -->
260 <div class="black-friday wp-2fa-extra-event-banner" style="margin-top: 20px; margin-right: 20px;">
261 <!-- SVG Icon on the Left -->
262 <img class="black-friday-svg" src="<?php echo esc_url( WP_2FA_URL . 'dist/images/upgrade-plugin-icon.svg' ); ?>" alt="Premium Plugin" width="113" height="101">
263
264 <!-- Text Content -->
265 <div class="black-friday-content">
266 <h2 class="black-friday-title"><?php \esc_html_e( 'Upgrade to Premium', 'wp-2fa' ); ?><br>
267 <span class="bf-title-line-2"><span class="bf-underline"><?php \esc_html_e( 'Black Friday', 'wp-2fa' ); ?></span> <?php \esc_html_e( ' Sale Now Live!', 'wp-2fa' ); ?></span>
268 </h2>
269 <a href="https://melapress.com/black-friday-cyber-monday/?utm_source=plugin&utm_medium=wp2fa&utm_campaign=BFCM2025" target="_blank" class="bf-cta-link"><?php \esc_html_e( 'Get Offer Now', 'wp-2fa' ); ?></a>
270 </div>
271
272 <!-- Close Button -->
273 <button aria-label="Close button" class="wp-2fa-extra-event-banner-close black-friday-close" data-dismiss-nonce="<?php echo \esc_attr( \wp_create_nonce( 'wp_2fa_dismiss_extra_event_banner_nonce' ) ); ?>"></button>
274 </div>
275 <!-- Copy END -->
276
277 <script type="text/javascript">
278 jQuery(document).ready(function( $ ) {
279 jQuery( 'body' ).on( 'click', '.wp-2fa-extra-event-banner-close', function ( e ) {
280 var nonce = jQuery( '.wp-2fa-extra-event-banner [data-dismiss-nonce]' ).attr( 'data-dismiss-nonce' );
281
282 jQuery.ajax({
283 type: 'POST',
284 url: '<?php echo \esc_url( \admin_url( 'admin-ajax.php' ) ); ?>',
285 async: true,
286 data: {
287 action: 'wp_2fa_dismiss_extra_event_banner',
288 nonce : nonce,
289 },
290 success: function ( result ) {
291 jQuery( '.wp-2fa-extra-event-banner' ).slideUp( 300 );
292 }
293 });
294 });
295 });
296 </script>
297 <style>
298 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&family=Quicksand:wght@600;700&display=swap');
299
300 :root {
301 --color-coral: #FF8977;
302 --color-deep: #020E26;
303 --color-pale-blue: #D9E4FD;
304 --color-light-blue: #8AAAF1;
305 --color-wp-2fa-maroon: #7A262A;
306 --color-wp-2fa-red: #DD2B10;
307 --ease-out-expo: cubic-bezier(0.32, 1, 0.3, 1);
308 --ease-out-back: cubic-bezier(0.64, 0.69, 0.1, 1);
309 }
310
311 /* ==================== Black Friday Banner ==================== */
312 .black-friday {
313 background-color: var(--color-deep);
314 font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
315 -webkit-font-smoothing: subpixel-antialiased;
316 color: #fff;
317 display: flex;
318 align-items: center;
319 padding: 1.66rem;
320 position: relative;
321 overflow: hidden;
322 transition: all 0.2s ease-in-out;
323 border: none;
324 border-left: 4px solid var(--color-coral);
325 gap: 1.5rem; /* Space between SVG and text */
326 }
327
328 .black-friday-svg {
329 flex-shrink: 0;
330 width: 134px;
331 height: 101px;
332 z-index: 1;
333 margin-right: 32px;
334 }
335
336 .black-friday-content {
337 max-width: 45%;
338 z-index: 1;
339 }
340
341 .black-friday-title {
342 font-family: Inter, sans-serif;
343 font-size: 2.2em;
344 letter-spacing: .5px;
345 text-transform: uppercase;
346 color: var(--color-coral);
347 line-height: .7em;
348 font-weight: 900;
349 margin-bottom: 4px;
350 }
351
352 .bf-title-line-2 {
353 color: #fff;
354 font-size: .725em;
355 }
356
357 .bf-underline {
358 text-decoration: underline;
359 }
360
361 .black-friday-text {
362 margin: .25rem 0 0;
363 font-size: 13px;
364 line-height: 1.3125rem;
365 }
366
367 .bf-link {
368 color: #fff;
369 font-weight: 400;
370 text-decoration: underline;
371 font-size: 0.875rem;
372 padding: 0.675rem 1.3rem .7rem 0;
373 transition: all 0.2s ease-in-out;
374 display: inline-block;
375 margin: .5rem 0 0;
376 }
377
378 .bf-link:hover {
379 color: #D9E4FD;
380 }
381
382 .bf-cta-link {
383 border-radius: 0.25rem;
384 background: #D9E4FD;
385 color: #454BF7;
386 font-weight: bold;
387 text-decoration: none;
388 font-size: 0.875rem;
389 padding: 0.675rem 1.3rem .7rem 1.3rem;
390 transition: all 0.2s ease-in-out;
391 display: inline-block;
392 margin: .5rem 0 0;
393 }
394
395 .bf-cta-link:hover {
396 background: #454BF7;
397 color: #D9E4FD;
398 }
399
400 .black-friday-close {
401 background-image: url('<?php echo esc_url( WP_2FA_URL . 'dist/images/close-icon-reverse.svg' ); ?>');
402 background-size: cover;
403 width: 12px;
404 height: 12px;
405 border: none;
406 cursor: pointer;
407 position: absolute;
408 top: 20px;
409 right: 20px;
410 background-color: transparent;
411 z-index: 1;
412 }
413
414 .black-friday {
415 background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 182 127"><path d="M181.413 -165.636L134.391 234.514L0 234.514L2.19345e-05 -165.636L181.413 -165.636Z" fill="%23B6C3F2"/></svg>');
416 background-repeat: no-repeat;
417 background-position: -20px 0;
418 }
419
420 /* Z-index for layered elements */
421 .plugin-update-content,
422 .wp-2fa-svg,
423 .plugin-update-close,
424 .black-friday-content,
425 .black-friday-svg,
426 .black-friday-close {
427 z-index: 1;
428 }
429
430 /* ==================== Responsive Design ==================== */
431 @media (max-width: 1200px) {
432 .plugin-update,
433 .black-friday {
434 background-image: none;
435 flex-direction: column;
436 text-align: center;
437 gap: 1rem;
438 }
439
440 .wp-2fa-svg,
441 .black-friday-svg {
442 width: 90px;
443 height: 80px;
444 margin: 0;
445 }
446
447 .plugin-update-content,
448 .black-friday-content {
449 max-width: 100%;
450 }
451 }
452 </style>
453
454 <?php
455 }
456 }
457
458 /**
459 * Handle event banner dismissal.
460 *
461 * @return void
462 *
463 * @since 3.1.0
464 */
465 public static function dismiss_extra_event_banner() {
466 // Grab POSTed data.
467 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : false;
468
469 // Check nonce.
470 if ( ! \current_user_can( 'manage_options' ) || empty( $nonce ) || ! $nonce || ! \wp_verify_nonce( $nonce, 'wp_2fa_dismiss_extra_event_banner_nonce' ) ) {
471 \wp_send_json_error( \esc_html__( 'Nonce Verification Failed.', 'wp-2fa' ) );
472 }
473
474 \delete_site_option( WP_2FA_PREFIX . '_extra_event_banner' );
475 \update_site_option( WP_2FA_PREFIX . '_extra_event_banner_dismissed', 'yes' );
476
477 $today_date = gmdate( 'Y-m-d' );
478 $today_date = gmdate( 'Y-m-d', strtotime( $today_date ) );
479
480 if ( gmdate( 'Y-m-d', strtotime( '11/28/2025' ) ) === $today_date ) {
481 \update_site_option( WP_2FA_PREFIX . '_extra_event_banner_super_dismissed', 'yes' );
482 }
483
484 \wp_send_json_success( \esc_html__( 'Complete.', 'wp-2fa' ) );
485 }
486
487 /**
488 * Shows survey banner to the admin.
489 *
490 * @return void
491 *
492 * @since 3.1.0
493 */
494 public static function show_survey_banner() {
495 $screen = \get_current_screen();
496
497 // Permanently hidden once the user has actually taken the survey.
498 if ( \get_site_option( WP_2FA_PREFIX . '_survey_taken', false ) ) {
499 return;
500 }
501
502 $survey_banner_dismissed = \get_site_option( WP_2FA_PREFIX . '_survey_banner_dismissed', false );
503 if ( $survey_banner_dismissed ) {
504 return;
505 }
506
507 $is_upgrade = false;
508 if ( in_array( $screen->base, self::PLUGIN_PAGES, true ) && Settings_Utils::get_option( Abstract_Migration::UPGRADE_NOTICE, false ) ) {
509 $is_upgrade = true;
510 }
511
512 $install_date = \get_site_option( WP_2FA_PREFIX . '_install_date', time() );
513 $two_weeks_ago = time() - ( 14 * 24 * 60 * 60 );
514
515 if ( ! $is_upgrade && $install_date > $two_weeks_ago ) {
516 return;
517 }
518
519 if ( in_array( $screen->base, self::PLUGIN_PAGES, true ) ) {
520 \remove_action( 'admin_notices', array( Plugin_Updated_Notice::class, 'plugin_update_banner' ), 30 );
521 \remove_action( 'network_admin_notices', array( Plugin_Updated_Notice::class, 'plugin_update_banner' ), 30 );
522 ?>
523 <!-- Survey Banner START -->
524 <div class="survey-banner wp-2fa-survey-banner" style="margin-top: 20px; margin-right: 20px;">
525 <!-- Text Content -->
526 <div class="survey-banner-content">
527 <div class="survey-banner-title"><?php \esc_html_e( 'Got 2 minutes? Help us shape the future of WP 2FA', 'wp-2fa' ); ?></div>
528 <a href="https://getformly.app/ThUFdP" target="_blank" class="survey-cta-link wp-2fa-survey-cta-link" data-take-nonce="<?php echo \esc_attr( \wp_create_nonce( 'wp_2fa_take_survey_nonce' ) ); ?>"><?php \esc_html_e( 'Take the survey', 'wp-2fa' ); ?></a>
529 </div>
530
531 <!-- Close Button -->
532 <button aria-label="Close button" class="wp-2fa-survey-banner-close survey-banner-close" data-dismiss-nonce="<?php echo \esc_attr( \wp_create_nonce( 'wp_2fa_dismiss_survey_banner_nonce' ) ); ?>"></button>
533 </div>
534 <!-- Survey Banner END -->
535
536 <script type="text/javascript">
537 jQuery(document).ready(function( $ ) {
538 jQuery( 'body' ).on( 'click', '.wp-2fa-survey-banner-close', function ( e ) {
539 var nonce = jQuery( '.wp-2fa-survey-banner [data-dismiss-nonce]' ).attr( 'data-dismiss-nonce' );
540
541 jQuery.ajax({
542 type: 'POST',
543 url: '<?php echo \esc_url( \admin_url( 'admin-ajax.php' ) ); ?>',
544 async: true,
545 data: {
546 action: 'wp_2fa_dismiss_survey_banner',
547 nonce : nonce,
548 },
549 success: function ( result ) {
550 jQuery( '.wp-2fa-survey-banner' ).slideUp( 300 );
551 }
552 });
553 });
554
555 // When the user actually takes the survey, permanently hide it.
556 jQuery( 'body' ).on( 'click', '.wp-2fa-survey-cta-link', function () {
557 var nonce = jQuery( this ).attr( 'data-take-nonce' );
558
559 jQuery.ajax({
560 type: 'POST',
561 url: '<?php echo \esc_url( \admin_url( 'admin-ajax.php' ) ); ?>',
562 async: true,
563 data: {
564 action: 'wp_2fa_take_survey',
565 nonce : nonce,
566 },
567 success: function () {
568 jQuery( '.wp-2fa-survey-banner' ).slideUp( 300 );
569 }
570 });
571 });
572 });
573 </script>
574 <style>
575 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&family=Quicksand:wght@600;700&display=swap');
576
577 :root {
578 --color-coral: #FF8977;
579 --color-deep: #020E26;
580 --color-pale-blue: #D9E4FD;
581 --color-light-blue: #8AAAF1;
582 --color-wp-2fa-maroon: #7A262A;
583 --color-wp-2fa-red: #DD2B10;
584 --ease-out-expo: cubic-bezier(0.32, 1, 0.3, 1);
585 --ease-out-back: cubic-bezier(0.64, 0.69, 0.1, 1);
586 }
587
588 /* ==================== Survey Banner ==================== */
589 .survey-banner {
590 /* background-color: var(--color-deep);
591 font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
592 -webkit-font-smoothing: subpixel-antialiased;
593 color: #fff; */
594 display: flex;
595 /* align-items: center; */
596 padding: 1rem;
597 position: relative;
598 /*overflow: hidden; */
599 transition: all 0.2s ease-in-out;
600 border: none;
601 border-left: 4px solid var(--color-coral);
602 border-bottom: 1px solid #ccc;
603 border-right: 1px solid #ccc;
604 border-top: 1px solid #ccc;
605 background: white;
606 /* gap: 1.5rem;
607 justify-content: center; */
608 }
609
610 .survey-banner-content {
611 /* max-width: 100%;
612 z-index: 1;
613 text-align: center; */
614 }
615
616 .survey-banner-title {
617 /* font-family: Inter, sans-serif;
618 font-size: 1.5em;
619 letter-spacing: .5px;
620 color: #fff; */
621 line-height: 1.2em;
622 font-weight: 700;
623 margin-bottom: 1rem;
624 }
625
626 .survey-cta-link {
627 border-radius: 0.25rem;
628 background: #D9E4FD;
629 color: #454BF7;
630 font-weight: bold;
631 text-decoration: none;
632 font-size: 0.875rem;
633 padding: 0.3rem 1rem .3rem 1rem;
634 transition: all 0.2s ease-in-out;
635 display: inline-block;
636 }
637
638 /* .survey-cta-link:hover {
639 background: #454BF7;
640 color: #D9E4FD;
641 } */
642
643 .survey-banner-close {
644 /* background-image: url('<?php echo esc_url( WP_2FA_URL . 'dist/images/close-icon.svg' ); ?>'); */
645 /* background-size: 12px; */
646 /* background-repeat: no-repeat; */
647 /* background-position: center; */
648 /* width: 20px; */
649 /* height: 20px; */
650 border: none;
651 /* border-radius: 50%; */
652 /* background-color: #ccc; */
653 cursor: pointer;
654 position: absolute;
655 top: 5px;
656 right: 5px;
657 z-index: 1;
658 background-color: transparent;
659 }
660 .survey-banner-close:before {
661 content: "\f153"/'';
662 font: normal 16px/20px dashicons;
663 }
664
665 /* ==================== Responsive Design ==================== */
666 @media (max-width: 1200px) {
667 .survey-banner {
668 flex-direction: column;
669 text-align: center;
670 gap: 1rem;
671 }
672 }
673 </style>
674
675 <?php
676 }
677 }
678
679 /**
680 * Handles the case where the user actually takes the survey.
681 * Sets a permanent option so the banner never appears again, regardless of plugin updates.
682 *
683 * @return void
684 *
685 * @since 3.1.0
686 */
687 public static function survey_taken() {
688 // Grab POSTed data.
689 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : false;
690
691 // Check nonce.
692 if ( ! \current_user_can( 'manage_options' ) || empty( $nonce ) || ! $nonce || ! \wp_verify_nonce( $nonce, 'wp_2fa_take_survey_nonce' ) ) {
693 \wp_send_json_error( \esc_html__( 'Nonce Verification Failed.', 'wp-2fa' ) );
694 }
695
696 \update_site_option( WP_2FA_PREFIX . '_survey_taken', 'yes' );
697 // Also set the regular dismissed flag so no edge-case shows the banner before next load.
698 \update_site_option( WP_2FA_PREFIX . '_survey_banner_dismissed', 'yes' );
699
700 \wp_send_json_success( \esc_html__( 'Complete.', 'wp-2fa' ) );
701 }
702
703 /**
704 * Handle survey banner dismissal.
705 *
706 * @return void
707 *
708 * @since 3.1.0
709 */
710 public static function dismiss_survey_banner() {
711 // Grab POSTed data.
712 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : false;
713
714 // Check nonce.
715 if ( ! \current_user_can( 'manage_options' ) || empty( $nonce ) || ! $nonce || ! \wp_verify_nonce( $nonce, 'wp_2fa_dismiss_survey_banner_nonce' ) ) {
716 \wp_send_json_error( \esc_html__( 'Nonce Verification Failed.', 'wp-2fa' ) );
717 }
718
719 \update_site_option( WP_2FA_PREFIX . '_survey_banner_dismissed', 'yes' );
720
721 \wp_send_json_success( \esc_html__( 'Complete.', 'wp-2fa' ) );
722 }
723
724 /**
725 * Handle notice dismissal.
726 *
727 * @since 3.0.0
728 *
729 * @return void
730 */
731 public static function dismiss_survey_notice() {
732 // Grab POSTed data.
733 $nonce_check = \check_ajax_referer( 'wp_2fa_dismiss_survey_notice_nonce', 'nonce' );
734
735 if ( ! $nonce_check ) {
736 \wp_send_json_error( esc_html__( 'Nonce Verification Failed.', 'wp-2fa' ) );
737 }
738 // Check nonce.
739 if ( ! \current_user_can( 'manage_options' ) ) {
740 \wp_send_json_error( esc_html__( 'Not enough privileges.', 'wp-2fa' ) );
741 }
742
743 Settings_Utils::update_option( 'wp_2fa_survey_notice_needed', 0 );
744
745 \wp_send_json_success( \esc_html__( 'Complete.', 'wp-2fa' ) );
746 }
747
748 /**
749 * Check is this is a multisite setup.
750 *
751 * @return bool
752 *
753 * @since 2.2.0
754 */
755 public static function is_multisite() {
756 if ( null === self::$is_multisite ) {
757 self::$is_multisite = function_exists( 'is_multisite' ) && is_multisite();
758 }
759
760 return self::$is_multisite;
761 }
762
763 /**
764 * Collects all the sites from multisite WP installation.
765 *
766 * @since 2.5.0
767 */
768 public static function get_multi_sites(): array {
769 if ( self::is_multisite() ) {
770 if ( empty( self::$sites ) ) {
771 self::$sites = self::get_sites();
772 }
773
774 return self::$sites;
775 }
776
777 return array();
778 }
779
780 /**
781 * Query sites from WPDB.
782 *
783 * @since 3.0.0
784 *
785 * @param int|null $limit — Maximum number of sites to return (null = no limit).
786 *
787 * @return object — Object with keys: blog_id, blogname, domain
788 */
789 public static function get_sites( $limit = null ) {
790 if ( self::is_multisite() ) {
791 global $wpdb;
792 // Build query.
793 if ( ! is_null( $limit ) ) {
794 $sql = $wpdb->prepare( 'SELECT blog_id, domain FROM ' . $wpdb->blogs . ' LIMIT %d', (int) $limit );
795 } else {
796 $sql = 'SELECT blog_id, domain FROM ' . $wpdb->blogs;
797 }
798
799 // Execute query.
800 $res = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
801
802 // Modify result.
803 foreach ( $res as $row ) {
804 $row->blogname = \esc_html( \get_blog_option( $row->blog_id, 'blogname' ) );
805 }
806 } else {
807 $res = new \stdClass();
808 $res->blog_id = \get_current_blog_id();
809 $res->blogname = \esc_html( \get_bloginfo( 'name' ) );
810 $res = array( $res );
811 }
812
813 // Return result.
814 return $res;
815 }
816
817 /**
818 * Calculating the signature.
819 *
820 * @param array $data - Array with data to create a signature for.
821 *
822 * @since 2.2.2
823 */
824 public static function calculate_api_signature( array $data ): string {
825 $now = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
826 $nonce = $now->getTimestamp();
827
828 $pk_hash = hash( 'sha512', $data['license_key'] . '|' . $nonce );
829 $authentication_string = base64_encode( $pk_hash . '|' . $nonce ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
830
831 return $authentication_string;
832 }
833
834 /**
835 * Checks if that is the WP login page or not.
836 *
837 * @return bool
838 *
839 * @since 2.4.1
840 */
841 public static function is_wp_login() {
842 $abs_path = str_replace( array( '\\', '/' ), DIRECTORY_SEPARATOR, ABSPATH );
843
844 if ( function_exists( 'is_account_page' ) && \is_account_page() ) {
845 // The user is on the WooCommerce login page.
846
847 return true;
848 }
849
850 // Legacy check using server globals; input is only used for detection
851 // not for state changes. Ignore strict-equals and input-validation
852 // PHPCS warnings for this read-only check.
853 return ( in_array( $abs_path . 'wp-login.php', get_included_files() ) || in_array( $abs_path . 'wp-register.php', get_included_files() ) ) || ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) || '/wp-login.php' == $_SERVER['PHP_SELF']; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.PHP.StrictInArray.MissingTrueStrict
854 }
855
856 /**
857 * Check whether we are on an admin and plugin page.
858 *
859 * @since 2.4.1
860 *
861 * @param array|string $slug ID(s) of a plugin page. Possible values: 'general', 'logs', 'about' or array of them.
862 *
863 * @return bool
864 */
865 public static function is_admin_page( $slug = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
866
867 // We're reading the `page` GET var for routing only; nonce
868 // verification is not required here. Ignore the PHPCS nonce
869 // recommendation for this read-only usage.
870 $cur_page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
871 $check = WP_2FA_PREFIX_PAGE;
872
873 return \is_admin() && ( false !== strpos( $cur_page, $check ) );
874 }
875
876 /**
877 * Remove all non-WP Mail SMTP plugin notices from our plugin pages.
878 *
879 * @since 2.4.1
880 */
881 public static function hide_unrelated_notices() {
882 // Bail if we're not on our screen or page.
883 if ( ! self::is_admin_page() ) {
884 return;
885 }
886
887 self::remove_unrelated_actions( 'user_admin_notices' );
888 self::remove_unrelated_actions( 'admin_notices' );
889 self::remove_unrelated_actions( 'all_admin_notices' );
890 self::remove_unrelated_actions( 'network_admin_notices' );
891 }
892
893 /**
894 * Creates a nonce for HTML field by given name.
895 *
896 * @param string $nonce_name -The name of the nonce to create.
897 *
898 * @return string
899 *
900 * @since 2.6.0
901 */
902 public static function create_data_nonce( string $nonce_name ): string {
903 return ' data-nonce="' . \esc_attr( \wp_create_nonce( $nonce_name ) ) . '"';
904 }
905
906 /**
907 * Extracts the domain part of the given string.
908 *
909 * @param string $url_to_check - The URL string to be checked.
910 *
911 * @return string
912 *
913 * @since 2.6.0
914 */
915 public static function extract_domain( string $url_to_check ): string {
916 // get the full domain.
917 // $urlparts = parse_url( \site_url() );.
918
919 if ( false !== strpos( $url_to_check, '@' ) ) {
920 $domain = \explode( '@', $url_to_check )[1];
921
922 return $domain;
923 }
924 $urlparts = parse_url( $url_to_check ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
925 $domain = $urlparts ['host'];
926
927 // get the TLD and domain.
928 $domainparts = explode( '.', $domain );
929 $domain = $domainparts[ count( $domainparts ) - 2 ] . '.' . $domainparts[ count( $domainparts ) - 1 ];
930
931 return $domain;
932 }
933
934 /**
935 * Remove all non-WP Mail SMTP notices from the our plugin pages based on the provided action hook.
936 *
937 * @since 2.4.1
938 *
939 * @param string $action The name of the action.
940 */
941 private static function remove_unrelated_actions( $action ) {
942 global $wp_filter;
943
944 if ( empty( $wp_filter[ $action ]->callbacks ) || ! is_array( $wp_filter[ $action ]->callbacks ) ) {
945 return;
946 }
947
948 foreach ( $wp_filter[ $action ]->callbacks as $priority => $hooks ) {
949 foreach ( $hooks as $name => $arr ) {
950 if (
951 ( // Cover object method callback case.
952 is_array( $arr['function'] ) &&
953 isset( $arr['function'][0] ) &&
954 is_object( $arr['function'][0] ) &&
955 false !== strpos( ( get_class( $arr['function'][0] ) ), 'WP2FA' )
956 ) ||
957 ( // Cover class static method callback case.
958 ! empty( $name ) &&
959 false !== strpos( ( $name ), 'WP2FA' )
960 )
961 ) {
962 continue;
963 }
964
965 unset( $wp_filter[ $action ]->callbacks[ $priority ][ $name ] );
966 }
967 }
968 }
969
970 /**
971 * Sets the internal variable with all the existing WP roles.
972 *
973 * @return void
974 *
975 * @since 2.2.0
976 */
977 private static function set_roles() {
978 if ( empty( self::$user_roles ) ) {
979 global $wp_roles;
980
981 if ( null === $wp_roles ) {
982 wp_roles();
983 }
984
985 self::$user_roles = array_flip( $wp_roles->get_names() );
986 }
987 }
988
989 /**
990 * Get the high level domain from a given URL using the Public Suffix List.
991 *
992 * @param string $url - The URL to extract the high level domain from.
993 *
994 * @return string|false - The high level domain or false on failure.
995 *
996 * @since 3.0.0
997 */
998 public static function wp_get_high_level_domain( $url ) {
999
1000 // === CONFIGURATION ===
1001 $psl_url = 'https://publicsuffix.org/list/public_suffix_list.dat';
1002 $cache_filename = 'public_suffix_list.dat';
1003 $cache_ttl = DAY_IN_SECONDS; // refresh every 24 hours.
1004
1005 // === Get uploads directory path ===
1006 $upload_dir = \wp_upload_dir();
1007 $cache_dir = \trailingslashit( $upload_dir['basedir'] );
1008 $cache_path = $cache_dir . $cache_filename;
1009
1010 // === Ensure uploads dir exists ===
1011 if ( ! file_exists( $cache_dir ) ) {
1012 \wp_mkdir_p( $cache_dir );
1013 }
1014
1015 // === Load or refresh Public Suffix List ===
1016 $psl_data = '';
1017 if ( file_exists( $cache_path ) && ( time() - filemtime( $cache_path ) ) < $cache_ttl ) {
1018 // use cached version.
1019 $psl_data = file_get_contents( $cache_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1020 } else {
1021 $response = \wp_remote_get( $psl_url, array( 'timeout' => 10 ) );
1022 if ( \is_wp_error( $response ) ) {
1023 // fallback to cached version if available.
1024 if ( file_exists( $cache_path ) ) {
1025 $psl_data = file_get_contents( $cache_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1026 } else {
1027 return false; // cannot proceed.
1028 }
1029 } else {
1030 $psl_data = \wp_remote_retrieve_body( $response );
1031 if ( $psl_data ) {
1032 file_put_contents( $cache_path, $psl_data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
1033 }
1034 }
1035 }
1036
1037 // === Parse PSL into array ===
1038 $psl = array();
1039 foreach ( explode( "\n", $psl_data ) as $line ) {
1040 $line = trim( $line );
1041 if ( '' === $line || str_starts_with( $line, '//' ) ) {
1042 continue;
1043 }
1044 $psl[] = $line;
1045 }
1046
1047 // === Parse input URL ===
1048 $host = parse_url( $url, PHP_URL_HOST ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
1049 if ( ! $host ) {
1050 return false;
1051 }
1052
1053 $host = strtolower( rtrim( $host, '.' ) );
1054 if ( strpos( $host, 'www.' ) === 0 ) {
1055 $host = substr( $host, 4 );
1056 }
1057
1058 $parts = explode( '.', $host );
1059 $num_parts = count( $parts );
1060 if ( $num_parts < 2 ) {
1061 return $host;
1062 }
1063
1064 // === Find longest matching PSL rule ===
1065 $matches = array();
1066 for ( $i = 0; $i < $num_parts; $i++ ) {
1067 $slice = implode( '.', array_slice( $parts, $i ) );
1068 $wildcard = '*.' . implode( '.', array_slice( $parts, $i + 1 ) );
1069 $exception = '!' . $slice;
1070
1071 if ( in_array( $exception, $psl, true ) ) {
1072 // Exception rule: one level above suffix.
1073 return implode( '.', array_slice( $parts, $i - 1 ) );
1074 }
1075
1076 if ( in_array( $slice, $psl, true ) || in_array( $wildcard, $psl, true ) ) {
1077 $matches[] = $slice;
1078 }
1079 }
1080
1081 $public_suffix = $matches ? end( $matches ) : implode( '.', array_slice( $parts, -1 ) );
1082
1083 // === Build the registrable domain ===
1084 $public_suffix_parts = explode( '.', $public_suffix );
1085 $suffix_count = count( $public_suffix_parts );
1086
1087 if ( $num_parts <= $suffix_count ) {
1088 return $host; // e.g., just "co.uk".
1089 }
1090
1091 $domain = $parts[ $num_parts - $suffix_count - 1 ] . '.' . $public_suffix;
1092
1093 return $domain;
1094 }
1095 }
1096 }
1097