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-migration.php
wp-2fa / includes / classes / Utils Last commit date
class-abstract-migration.php 12 hours ago class-date-time-utils.php 12 hours ago class-debugging.php 12 hours ago class-generate-modal.php 12 hours ago class-migration.php 12 hours ago class-request-utils.php 12 hours ago class-settings-utils.php 12 hours ago class-upgrade-guard.php 12 hours ago class-user-utils.php 12 hours ago class-validator.php 12 hours ago class-white-label.php 12 hours ago index.php 12 hours ago
class-migration.php
590 lines
1 <?php
2 /**
3 * Responsible for plugin updates.
4 *
5 * @package wp2fa
6 * @subpackage utils
7 * @copyright 2026 Melapress
8 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
9 * @link https://wordpress.org/plugins/wp-2fa/
10 */
11
12 declare(strict_types=1);
13
14 namespace WP2FA\Utils;
15
16 use WP2FA\Utils\Abstract_Migration;
17 use WP2FA\Utils\User_Utils;
18 use WP2FA\Utils\Settings_Utils;
19
20 defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
21
22 /**
23 * Migration class
24 */
25 if ( ! class_exists( '\WP2FA\Utils\Migration' ) ) {
26
27 /**
28 * Put all you migration methods here
29 *
30 * @package WP2FA\Utils
31 * @since 1.6
32 */
33 class Migration extends Abstract_Migration {
34
35 /**
36 * The name of the option from which we should extract version
37 * Note: version is expected in version format - 1.0.0; 1; 1.0; 1.0.0.0
38 * Note: only numbers will be processed
39 *
40 * @var string
41 *
42 * @since 1.6.0
43 */
44 protected static $version_option_name = WP_2FA_PREFIX . 'plugin_version';
45
46 /**
47 * The constant name where the plugin version is stored
48 * Note: version is expected in version format - 1.0.0; 1; 1.0; 1.0.0.0
49 * Note: only numbers will be processed
50 *
51 * @var string
52 *
53 * @since 1.6.0
54 */
55 protected static $const_name_of_plugin_version = 'WP_2FA_VERSION';
56
57 /**
58 * The name of the plugin settings
59 *
60 * @var string
61 *
62 * @since 1.6.0
63 */
64 private static $plugin_settings_name = WP_2FA_SETTINGS_NAME;
65
66 /**
67 * The name of the plugin policy settings
68 *
69 * @var string
70 *
71 * @since 1.6.0
72 */
73 private static $plugin_policy_name = WP_2FA_POLICY_SETTINGS_NAME;
74
75 /**
76 * The name of the plugin white label settings
77 *
78 * @var string
79 *
80 * @since 1.6.0
81 */
82 private static $plugin_white_label_name = WP_2FA_WHITE_LABEL_SETTINGS_NAME;
83
84 /**
85 * The name of the plugin email settings
86 *
87 * @var string
88 *
89 * @since 1.6.0
90 */
91 private static $plugin_email_settings_name = WP_2FA_EMAIL_SETTINGS_NAME;
92
93 /**
94 * Migration for version upto 1.6.0
95 *
96 * @return void
97 *
98 * @since 1.6.0
99 */
100 protected static function migrate_up_to_160() {
101 $settings = self::get_settings( self::$plugin_settings_name );
102 if ( ! is_array( $settings ) ) {
103 return;
104 }
105
106 $needs_update = false;
107
108 $settings_to_convert = array( 'enforced_roles', 'enforced_users', 'excluded_users', 'excluded_roles' );
109 foreach ( $settings_to_convert as $setting_name ) {
110 if ( array_key_exists( $setting_name, $settings ) && ! is_array( $settings[ $setting_name ] ) ) {
111 $settings[ $setting_name ] = array_filter(
112 array_map( 'sanitize_text_field', explode( ',', $settings[ $setting_name ] ) )
113 );
114 $needs_update = true;
115 }
116 }
117
118 if ( ! isset( $settings['backup_codes_enabled'] ) ) {
119 $settings['backup_codes_enabled'] = 'yes';
120 $needs_update = true;
121 }
122
123 if ( $needs_update ) {
124 // Update settings.
125 self::set_settings( self::$plugin_settings_name, $settings );
126 }
127 }
128
129 /**
130 * Migration for version upto 1.6.2
131 *
132 * @return void
133 *
134 * @since 1.6.2
135 */
136 protected static function migrate_up_to_162() {
137 $settings = self::get_settings( self::$plugin_settings_name );
138 if ( ! is_array( $settings ) ) {
139 return;
140 }
141
142 $needs_update = false;
143
144 $settings_to_convert = array( 'excluded_sites' );
145 foreach ( $settings_to_convert as $setting_name ) {
146 if ( array_key_exists( $setting_name, $settings ) && ! is_array( $settings[ $setting_name ] ) ) {
147 $original_settings_split = array_filter(
148 array_map( 'sanitize_text_field', explode( ',', $settings[ $setting_name ] ) )
149 );
150 $settings[ $setting_name ] = array();
151 foreach ( $original_settings_split as $value ) {
152 $settings[ $setting_name ][] = sanitize_text_field( mb_substr( $value, mb_strrpos( $value, ':' ) + 1 ) );
153 }
154 $needs_update = true;
155 }
156 }
157
158 self::migrate_up_to_160();
159
160 if ( $needs_update ) {
161 // Update settings.
162 self::set_settings( self::$plugin_settings_name, $settings );
163 }
164 }
165
166 /**
167 * Migration for version upto 1.5.0
168 *
169 * @return void
170 *
171 * @since 1.6.0
172 */
173 protected static function migrate_up_to_150() {
174 $settings = self::get_settings( self::$plugin_settings_name );
175
176 if ( is_array( $settings ) && array_key_exists( 'enforcment-policy', $settings ) ) {
177 // Correct setting name.
178 $settings['enforcement-policy'] = sanitize_text_field( $settings['enforcment-policy'] );
179 // Remove old setting.
180 unset( $settings['enforcment-policy'] );
181 // Update settings.
182 self::set_settings( self::$plugin_settings_name, $settings );
183 }
184 }
185
186 /**
187 * Migration for version upto 1.7.0
188 *
189 * @return void
190 *
191 * @since 1.6.0
192 */
193 protected static function migrate_up_to_170() {
194 $settings = self::get_settings( self::$plugin_settings_name );
195
196 if ( is_array( $settings ) && array_key_exists( 'notify_users', $settings ) ) {
197 // Remove old setting.
198 unset( $settings['notify_users'] );
199 // Update settings.
200 self::set_settings( self::$plugin_settings_name, $settings );
201 }
202
203 $email_settings = self::get_settings( self::$plugin_email_settings_name );
204 $items_to_remove = array( 'send_enforced_email', 'enforced_email_subject', 'enforced_email_body' );
205
206 if ( is_array( $email_settings ) && User_Utils::in_array_all( $items_to_remove, $email_settings ) ) {
207 foreach ( $items_to_remove as $item ) {
208 if ( isset( $email_settings[ $item ] ) ) {
209 unset( $email_settings[ $item ] );
210 }
211 }
212 // Update settings.
213 self::set_settings( self::$plugin_email_settings_name, $email_settings );
214 }
215 }
216
217 /**
218 * Migration for version upto 2.0.0
219 * Separates the current settings into 3 different types of settings:
220 * - Policy
221 * - General
222 * - White label
223 *
224 * @return void
225 *
226 * @since 1.6.0
227 */
228 protected static function migrate_up_to_200() {
229 $settings = self::get_settings( self::$plugin_settings_name );
230
231 if ( is_array( $settings ) ) {
232
233 $new_settings_array = array_flip(
234 array(
235 'enable_grace_cron',
236 'limit_access',
237 'delete_data_upon_uninstall',
238 'enable_destroy_session',
239 )
240 );
241
242 $new_white_label_array = array_flip(
243 array(
244 'default-text-code-page',
245 )
246 );
247
248 $settings_array = array_intersect_key(
249 $settings,
250 $new_settings_array
251 );
252
253 $settings = array_diff_key( $settings, $new_settings_array );
254
255 self::set_settings( self::$plugin_settings_name, $settings_array );
256
257 $white_label_settings = array_intersect_key(
258 $settings,
259 $new_white_label_array
260 );
261
262 $settings = array_diff_key( $settings, $new_white_label_array );
263
264 self::set_settings( self::$plugin_white_label_name, $white_label_settings );
265
266 self::set_settings( self::$plugin_policy_name, $settings );
267 }
268 }
269
270 /**
271 * Migration for version upto 2.2.0
272 *
273 * @return void
274 *
275 * @since 1.6.0
276 */
277 protected static function migrate_up_to_220() {
278 global $wpdb;
279
280 $new_prefix = 'wp_2fa_trusted_device_';
281 $old_prefix = 'wp2fa_trusted_device_';
282
283 \delete_transient( 'wp_2fa_config_file_hash' );
284
285 $wpdb->query(
286 $wpdb->prepare(
287 "
288 UPDATE $wpdb->usermeta
289 SET meta_key = REPLACE( meta_key, %s, %s )
290 WHERE meta_key LIKE %s
291 ",
292 array(
293 \sanitize_key( $old_prefix ),
294 \sanitize_key( $new_prefix ),
295 \sanitize_key( $old_prefix . '%' ),
296 )
297 )
298 );
299 }
300
301 /**
302 * Migration for version upto 2.3.0
303 *
304 * @return void
305 *
306 * @since 1.6.0
307 */
308 protected static function migrate_up_to_230() {
309
310 $version = self::get_settings( self::$version_option_name );
311
312 if ( $version && version_compare( $version, '2.2.1', '<=' ) ) {
313 $settings = self::get_settings( self::$plugin_white_label_name );
314
315 if ( isset( $settings['enable_wizard_styling'] ) ) {
316 $settings['enable_wizard_styling'] = false;
317 } else {
318 $settings = array();
319 $settings['enable_wizard_styling'] = false;
320 }
321
322 self::set_settings( self::$plugin_white_label_name, $settings );
323 }
324 }
325
326 /**
327 * Migration for version upto 2.4.0
328 *
329 * @return void
330 *
331 * @since 1.6.0
332 */
333 protected static function migrate_up_to_240() {
334
335 \delete_transient( 'wp_2fa_config_file_hash' );
336
337 if ( \wp_next_scheduled( 'wp_2fa_check_grace_period_status' ) ) {
338 \wp_clear_scheduled_hook( 'wp_2fa_check_grace_period_status' );
339 }
340 }
341
342 /**
343 * Migration for version upto 2.6.2
344 *
345 * @return void
346 *
347 * @since 1.6.0
348 */
349 protected static function migrate_up_to_262() {
350
351 self::migrate_up_to_240();
352 }
353
354 /**
355 * Migration for version upto 2.6.3
356 *
357 * @return void
358 *
359 * @since 1.6.0
360 */
361 protected static function migrate_up_to_263() {
362
363 self::migrate_up_to_240();
364 }
365
366 /**
367 * Migration for version upto 2.8.0
368 *
369 * @return void
370 *
371 * @since 2.8.0
372 */
373 protected static function migrate_up_to_280() {
374
375 self::migrate_up_to_240();
376 }
377
378 /**
379 * Migration for version upto 3.0.0
380 *
381 * @return void
382 *
383 * @since 3.0.0
384 */
385 protected static function migrate_up_to_300() {
386
387 Settings_Utils::delete_option( 'update_redirection_needed' );
388 Settings_Utils::delete_option( 'update_notice_needed' );
389 }
390
391 /**
392 * Migration for version upto 2.9.2
393 *
394 * @return void
395 *
396 * @since 2.9.2
397 */
398 protected static function migrate_up_to_292() {
399
400 Settings_Utils::delete_option( 'method_selection_single' );
401
402 $settings = self::get_settings( self::$plugin_settings_name );
403
404 if ( \is_array( $settings ) && isset( $settings['enable_rest'] ) ) {
405
406 $settings['enable_rest'] = false;
407
408 self::set_settings( self::$plugin_settings_name, $settings );
409 }
410 }
411
412 /**
413 * Migration for version upto 4.0.0
414 *
415 * Disable the new interface for existing (upgrading) users.
416 * Fresh installs will default to enabled since the option won't exist.
417 *
418 * Also renames settings keys that previously started with '2fa_' to 'wp-2fa_'
419 * to comply with CSS naming conventions (no class/ID names starting with a digit).
420 *
421 * @return void
422 *
423 * @since 4.0.0
424 */
425 protected static function migrate_up_to_400() {
426
427 \delete_transient( 'wp_2fa_config_file_hash' );
428
429 // On multisite, delete the config hash transient from ALL subsites
430 // to prevent stale checksums from blocking extensions after upgrade.
431 if ( \is_multisite() ) {
432 $sites = \get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
433 foreach ( $sites as $blog_id ) {
434 \switch_to_blog( $blog_id );
435 \delete_transient( 'wp_2fa_config_file_hash' );
436 \restore_current_blog();
437 }
438 }
439
440 // Detect fresh install: if no policy settings exist, this is a new install
441 // (migration runs before the activation hook can set the version).
442 // In that case, skip interface/dialog changes so the new interface is enabled
443 // by default and the first-time wizard can run unimpeded.
444 $existing_policy = self::get_settings( self::$plugin_policy_name );
445 $is_fresh_install = empty( $existing_policy );
446
447 $settings = self::get_settings( self::$plugin_settings_name );
448
449 if ( ! \is_array( $settings ) ) {
450 $settings = array();
451 }
452
453 if ( ! $is_fresh_install ) {
454 $settings['use_new_interface'] = false;
455
456 self::set_settings( self::$plugin_settings_name, $settings );
457 }
458
459 self::rename_white_label_keys();
460
461 self::migrate_sms_templates();
462
463 // Show the new interface announcement dialog only for upgrades.
464 // Skip for fresh installs, WP-CLI and bulk updates.
465 if ( ! $is_fresh_install ) {
466 $show_dialog = true;
467
468 if ( defined( 'WP_CLI' ) && WP_CLI ) {
469 $show_dialog = false;
470 } else {
471 $manual_update = \get_transient( 'wp_2fa_manual_update' );
472 if ( '0' === $manual_update ) {
473 $show_dialog = false;
474 }
475 \delete_transient( 'wp_2fa_manual_update' );
476 }
477
478 if ( $show_dialog ) {
479 Settings_Utils::update_option( \WP2FA\Admin\New_Interface_Notice::SHOW_DIALOG_OPTION, 1 );
480 }
481 }
482 }
483
484 /**
485 * Migrates SMS template settings from plain string format to array format.
486 *
487 * Prior to version 4.0.0, SMS templates were stored as plain strings:
488 * "default-twilio-registration-text" => "custom text"
489 *
490 * From version 4.0.0 onwards, they must be stored as arrays with a 'body' key:
491 * "default-twilio-registration-text" => array( "body" => "custom text" )
492 *
493 * @return void
494 *
495 * @since 4.0.0
496 */
497 private static function migrate_sms_templates() {
498 $email_settings = self::get_settings( self::$plugin_email_settings_name );
499
500 if ( ! \is_array( $email_settings ) ) {
501 return;
502 }
503
504 $sms_keys = array(
505 'default-twilio-registration-text',
506 'default-twilio-code-text',
507 );
508
509 $updated = false;
510
511 foreach ( $sms_keys as $key ) {
512 if ( isset( $email_settings[ $key ] ) && \is_string( $email_settings[ $key ] ) ) {
513 $email_settings[ $key ] = array( 'body' => $email_settings[ $key ] );
514 $updated = true;
515 }
516 }
517
518 if ( $updated ) {
519 self::set_settings( self::$plugin_email_settings_name, $email_settings );
520 }
521 }
522
523 /**
524 * Renames white label settings keys that are also used as wp_editor HTML
525 * element IDs, from '2fa_*' to 'wp-2fa_*' so they no longer start with a digit.
526 *
527 * Only keys that directly become HTML id attributes are renamed.
528 * Internal-only settings keys (e.g. '2fa_settings_last_updated_by') and
529 * user meta keys (e.g. 'wp_2fa_2fa_status') are left unchanged because
530 * their final resolved values never start with a digit.
531 *
532 * @return void
533 *
534 * @since 4.0.0
535 */
536 private static function rename_white_label_keys() {
537
538 $white_label_key_map = array(
539 '2fa_required_intro' => 'wp-2fa_required_intro',
540 '2fa_wizard_cancel' => 'wp-2fa_wizard_cancel',
541 );
542
543 $white_label_settings = self::get_settings( self::$plugin_white_label_name );
544
545 if ( \is_array( $white_label_settings ) ) {
546 $updated = false;
547
548 foreach ( $white_label_key_map as $old_key => $new_key ) {
549 if ( \array_key_exists( $old_key, $white_label_settings ) && ! \array_key_exists( $new_key, $white_label_settings ) ) {
550 $white_label_settings[ $new_key ] = $white_label_settings[ $old_key ];
551 unset( $white_label_settings[ $old_key ] );
552 $updated = true;
553 }
554 }
555
556 if ( $updated ) {
557 self::set_settings( self::$plugin_white_label_name, $white_label_settings );
558 }
559 }
560 }
561
562 /**
563 * Returns the plugin settings by a given setting type
564 *
565 * @param mixed $setting_name - The setting which needs to be extracted.
566 *
567 * @return mixed
568 *
569 * @since 1.6.0
570 */
571 private static function get_settings( $setting_name ) {
572 return Settings_Utils::get_option( sanitize_key( $setting_name ) );
573 }
574
575 /**
576 * Updates the plugin settings
577 *
578 * @param mixed $setting_name - The setting which needs to be updated.
579 * @param mixed $settings - The settings values.
580 *
581 * @return void
582 *
583 * @since 1.6.0
584 */
585 private static function set_settings( $setting_name, $settings ) {
586 Settings_Utils::update_option( sanitize_key( $setting_name ), $settings );
587 }
588 }
589 }
590