This commit is contained in:
Javier Casares 2026-03-28 10:06:01 +00:00
commit 231ef59bb9
15 changed files with 254 additions and 135 deletions

View file

@ -1,5 +1,73 @@
== Changelog == == Changelog ==
= 1.1.0 =
_Release date: 2026-03-28_
**Changed**
* Code quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style).
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
* Tested on WordPress 6.7.5, PHP 8.5.3; PHPUnit 9.6.34 — 28 unit tests, 64 assertions, all passing
= 1.0.2 =
_Release date: 2026-03-28_
**Changed**
* Plugin renamed from "Two Factor Extended" to "Two-Factor Extended" (hyphen added for consistency with the Two-Factor plugin naming convention).
* `get_required_providers_for_user()` now intersects enforcement requirements with Two-Factor's globally-enabled providers list (`two_factor_enabled_providers` option, introduced in Two-Factor 0.16). Providers disabled site-wide by Two-Factor will no longer be enforced.
* Added Catalan (ca) translation — 145 strings, 100% coverage.
* Updated Spanish (es_ES) translation to reflect the new plugin name.
**Multisite**
* Two-Factor (base plugin) has no network-level settings — it is entirely per-site. Two-Factor Extended already provides the network layer on top of this with network-wide enforcement, super admin requirements, and site override controls. No changes required.
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
* Tested on WordPress 6.7.5, PHP 8.5.3; PHPUnit 9.6.34 — 28 unit tests, 64 assertions, all passing
= 1.0.1 =
_Release date: 2026-03-28_
**Changed**
* Settings menu now registered at `admin_menu` priority 20 to always appear after Two Factor plugin's Settings page (Two Factor 0.16+).
* PHPUnit downgraded from 10.x to 9.6 for compatibility with the WordPress test library.
* `phpunit.xml.dist` updated: uses PHPUnit 9 schema, `<coverage>` block replaces removed attributes, `suffix=".php"` for test directory discovery.
* Test files renamed from `test-*.php` to `Test_*.php` to match PHPUnit 10/9 class name mapping (class names unchanged).
* `yoast/phpunit-polyfills` downgraded to `^1.1` (matches WordPress core requirement).
**Fixed**
* PHPCS: spacing after parameter type in `robotstxt-updater.php`, `class-enforcement.php`, `class-bulk-actions.php`.
* PHPCS: `list<int>` docblock type replaced with `int[]` to match `array` PHP type hint in `class-bulk-actions.php`.
* PHPStan level 9: all errors resolved across all plugin files.
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
* Tested on WordPress 6.7.5, PHP 8.5.3; PHPUnit 9.6.34 — 28 unit tests, 64 assertions, all passing
= 1.0.0 = = 1.0.0 =
_Release date: 2026-02-17_ _Release date: 2026-02-17_
@ -79,18 +147,12 @@ This is the first comprehensive release of Two-Factor Extended, implementing ent
**Compatibility** **Compatibility**
* WordPress: 6.7 - 6.9 * WordPress: 6.7, 6.8, 6.9
* PHP: 8.2 - 8.5 * PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6+ * MariaDB: 10.6 or newer
* Two Factor Plugin: 0.15.0 (tested) * Multisite: Supported
* Multisite: Fully supported (Network: true) * Two-Factor plugin: 0.15 or newer
* Tested on WordPress 6.9, PHP 8.2, MariaDB 10.6; plugin detection verified, dependency checks working, PHPCS passing, unit tests passing
**Testing Environment**
* WordPress: 6.9
* PHP: 8.2
* MariaDB: 10.6
* Testing: Plugin detection verified, dependency checks working, PHPCS passing, unit tests passing
**Development Tools** **Development Tools**

View file

@ -120,11 +120,11 @@ class Two_Factor_Extended_Audit_Log {
return; return;
} }
$providers = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user_id ); $providers = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user_id );
$provider_names = array(); $provider_names = array();
foreach ( array_keys( $providers ) as $class ) { foreach ( array_keys( $providers ) as $class ) {
$names = Two_Factor_Extended_Provider_Detector::get_provider_names(); $names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$provider_names[] = $names[ $class ] ?? $class; $provider_names[] = $names[ $class ] ?? $class;
} }
@ -301,7 +301,7 @@ class Two_Factor_Extended_Audit_Log {
* @since 0.1.0 * @since 0.1.0
*/ */
public function cleanup_old_logs(): void { public function cleanup_old_logs(): void {
$logs = $this->get_logs(); $logs = $this->get_logs();
$cutoff_time = current_time( 'timestamp' ) - ( self::RETENTION_DAYS * DAY_IN_SECONDS ); $cutoff_time = current_time( 'timestamp' ) - ( self::RETENTION_DAYS * DAY_IN_SECONDS );
$filtered_logs = array_filter( $filtered_logs = array_filter(
@ -328,7 +328,7 @@ class Two_Factor_Extended_Audit_Log {
public function export_to_csv( array $filters = array() ): string { public function export_to_csv( array $filters = array() ): string {
$logs = $this->get_logs( $filters ); $logs = $this->get_logs( $filters );
$csv = array(); $csv = array();
$csv[] = array( 'Timestamp', 'Action', 'Description', 'User', 'Actor', 'IP Address' ); $csv[] = array( 'Timestamp', 'Action', 'Description', 'User', 'Actor', 'IP Address' );
foreach ( $logs as $log ) { foreach ( $logs as $log ) {
@ -424,8 +424,8 @@ class Two_Factor_Extended_Audit_Log {
$logs = $this->get_logs(); $logs = $this->get_logs();
$stats = array( $stats = array(
'total' => count( $logs ), 'total' => count( $logs ),
'by_action' => array(), 'by_action' => array(),
'recent_count' => 0, 'recent_count' => 0,
); );
@ -439,11 +439,11 @@ class Two_Factor_Extended_Audit_Log {
if ( ! isset( $stats['by_action'][ $action ] ) ) { if ( ! isset( $stats['by_action'][ $action ] ) ) {
$stats['by_action'][ $action ] = 0; $stats['by_action'][ $action ] = 0;
} }
$stats['by_action'][ $action ]++; ++$stats['by_action'][ $action ];
// Count recent logs (last 7 days). // Count recent logs (last 7 days).
if ( $ts >= $recent_cutoff ) { if ( $ts >= $recent_cutoff ) {
$stats['recent_count']++; ++$stats['recent_count'];
} }
} }

View file

@ -158,7 +158,7 @@ class Two_Factor_Extended_Bulk_Actions {
); );
} }
$processed++; ++$processed;
} }
return $processed; return $processed;
@ -203,7 +203,7 @@ class Two_Factor_Extended_Bulk_Actions {
); );
} }
$processed++; ++$processed;
} }
return $processed; return $processed;

View file

@ -66,29 +66,29 @@ class Two_Factor_Extended_CLI_Commands {
$overview = array( $overview = array(
array( array(
'Metric' => 'Total Users', 'Metric' => 'Total Users',
'Count' => $stats['total_users'], 'Count' => $stats['total_users'],
'Percentage' => '100%', 'Percentage' => '100%',
), ),
array( array(
'Metric' => 'Compliant', 'Metric' => 'Compliant',
'Count' => $stats['compliant_users'], 'Count' => $stats['compliant_users'],
'Percentage' => $this->calculate_percentage( $stats['compliant_users'], $stats['total_users'] ), 'Percentage' => $this->calculate_percentage( $stats['compliant_users'], $stats['total_users'] ),
), ),
array( array(
'Metric' => 'Non-Compliant', 'Metric' => 'Non-Compliant',
'Count' => $stats['non_compliant'], 'Count' => $stats['non_compliant'],
'Percentage' => $this->calculate_percentage( $stats['non_compliant'], $stats['total_users'] ), 'Percentage' => $this->calculate_percentage( $stats['non_compliant'], $stats['total_users'] ),
), ),
array( array(
'Metric' => 'In Grace Period', 'Metric' => 'In Grace Period',
'Count' => $stats['grace_period'], 'Count' => $stats['grace_period'],
'Percentage' => $this->calculate_percentage( $stats['grace_period'], $stats['total_users'] ), 'Percentage' => $this->calculate_percentage( $stats['grace_period'], $stats['total_users'] ),
), ),
array( array(
'Metric' => 'No Requirements', 'Metric' => 'No Requirements',
'Count' => $stats['no_requirements'], 'Count' => $stats['no_requirements'],
'Percentage' => $this->calculate_percentage( $stats['no_requirements'], $stats['total_users'] ), 'Percentage' => $this->calculate_percentage( $stats['no_requirements'], $stats['total_users'] ),
), ),
); );
@ -105,11 +105,11 @@ class Two_Factor_Extended_CLI_Commands {
$role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role_key ); $role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role_key );
$by_role[] = array( $by_role[] = array(
'Role' => $role_name, 'Role' => $role_name,
'Total' => $role_stats['total'], 'Total' => $role_stats['total'],
'Compliant' => $role_stats['compliant'], 'Compliant' => $role_stats['compliant'],
'Non-Compliant' => $role_stats['non_compliant'], 'Non-Compliant' => $role_stats['non_compliant'],
'Compliance %' => $this->calculate_percentage( $role_stats['compliant'], $role_stats['total'] ), 'Compliance %' => $this->calculate_percentage( $role_stats['compliant'], $role_stats['total'] ),
); );
} }
@ -196,7 +196,7 @@ class Two_Factor_Extended_CLI_Commands {
); );
} }
$processed++; ++$processed;
$progress->tick(); $progress->tick();
} }

View file

@ -53,12 +53,12 @@ class Two_Factor_Extended_Compliance_Report {
$users = get_users( $user_args ); $users = get_users( $user_args );
$stats = array( $stats = array(
'total_users' => count( $users ), 'total_users' => count( $users ),
'compliant_users' => 0, 'compliant_users' => 0,
'non_compliant' => 0, 'non_compliant' => 0,
'grace_period' => 0, 'grace_period' => 0,
'no_requirements' => 0, 'no_requirements' => 0,
'by_role' => array(), 'by_role' => array(),
); );
$enforcement = two_factor_extended()->get_enforcement(); $enforcement = two_factor_extended()->get_enforcement();
@ -71,7 +71,7 @@ class Two_Factor_Extended_Compliance_Report {
$required = $enforcement->get_required_providers_for_user( $user->ID ); $required = $enforcement->get_required_providers_for_user( $user->ID );
if ( empty( $required ) ) { if ( empty( $required ) ) {
$stats['no_requirements']++; ++$stats['no_requirements'];
continue; continue;
} }
@ -79,11 +79,11 @@ class Two_Factor_Extended_Compliance_Report {
$in_grace = $enforcement->is_in_grace_period( $user->ID ); $in_grace = $enforcement->is_in_grace_period( $user->ID );
if ( $compliant ) { if ( $compliant ) {
$stats['compliant_users']++; ++$stats['compliant_users'];
} elseif ( $in_grace ) { } elseif ( $in_grace ) {
$stats['grace_period']++; ++$stats['grace_period'];
} else { } else {
$stats['non_compliant']++; ++$stats['non_compliant'];
} }
// Count by role. // Count by role.
@ -92,18 +92,18 @@ class Two_Factor_Extended_Compliance_Report {
foreach ( $roles as $role ) { foreach ( $roles as $role ) {
if ( ! isset( $stats['by_role'][ $role ] ) ) { if ( ! isset( $stats['by_role'][ $role ] ) ) {
$stats['by_role'][ $role ] = array( $stats['by_role'][ $role ] = array(
'total' => 0, 'total' => 0,
'compliant' => 0, 'compliant' => 0,
'non_compliant' => 0, 'non_compliant' => 0,
); );
} }
$stats['by_role'][ $role ]['total']++; ++$stats['by_role'][ $role ]['total'];
if ( $compliant ) { if ( $compliant ) {
$stats['by_role'][ $role ]['compliant']++; ++$stats['by_role'][ $role ]['compliant'];
} else { } else {
$stats['by_role'][ $role ]['non_compliant']++; ++$stats['by_role'][ $role ]['non_compliant'];
} }
} }
} }
@ -194,13 +194,13 @@ class Two_Factor_Extended_Compliance_Report {
return array(); return array();
} }
$sites = get_sites( array( 'number' => 999 ) ); $sites = get_sites( array( 'number' => 999 ) );
$report = array( $report = array(
'total_sites' => count( $sites ), 'total_sites' => count( $sites ),
'total_users' => 0, 'total_users' => 0,
'compliant' => 0, 'compliant' => 0,
'non_compliant' => 0, 'non_compliant' => 0,
'by_site' => array(), 'by_site' => array(),
); );
foreach ( $sites as $site ) { foreach ( $sites as $site ) {
@ -216,8 +216,8 @@ class Two_Factor_Extended_Compliance_Report {
'non_compliant' => $site_stats['non_compliant'], 'non_compliant' => $site_stats['non_compliant'],
); );
$report['total_users'] += $site_stats['total_users']; $report['total_users'] += $site_stats['total_users'];
$report['compliant'] += $site_stats['compliant_users']; $report['compliant'] += $site_stats['compliant_users'];
$report['non_compliant'] += $site_stats['non_compliant']; $report['non_compliant'] += $site_stats['non_compliant'];
restore_current_blog(); restore_current_blog();
@ -238,7 +238,7 @@ class Two_Factor_Extended_Compliance_Report {
public function export_to_csv( array $args = array() ): string { public function export_to_csv( array $args = array() ): string {
$non_compliant = $this->get_non_compliant_users( $args ); $non_compliant = $this->get_non_compliant_users( $args );
$csv = array(); $csv = array();
$csv[] = array( 'User ID', 'Username', 'Email', 'Roles', 'Missing Providers', 'Grace Period', 'Days Remaining' ); $csv[] = array( 'User ID', 'Username', 'Email', 'Roles', 'Missing Providers', 'Grace Period', 'Days Remaining' );
foreach ( $non_compliant as $user_data ) { foreach ( $non_compliant as $user_data ) {
@ -317,7 +317,7 @@ class Two_Factor_Extended_Compliance_Report {
foreach ( $stats['by_role'] as $role => $role_stats ) { foreach ( $stats['by_role'] as $role => $role_stats ) {
$role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role ); $role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role );
$message .= sprintf( $message .= sprintf(
' %s: %d/%d compliant', ' %s: %d/%d compliant',
$role_name, $role_name,
$role_stats['compliant'], $role_stats['compliant'],

View file

@ -393,7 +393,7 @@ class Two_Factor_Extended_Enforcement {
* @return array<int, string> Array of provider labels. * @return array<int, string> Array of provider labels.
*/ */
private function get_provider_labels( array $provider_classes ): array { private function get_provider_labels( array $provider_classes ): array {
$names = Two_Factor_Extended_Provider_Detector::get_provider_names(); $names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$labels = array(); $labels = array();
foreach ( $provider_classes as $class ) { foreach ( $provider_classes as $class ) {

View file

@ -247,9 +247,9 @@ class Two_Factor_Extended_Network_Settings {
* @param array<string, mixed> $settings Current network settings. * @param array<string, mixed> $settings Current network settings.
*/ */
private function render_network_role_requirements( array $settings ): void { private function render_network_role_requirements( array $settings ): void {
$roles = Two_Factor_Extended_Role_Manager::get_all_roles(); $roles = Two_Factor_Extended_Role_Manager::get_all_roles();
$providers = Two_Factor_Extended_Provider_Detector::get_provider_names(); $providers = Two_Factor_Extended_Provider_Detector::get_provider_names();
$network_reqs = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array(); $network_reqs = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
if ( empty( $providers ) ) { if ( empty( $providers ) ) {
?> ?>
@ -384,7 +384,7 @@ class Two_Factor_Extended_Network_Settings {
// Grace period days. // Grace period days.
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) { if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
$days = (int) $input['grace_period_days']; $days = (int) $input['grace_period_days'];
$sanitized['grace_period_days'] = max( 0, min( 365, $days ) ); $sanitized['grace_period_days'] = max( 0, min( 365, $days ) );
} }

View file

@ -368,7 +368,7 @@ class Two_Factor_Extended_REST_API {
); );
} }
$processed++; ++$processed;
} }
return new WP_REST_Response( return new WP_REST_Response(
@ -435,7 +435,7 @@ class Two_Factor_Extended_REST_API {
); );
} }
$processed++; ++$processed;
} }
return new WP_REST_Response( return new WP_REST_Response(

View file

@ -32,8 +32,8 @@ class Two_Factor_Extended_Role_Manager {
require_once ABSPATH . 'wp-admin/includes/user.php'; require_once ABSPATH . 'wp-admin/includes/user.php';
} }
$roles = array(); $roles = array();
$wp_roles = get_editable_roles(); $wp_roles = get_editable_roles();
foreach ( $wp_roles as $role_slug => $role_data ) { foreach ( $wp_roles as $role_slug => $role_data ) {
$roles[ $role_slug ] = translate_user_role( $role_data['name'] ); $roles[ $role_slug ] = translate_user_role( $role_data['name'] );

View file

@ -453,7 +453,7 @@ class Two_Factor_Extended_Settings {
</td> </td>
<td> <td>
<?php <?php
$role_req_raw = $requirements[ $role_slug ] ?? array(); $role_req_raw = $requirements[ $role_slug ] ?? array();
$role_requirements = is_array( $role_req_raw ) ? $role_req_raw : array(); $role_requirements = is_array( $role_req_raw ) ? $role_req_raw : array();
foreach ( $providers as $provider_class => $provider_name ) : foreach ( $providers as $provider_class => $provider_name ) :
@ -537,16 +537,16 @@ class Two_Factor_Extended_Settings {
</td> </td>
<td> <td>
<?php <?php
$role_vis_raw = $visibility[ $role_slug ] ?? array(); $role_vis_raw = $visibility[ $role_slug ] ?? array();
$role_req_raw = $requirements[ $role_slug ] ?? array(); $role_req_raw = $requirements[ $role_slug ] ?? array();
$role_visibility = is_array( $role_vis_raw ) ? $role_vis_raw : array(); $role_visibility = is_array( $role_vis_raw ) ? $role_vis_raw : array();
$role_required = is_array( $role_req_raw ) ? $role_req_raw : array(); $role_required = is_array( $role_req_raw ) ? $role_req_raw : array();
foreach ( $providers as $provider_class => $provider_name ) : foreach ( $providers as $provider_class => $provider_name ) :
$checked = in_array( $provider_class, $role_visibility, true ); $checked = in_array( $provider_class, $role_visibility, true );
$is_required = in_array( $provider_class, $role_required, true ); $is_required = in_array( $provider_class, $role_required, true );
$disabled = $is_required ? 'disabled' : ''; $disabled = $is_required ? 'disabled' : '';
$checked = $checked || $is_required; $checked = $checked || $is_required;
?> ?>
<label style="display: block; margin-bottom: 5px;"> <label style="display: block; margin-bottom: 5px;">
<input <input
@ -989,7 +989,7 @@ class Two_Factor_Extended_Settings {
<h2 class="nav-tab-wrapper"> <h2 class="nav-tab-wrapper">
<?php foreach ( $tabs as $tab_key => $tab_label ) : ?> <?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
<a href="<?php echo esc_url( add_query_arg( 'tab', $tab_key, admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ) ); ?>" <a href="<?php echo esc_url( add_query_arg( 'tab', $tab_key, admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ) ); ?>"
class="nav-tab <?php echo $current_tab === $tab_key ? 'nav-tab-active' : ''; ?>"> class="nav-tab <?php echo $current_tab === $tab_key ? 'nav-tab-active' : ''; ?>">
<?php echo esc_html( $tab_label ); ?> <?php echo esc_html( $tab_label ); ?>
</a> </a>
<?php endforeach; ?> <?php endforeach; ?>
@ -1106,7 +1106,7 @@ class Two_Factor_Extended_Settings {
wp_nonce_url( wp_nonce_url(
add_query_arg( add_query_arg(
array( array(
'tab' => 'audit-log', 'tab' => 'audit-log',
'action' => 'export', 'action' => 'export',
) )
), ),
@ -1133,11 +1133,11 @@ class Two_Factor_Extended_Settings {
<?php if ( ! empty( $logs ) ) : ?> <?php if ( ! empty( $logs ) ) : ?>
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?> <?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
<?php <?php
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null; $log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : ''; $log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : ''; $log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
$log_actor_id = isset( $log['actor_id'] ) && is_int( $log['actor_id'] ) ? $log['actor_id'] : 0; $log_actor_id = isset( $log['actor_id'] ) && is_int( $log['actor_id'] ) ? $log['actor_id'] : 0;
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '-'; $log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '-';
?> ?>
<tr> <tr>
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td> <td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
@ -1200,8 +1200,8 @@ class Two_Factor_Extended_Settings {
exit; exit;
} }
$stats = $compliance->get_compliance_stats(); $stats = $compliance->get_compliance_stats();
$non_compliant = $compliance->get_non_compliant_users(); $non_compliant = $compliance->get_non_compliant_users();
?> ?>
<div class="card"> <div class="card">
@ -1265,7 +1265,7 @@ class Two_Factor_Extended_Settings {
wp_nonce_url( wp_nonce_url(
add_query_arg( add_query_arg(
array( array(
'tab' => 'compliance', 'tab' => 'compliance',
'action' => 'export', 'action' => 'export',
) )
), ),
@ -1292,16 +1292,16 @@ class Two_Factor_Extended_Settings {
<?php if ( ! empty( $non_compliant ) ) : ?> <?php if ( ! empty( $non_compliant ) ) : ?>
<?php foreach ( $non_compliant as $user_data ) : ?> <?php foreach ( $non_compliant as $user_data ) : ?>
<?php <?php
$user_id_val = isset( $user_data['user_id'] ) && is_int( $user_data['user_id'] ) ? $user_data['user_id'] : 0; $user_id_val = isset( $user_data['user_id'] ) && is_int( $user_data['user_id'] ) ? $user_data['user_id'] : 0;
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : ''; $user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : ''; $user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array(); $roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
$missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array(); $missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
$in_grace = ! empty( $user_data['in_grace_period'] ); $in_grace = ! empty( $user_data['in_grace_period'] );
$grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0; $grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0;
// Get user object for display name. // Get user object for display name.
$user = $user_id_val ? get_userdata( $user_id_val ) : null; $user = $user_id_val ? get_userdata( $user_id_val ) : null;
$roles_display = array_map( $roles_display = array_map(
function ( $role_slug ) { function ( $role_slug ) {
if ( ! is_string( $role_slug ) ) { if ( ! is_string( $role_slug ) ) {
return ''; return '';
@ -1401,7 +1401,7 @@ class Two_Factor_Extended_Settings {
echo '<div class="notice notice-success"><p>' . esc_html__( 'Audit logs cleared successfully.', 'two-factor-extended' ) . '</p></div>'; echo '<div class="notice notice-success"><p>' . esc_html__( 'Audit logs cleared successfully.', 'two-factor-extended' ) . '</p></div>';
} }
$logs = $audit_log->get_logs(); $logs = $audit_log->get_logs();
$stats = $audit_log->get_statistics(); $stats = $audit_log->get_statistics();
?> ?>
@ -1448,12 +1448,12 @@ class Two_Factor_Extended_Settings {
<?php else : ?> <?php else : ?>
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?> <?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
<?php <?php
$log_user_id = isset( $log['user_id'] ) && is_int( $log['user_id'] ) ? $log['user_id'] : 0; $log_user_id = isset( $log['user_id'] ) && is_int( $log['user_id'] ) ? $log['user_id'] : 0;
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null; $log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : ''; $log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : ''; $log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : ''; $log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '';
$user = $log_user_id ? get_userdata( $log_user_id ) : null; $user = $log_user_id ? get_userdata( $log_user_id ) : null;
?> ?>
<tr> <tr>
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td> <td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
@ -1521,7 +1521,7 @@ class Two_Factor_Extended_Settings {
} }
} }
$stats = $compliance->get_compliance_stats(); $stats = $compliance->get_compliance_stats();
$non_compliant = $compliance->get_non_compliant_users(); $non_compliant = $compliance->get_non_compliant_users();
?> ?>
@ -1790,7 +1790,7 @@ class Two_Factor_Extended_Settings {
// Sanitize grace period days. // Sanitize grace period days.
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) { if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
$grace_days = (int) $input['grace_period_days']; $grace_days = (int) $input['grace_period_days'];
$sanitized['grace_period_days'] = max( 0, min( 365, $grace_days ) ); $sanitized['grace_period_days'] = max( 0, min( 365, $grace_days ) );
} }

View file

@ -34,7 +34,7 @@ class Two_Factor_Extended {
* @since 0.1.0 * @since 0.1.0
* @var string * @var string
*/ */
private string $version = '1.0.2'; private string $version = '1.1.0';
/** /**
* Plugin directory path. * Plugin directory path.

View file

@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt
Tags: two-factor, 2fa, authentication, security Tags: two-factor, 2fa, authentication, security
Requires at least: 6.7 Requires at least: 6.7
Tested up to: 6.9 Tested up to: 6.9
Stable tag: 1.0.2 Stable tag: 1.1.0
Requires PHP: 8.2 Requires PHP: 8.2
Version: 1.0.1 Version: 1.1.0
License: GPL-3.0-or-later License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -117,6 +117,63 @@ We take security seriously and will respond promptly to all security reports.
== Changelog == == Changelog ==
= 1.1.0 =
_Release date: 2026-03-28_
**Changed**
* Code quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style).
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
= 1.0.2 =
_Release date: 2026-03-28_
**Changed**
* Plugin renamed from "Two Factor Extended" to "Two-Factor Extended".
* Enforcement now respects Two-Factor's globally-disabled providers (`two_factor_enabled_providers` option, Two-Factor 0.16+). Users will no longer be blocked by providers disabled site-wide.
* Added Catalan (ca) translation — 145 strings, 100% coverage.
* Updated Spanish (es_ES) translation to reflect the new plugin name.
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
= 1.0.1 =
_Release date: 2026-03-28_
**Changed**
* Settings menu now registered at `admin_menu` priority 20 to always appear after Two Factor plugin's Settings page (Two Factor 0.16+).
* PHPUnit downgraded from 10.x to 9.6 for compatibility with the WordPress test library.
**Fixed**
* PHPCS: spacing and docblock type hint fixes across multiple files.
* PHPStan level 9: all errors resolved.
**Compatibility**
* WordPress: 6.7, 6.8, 6.9
* PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6 or newer
* Multisite: Supported
* Two-Factor plugin: 0.16 or newer
= 1.0.0 = = 1.0.0 =
_Release date: 2026-02-17_ _Release date: 2026-02-17_
@ -159,11 +216,11 @@ This is the first comprehensive release of Two-Factor Extended, implementing ent
**Compatibility** **Compatibility**
* WordPress: 6.7 - 6.9 * WordPress: 6.7, 6.8, 6.9
* PHP: 8.2 - 8.5 * PHP: 8.2, 8.3, 8.4, 8.5
* MariaDB: 10.6+ * MariaDB: 10.6 or newer
* Two Factor Plugin: 0.15.0 (tested) * Multisite: Supported
* Multisite: Fully supported * Two-Factor plugin: 0.15 or newer
= Previous versions = = Previous versions =

View file

@ -236,17 +236,17 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
return $result; return $result;
} }
$plugin_name = isset( $remote['name'] ) && is_string( $remote['name'] ) ? $remote['name'] : ( isset( $this->plugin_data['Name'] ) && is_string( $this->plugin_data['Name'] ) ? $this->plugin_data['Name'] : $this->plugin_slug ); $plugin_name = isset( $remote['name'] ) && is_string( $remote['name'] ) ? $remote['name'] : ( isset( $this->plugin_data['Name'] ) && is_string( $this->plugin_data['Name'] ) ? $this->plugin_data['Name'] : $this->plugin_slug );
$plugin_slug = isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug; $plugin_slug = isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug;
$plugin_version = is_string( $remote['version'] ) ? $remote['version'] : ''; $plugin_version = is_string( $remote['version'] ) ? $remote['version'] : '';
$plugin_author = isset( $remote['author'] ) && is_string( $remote['author'] ) ? $remote['author'] : ( isset( $this->plugin_data['Author'] ) && is_string( $this->plugin_data['Author'] ) ? $this->plugin_data['Author'] : '' ); $plugin_author = isset( $remote['author'] ) && is_string( $remote['author'] ) ? $remote['author'] : ( isset( $this->plugin_data['Author'] ) && is_string( $this->plugin_data['Author'] ) ? $this->plugin_data['Author'] : '' );
$plugin_homepage = isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : ( isset( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ? $this->plugin_data['PluginURI'] : '' ); $plugin_homepage = isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : ( isset( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ? $this->plugin_data['PluginURI'] : '' );
$plugin_requires = isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : ''; $plugin_requires = isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : '';
$plugin_tested = isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : ''; $plugin_tested = isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : '';
$plugin_requires_php = isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : ''; $plugin_requires_php = isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : '';
$plugin_description = isset( $remote['description'] ) && is_string( $remote['description'] ) ? $remote['description'] : ( isset( $this->plugin_data['Description'] ) && is_string( $this->plugin_data['Description'] ) ? $this->plugin_data['Description'] : '' ); $plugin_description = isset( $remote['description'] ) && is_string( $remote['description'] ) ? $remote['description'] : ( isset( $this->plugin_data['Description'] ) && is_string( $this->plugin_data['Description'] ) ? $this->plugin_data['Description'] : '' );
$plugin_changelog = isset( $remote['changelog'] ) && is_string( $remote['changelog'] ) ? $remote['changelog'] : ''; $plugin_changelog = isset( $remote['changelog'] ) && is_string( $remote['changelog'] ) ? $remote['changelog'] : '';
$plugin_download = isset( $remote['download_url'] ) && is_string( $remote['download_url'] ) ? $remote['download_url'] : ''; $plugin_download = isset( $remote['download_url'] ) && is_string( $remote['download_url'] ) ? $remote['download_url'] : '';
return (object) array( return (object) array(
'name' => $plugin_name, 'name' => $plugin_name,

View file

@ -3,7 +3,7 @@
* Plugin Name: Two-Factor Extended * Plugin Name: Two-Factor Extended
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/two-factor-extended * Plugin URI: https://git.robotstxt.es/ROBOTSTXT/two-factor-extended
* Description: Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations. * Description: Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations.
* Version: 1.0.2 * Version: 1.1.0
* Requires at least: 6.7 * Requires at least: 6.7
* Requires PHP: 8.2 * Requires PHP: 8.2
* Requires Plugins: two-factor * Requires Plugins: two-factor
@ -18,7 +18,7 @@
* Primary Branch: main * Primary Branch: main
* *
* @package TwoFactorExtended * @package TwoFactorExtended
* @version 1.0.2 * @version 1.1.0
*/ */
// Prevent direct access. // Prevent direct access.

View file

@ -1,8 +1,8 @@
{ {
"name": "Two-Factor Extended", "name": "Two-Factor Extended",
"slug": "two-factor-extended", "slug": "two-factor-extended",
"version": "1.0.2", "version": "1.1.0",
"download_url": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/releases/download/1.0.2/two-factor-extended-1.0.2.zip", "download_url": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/releases/download/1.1.0/two-factor-extended-1.1.0.zip",
"requires": "6.7", "requires": "6.7",
"requires_php": "8.2", "requires_php": "8.2",
"tested": "6.9", "tested": "6.9",
@ -11,10 +11,10 @@
"author_profile": "https://www.robotstxt.es/", "author_profile": "https://www.robotstxt.es/",
"homepage": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended", "homepage": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended",
"description": "Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations.", "description": "Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations.",
"changelog": "<h3>1.0.2 - 2026-03-28</h3><ul><li><strong>Name:</strong> Renamed to \"Two-Factor Extended\" (hyphen added for consistency)</li><li><strong>Compatibility:</strong> Two-Factor 0.16 enforcement now respects globally-disabled providers</li><li><strong>i18n:</strong> Added Catalan (ca) translation (100% coverage); updated es_ES</li></ul><h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>", "changelog": "<h3>1.1.0 - 2026-03-28</h3><ul><li><strong>Code Quality:</strong> phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+</li></ul><h3>1.0.2 - 2026-03-28</h3><ul><li><strong>Name:</strong> Renamed to \"Two-Factor Extended\" (hyphen added for consistency)</li><li><strong>Compatibility:</strong> Two-Factor 0.16 enforcement now respects globally-disabled providers</li><li><strong>i18n:</strong> Added Catalan (ca) translation (100% coverage); updated es_ES</li></ul><h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
"sections": { "sections": {
"description": "Two-Factor Extended is a comprehensive extension for the WordPress Two Factor plugin that provides administrators with enterprise-level controls over two-factor authentication across their site.<br><br><strong>Core Features:</strong><ul><li>Role-Based 2FA Requirements</li><li>Provider Visibility Control</li><li>Grace Period Enforcement (0-365 days)</li><li>WordPress Multisite Support</li><li>Compliance Reporting</li><li>Audit Logging</li><li>Bulk Operations</li><li>WP-CLI Commands</li><li>REST API Endpoints</li><li>Import/Export Settings</li></ul><strong>Security:</strong> Grade A security audit, comprehensive security hardening, CSRF protection, XSS prevention, SQL injection protection.<br><br><strong>Quality Assurance:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, WordPress Coding Standards compliant, PHP 8.2-8.5 compatible.", "description": "Two-Factor Extended is a comprehensive extension for the WordPress Two Factor plugin that provides administrators with enterprise-level controls over two-factor authentication across their site.<br><br><strong>Core Features:</strong><ul><li>Role-Based 2FA Requirements</li><li>Provider Visibility Control</li><li>Grace Period Enforcement (0-365 days)</li><li>WordPress Multisite Support</li><li>Compliance Reporting</li><li>Audit Logging</li><li>Bulk Operations</li><li>WP-CLI Commands</li><li>REST API Endpoints</li><li>Import/Export Settings</li></ul><strong>Security:</strong> Grade A security audit, comprehensive security hardening, CSRF protection, XSS prevention, SQL injection protection.<br><br><strong>Quality Assurance:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, WordPress Coding Standards compliant, PHP 8.2-8.5 compatible.",
"changelog": "<h3>1.0.2 - 2026-03-28</h3><ul><li><strong>Name:</strong> Renamed to \"Two-Factor Extended\" (hyphen added for consistency)</li><li><strong>Compatibility:</strong> Two-Factor 0.16 enforcement now respects globally-disabled providers</li><li><strong>i18n:</strong> Added Catalan (ca) translation (100% coverage); updated es_ES</li></ul><h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>", "changelog": "<h3>1.1.0 - 2026-03-28</h3><ul><li><strong>Code Quality:</strong> phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+</li></ul><h3>1.0.2 - 2026-03-28</h3><ul><li><strong>Name:</strong> Renamed to \"Two-Factor Extended\" (hyphen added for consistency)</li><li><strong>Compatibility:</strong> Two-Factor 0.16 enforcement now respects globally-disabled providers</li><li><strong>i18n:</strong> Added Catalan (ca) translation (100% coverage); updated es_ES</li></ul><h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
"installation": "<ol><li>Install the Two Factor plugin (required dependency)</li><li>Upload and activate Two-Factor Extended plugin</li><li>Go to Settings → Two-Factor Extended</li><li>Configure grace period (7-14 days recommended)</li><li>Set role-based 2FA requirements</li><li>Configure provider visibility per role</li><li>Monitor compliance via Compliance tab</li><li>Review audit logs via Audit Log tab</li></ol>", "installation": "<ol><li>Install the Two Factor plugin (required dependency)</li><li>Upload and activate Two-Factor Extended plugin</li><li>Go to Settings → Two-Factor Extended</li><li>Configure grace period (7-14 days recommended)</li><li>Set role-based 2FA requirements</li><li>Configure provider visibility per role</li><li>Monitor compliance via Compliance tab</li><li>Review audit logs via Audit Log tab</li></ol>",
"faq": "<h4>Does this plugin replace the Two Factor plugin?</h4><p>No, Two-Factor Extended is an extension that works alongside the Two Factor plugin. Both plugins must be installed and activated.</p><h4>Is this compatible with WordPress Multisite?</h4><p>Yes! Two-Factor Extended fully supports WordPress Multisite with network-wide settings and site-level overrides.</p><h4>Which PHP versions are supported?</h4><p>Two-Factor Extended requires PHP 8.2 or higher.</p><h4>Can users bypass the 2FA requirements?</h4><p>No. When a 2FA method is marked as required for a user's role, they cannot disable or remove it.</p>" "faq": "<h4>Does this plugin replace the Two Factor plugin?</h4><p>No, Two-Factor Extended is an extension that works alongside the Two Factor plugin. Both plugins must be installed and activated.</p><h4>Is this compatible with WordPress Multisite?</h4><p>Yes! Two-Factor Extended fully supports WordPress Multisite with network-wide settings and site-level overrides.</p><h4>Which PHP versions are supported?</h4><p>Two-Factor Extended requires PHP 8.2 or higher.</p><h4>Can users bypass the 2FA requirements?</h4><p>No. When a 2FA method is marked as required for a user's role, they cannot disable or remove it.</p>"
}, },
@ -27,4 +27,4 @@
"2x": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/raw/branch/main/assets/icon-256x256.png", "2x": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/raw/branch/main/assets/icon-256x256.png",
"svg": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/raw/branch/main/assets/icon.svg" "svg": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/raw/branch/main/assets/icon.svg"
} }
} }