diff --git a/changelog.txt b/changelog.txt index ac82bca..cf903ac 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,24 @@ == Changelog == += 1.1.1 = + +_Release date: 2026-03-28_ + +**Fixed** + +* Version constant mismatch: `TWO_FACTOR_EXTENDED_VERSION` was `1.0.2` instead of `1.1.0`. +* Replaced `current_time('timestamp')` with `time()` in audit log and settings export to ensure UTC-correct timestamps. +* Added `wp_unslash()` before sanitizing `$_GET['tab']` in the settings page renderer. + +**Compatibility** + +* WordPress: 6.8, 6.9, 7.0 +* 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 7.0, PHP 8.5.3; PHPUnit 9.6.34 — 28 unit tests, 64 assertions, all passing + = 1.1.0 = _Release date: 2026-03-28_ diff --git a/includes/class-audit-log.php b/includes/class-audit-log.php index 18d656e..95cf7b5 100644 --- a/includes/class-audit-log.php +++ b/includes/class-audit-log.php @@ -86,7 +86,7 @@ class Two_Factor_Extended_Audit_Log { $logs = $this->get_logs(); $log_entry = array( - 'timestamp' => current_time( 'timestamp' ), + 'timestamp' => time(), 'action' => sanitize_key( $action ), 'description' => sanitize_text_field( $description ), 'user_id' => $user_id, @@ -302,7 +302,7 @@ class Two_Factor_Extended_Audit_Log { */ public function cleanup_old_logs(): void { $logs = $this->get_logs(); - $cutoff_time = current_time( 'timestamp' ) - ( self::RETENTION_DAYS * DAY_IN_SECONDS ); + $cutoff_time = time() - ( self::RETENTION_DAYS * DAY_IN_SECONDS ); $filtered_logs = array_filter( $logs, @@ -429,7 +429,7 @@ class Two_Factor_Extended_Audit_Log { 'recent_count' => 0, ); - $recent_cutoff = current_time( 'timestamp' ) - ( 7 * DAY_IN_SECONDS ); + $recent_cutoff = time() - ( 7 * DAY_IN_SECONDS ); foreach ( $logs as $log ) { $action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : ''; diff --git a/includes/class-settings.php b/includes/class-settings.php index 609cc9b..309a5e5 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -769,7 +769,7 @@ class Two_Factor_Extended_Settings { $export_data = array( 'version' => TWO_FACTOR_EXTENDED_VERSION, - 'timestamp' => current_time( 'timestamp' ), + 'timestamp' => time(), 'settings' => $settings, ); @@ -968,9 +968,8 @@ class Two_Factor_Extended_Settings { } // Get current tab. - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab parameter for UI navigation - $tab_param = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? $_GET['tab'] : ''; - $current_tab = $tab_param ? sanitize_key( $tab_param ) : 'settings'; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab parameter for UI navigation; no state change + $current_tab = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; // Define tabs. $tabs = array( diff --git a/install-wp-tests.sh b/install-wp-tests.sh new file mode 100644 index 0000000..03cf020 --- /dev/null +++ b/install-wp-tests.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash + +# See https://raw.githubusercontent.com/wp-cli/scaffold-command/master/templates/install-wp-tests.sh + +# Set up colors for output +RED="\033[0;31m" +GREEN="\033[0;32m" +YELLOW="\033[0;33m" +CYAN="\033[0;36m" +RESET="\033[0m" + +if [ $# -lt 3 ]; then + echo -e "${YELLOW}Usage:${RESET} $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_TESTS_FILE="$WP_TESTS_DIR"/includes/functions.php +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} +WP_CORE_FILE="$WP_CORE_DIR"/wp-settings.php + +download() { + if command -v curl > /dev/null 2>&1; then + curl -L -s "$1" > "$2"; + return $? + elif command -v wget > /dev/null 2>&1; then + wget -nv -O "$2" "$1" + return $? + else + echo -e "${RED}Error: Neither curl nor wget is installed.${RESET}" + exit 1 + fi +} + +check_for_updates() { + local remote_url="https://raw.githubusercontent.com/wp-cli/scaffold-command/main/templates/install-wp-tests.sh" + local tmp_script="$TMPDIR/install-wp-tests.sh.latest" + + if ! download "$remote_url" "$tmp_script"; then + echo -e "${YELLOW}Warning: Failed to download the latest version of the script for update check.${RESET}" + return + fi + + if [ ! -f "$tmp_script" ] || [ ! -s "$tmp_script" ]; then + echo -e "${YELLOW}Warning: Downloaded script is missing or empty, cannot check for updates.${RESET}" + rm -f "$tmp_script" + return + fi + + local local_hash="" + local remote_hash="" + + if command -v shasum > /dev/null; then + local_hash=$(shasum -a 256 "$0" | awk '{print $1}') + remote_hash=$(shasum -a 256 "$tmp_script" | awk '{print $1}') + elif command -v sha256sum > /dev/null; then + local_hash=$(sha256sum "$0" | awk '{print $1}') + remote_hash=$(sha256sum "$tmp_script" | awk '{print $1}') + else + echo -e "${YELLOW}Warning: Could not find shasum or sha256sum to check for script updates.${RESET}" + rm "$tmp_script" + return + fi + + rm "$tmp_script" + + if [ "$local_hash" != "$remote_hash" ]; then + echo -e "${YELLOW}Warning: A newer version of this script is available at $remote_url${RESET}" + fi +} +# Allow disabling the update check by setting WP_INSTALL_TESTS_SKIP_UPDATE_CHECK=true in the environment. +if [ "${WP_INSTALL_TESTS_SKIP_UPDATE_CHECK:-false}" != "true" ]; then + check_for_updates +fi + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + LATEST_VERSION=$(grep -oE '"version":"[^"]*' /tmp/wp-latest.json | head -n 1 | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo -e "${RED}Error: Latest WordPress version could not be found.${RESET}" + exit 1 + fi + # The version-check endpoint returns major.minor (e.g., 6.9), but GitHub tags include the patch version (e.g., 6.9.0) + if [[ $LATEST_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + LATEST_VERSION="${LATEST_VERSION}.0" + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -f $WP_CORE_FILE ]; then + echo -e "${CYAN}WordPress is already installed.${RESET}" + return; + fi + + echo -e "${CYAN}Installing WordPress...${RESET}" + + rm -rf $WP_CORE_DIR + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + download https://github.com/WordPress/wordpress/archive/refs/heads/master.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + echo -e "${GREEN}WordPress installed successfully.${RESET}" +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist or only partially exists + if [ ! -f $WP_TESTS_FILE ]; then + echo -e "${CYAN}Installing test suite...${RESET}" + # set up testing suite + rm -rf $WP_TESTS_DIR + mkdir -p $WP_TESTS_DIR + + if [[ $WP_TESTS_TAG == 'trunk' ]]; then + ref=trunk + archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/heads/${ref}.tar.gz" + elif [[ $WP_TESTS_TAG == branches/* ]]; then + ref=${WP_TESTS_TAG#branches/} + archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/heads/${ref}.tar.gz" + else + ref=${WP_TESTS_TAG#tags/} + archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/tags/${ref}.tar.gz" + fi + + if [ -z "$ref" ]; then + echo -e "${RED}Error:${RESET} Unable to determine git reference from WP_TESTS_TAG: $WP_TESTS_TAG" + exit 1 + fi + + download "${archive_url}" "$TMPDIR/wordpress-develop.tar.gz" + + # Validate that the tarball was downloaded correctly before extracting + if [ ! -s "$TMPDIR/wordpress-develop.tar.gz" ]; then + echo -e "${RED}Error:${RESET} Downloaded test suite archive is missing or empty: $TMPDIR/wordpress-develop.tar.gz" + exit 1 + fi + + if ! tar -tzf "$TMPDIR/wordpress-develop.tar.gz" >/dev/null 2>&1; then + echo -e "${RED}Error:${RESET} Downloaded test suite archive is not a valid tar.gz file: $TMPDIR/wordpress-develop.tar.gz" + exit 1 + fi + + tar -zxmf "$TMPDIR/wordpress-develop.tar.gz" -C "$TMPDIR" + mv "$TMPDIR/wordpress-develop-${ref}/tests/phpunit/includes" "$WP_TESTS_DIR"/ + mv "$TMPDIR/wordpress-develop-${ref}/tests/phpunit/data" "$WP_TESTS_DIR"/ + rm -rf "$TMPDIR/wordpress-develop-${ref}" + rm "$TMPDIR/wordpress-develop.tar.gz" + echo -e "${GREEN}Test suite installed.${RESET}" + else + echo -e "${CYAN}Test suite is already installed.${RESET}" + fi + + if [ ! -f "$WP_TESTS_DIR"/wp-tests-config.php ]; then + echo -e "${CYAN}Configuring test suite...${RESET}" + if [[ $WP_TESTS_TAG == 'trunk' ]]; then + ref=trunk + elif [[ $WP_TESTS_TAG == branches/* ]]; then + ref=${WP_TESTS_TAG#branches/} + else + ref=${WP_TESTS_TAG#tags/} + fi + + if [ -z "$ref" ]; then + echo -e "${RED}Error:${RESET} Unable to determine git reference from WP_TESTS_TAG: $WP_TESTS_TAG" + exit 1 + fi + + download https://raw.githubusercontent.com/WordPress/wordpress-develop/${ref}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + echo -e "${GREEN}Test suite configured.${RESET}" + else + echo -e "${CYAN}Test suite is already configured.${RESET}" + fi + +} + +recreate_db() { + shopt -s nocasematch + if [[ $1 =~ ^(y|yes)$ ]] + then + echo -e "${CYAN}Recreating the database ($DB_NAME)...${RESET}" + if command -v mariadb-admin > /dev/null 2>&1; then + mariadb-admin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + else + mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + fi + create_db + echo -e "${GREEN}Database ($DB_NAME) recreated.${RESET}" + else + echo -e "${YELLOW}Leaving the existing database ($DB_NAME) in place.${RESET}" + fi + shopt -u nocasematch +} + +create_db() { + if command -v mariadb-admin > /dev/null 2>&1; then + mariadb-admin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA + else + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA + fi +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + echo -e "${YELLOW}Skipping database creation.${RESET}" + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + if command -v mariadb > /dev/null 2>&1; then + local DB_CLIENT='mariadb' + else + local DB_CLIENT='mysql' + fi + if $DB_CLIENT --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep -q "^$DB_NAME$"; + then + echo -e "${YELLOW}Reinstalling will delete the existing test database ($DB_NAME)${RESET}" + read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB + recreate_db $DELETE_EXISTING_DB + else + echo -e "${CYAN}Creating database ($DB_NAME)...${RESET}" + create_db + echo -e "${GREEN}Database ($DB_NAME) created.${RESET}" + fi +} + +install_wp +install_test_suite +install_db +echo -e "${GREEN}Done.${RESET}" diff --git a/readme.txt b/readme.txt index 6994db2..19d0853 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === Two-Factor Extended === Contributors: javiercasares, robotstxt Tags: two-factor, 2fa, authentication, security -Requires at least: 6.7 -Tested up to: 6.9 -Stable tag: 1.1.0 +Requires at least: 6.8 +Tested up to: 7.0 +Stable tag: 1.1.1 Requires PHP: 8.2 -Version: 1.1.0 +Version: 1.1.1 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt @@ -117,6 +117,25 @@ We take security seriously and will respond promptly to all security reports. == Changelog == += 1.1.1 = + +_Release date: 2026-03-28_ + +**Fixed** + +* Version constant mismatch: `TWO_FACTOR_EXTENDED_VERSION` was `1.0.2` instead of `1.1.0`. +* Replaced `current_time('timestamp')` with `time()` in audit log and settings export to ensure UTC-correct timestamps. +* Added `wp_unslash()` before sanitizing `$_GET['tab']` in the settings page renderer. + +**Compatibility** + +* WordPress: 6.8, 6.9, 7.0 +* 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 7.0, PHP 8.5.3; PHPUnit 9.6.34 — 28 unit tests, 64 assertions, all passing + = 1.1.0 = _Release date: 2026-03-28_ diff --git a/two-factor-extended.php b/two-factor-extended.php index 264776a..51cabf4 100644 --- a/two-factor-extended.php +++ b/two-factor-extended.php @@ -3,8 +3,8 @@ * Plugin Name: 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. - * Version: 1.1.0 - * Requires at least: 6.7 + * Version: 1.1.1 + * Requires at least: 6.8 * Requires PHP: 8.2 * Requires Plugins: two-factor * Author: ROBOTSTXT @@ -18,7 +18,7 @@ * Primary Branch: main * * @package TwoFactorExtended - * @version 1.1.0 + * @version 1.1.1 */ // Prevent direct access. @@ -27,7 +27,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Define plugin constants. -define( 'TWO_FACTOR_EXTENDED_VERSION', '1.0.2' ); +define( 'TWO_FACTOR_EXTENDED_VERSION', '1.1.1' ); define( 'TWO_FACTOR_EXTENDED_PLUGIN_FILE', __FILE__ ); define( 'TWO_FACTOR_EXTENDED_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'TWO_FACTOR_EXTENDED_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); diff --git a/update.json b/update.json index 74f41a5..ce3d265 100644 --- a/update.json +++ b/update.json @@ -1,20 +1,20 @@ { "name": "Two-Factor Extended", "slug": "two-factor-extended", - "version": "1.1.0", - "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", + "version": "1.1.1", + "download_url": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/releases/download/1.1.1/two-factor-extended-1.1.1.zip", + "requires": "6.8", "requires_php": "8.2", - "tested": "6.9", + "tested": "7.0", "last_updated": "2026-03-28", "author": "javiercasares, ROBOTSTXT", "author_profile": "https://www.robotstxt.es/", "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.", - "changelog": "

1.1.0 - 2026-03-28

  • Code Quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+

1.0.2 - 2026-03-28

  • Name: Renamed to \"Two-Factor Extended\" (hyphen added for consistency)
  • Compatibility: Two-Factor 0.16 enforcement now respects globally-disabled providers
  • i18n: Added Catalan (ca) translation (100% coverage); updated es_ES

1.0.1 - 2026-03-28

  • Compatibility: Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin
  • Code Quality: PHPStan level 9 clean, PHPCS WordPress Coding Standards clean
  • Tests: PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)

1.0.0 - 2026-02-17

  • Initial Production Release: Complete enterprise-level two-factor authentication management for WordPress
  • Core Features: Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support
  • Advanced Features: Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings
  • Security: Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention
  • Quality: 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested
", + "changelog": "

1.1.1 - 2026-03-28

  • Fixed: Version constant mismatch — TWO_FACTOR_EXTENDED_VERSION was 1.0.2 instead of 1.1.0
  • Fixed: Replaced current_time('timestamp') with time() for UTC-correct timestamps
  • Fixed: Added wp_unslash() before sanitizing $_GET['tab']
  • Compatibility: WordPress 6.8-7.0, PHP 8.2-8.5, Two-Factor 0.16+

1.1.0 - 2026-03-28

  • Code Quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+

1.0.2 - 2026-03-28

  • Name: Renamed to \"Two-Factor Extended\" (hyphen added for consistency)
  • Compatibility: Two-Factor 0.16 enforcement now respects globally-disabled providers
  • i18n: Added Catalan (ca) translation (100% coverage); updated es_ES

1.0.1 - 2026-03-28

  • Compatibility: Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin
  • Code Quality: PHPStan level 9 clean, PHPCS WordPress Coding Standards clean
  • Tests: PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)

1.0.0 - 2026-02-17

  • Initial Production Release: Complete enterprise-level two-factor authentication management for WordPress
  • Core Features: Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support
  • Advanced Features: Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings
  • Security: Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention
  • Quality: 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested
", "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.

Core Features:
  • Role-Based 2FA Requirements
  • Provider Visibility Control
  • Grace Period Enforcement (0-365 days)
  • WordPress Multisite Support
  • Compliance Reporting
  • Audit Logging
  • Bulk Operations
  • WP-CLI Commands
  • REST API Endpoints
  • Import/Export Settings
Security: Grade A security audit, comprehensive security hardening, CSRF protection, XSS prevention, SQL injection protection.

Quality Assurance: 28 unit tests, WCAG 2.1 Level AA compliant, WordPress Coding Standards compliant, PHP 8.2-8.5 compatible.", - "changelog": "

1.1.0 - 2026-03-28

  • Code Quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+

1.0.2 - 2026-03-28

  • Name: Renamed to \"Two-Factor Extended\" (hyphen added for consistency)
  • Compatibility: Two-Factor 0.16 enforcement now respects globally-disabled providers
  • i18n: Added Catalan (ca) translation (100% coverage); updated es_ES

1.0.1 - 2026-03-28

  • Compatibility: Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin
  • Code Quality: PHPStan level 9 clean, PHPCS WordPress Coding Standards clean
  • Tests: PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)

1.0.0 - 2026-02-17

  • Initial Production Release: Complete enterprise-level two-factor authentication management for WordPress
  • Core Features: Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support
  • Advanced Features: Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings
  • Security: Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention
  • Quality: 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested
", + "changelog": "

1.1.1 - 2026-03-28

  • Fixed: Version constant mismatch — TWO_FACTOR_EXTENDED_VERSION was 1.0.2 instead of 1.1.0
  • Fixed: Replaced current_time('timestamp') with time() for UTC-correct timestamps
  • Fixed: Added wp_unslash() before sanitizing $_GET['tab']
  • Compatibility: WordPress 6.8-7.0, PHP 8.2-8.5, Two-Factor 0.16+

1.1.0 - 2026-03-28

  • Code Quality: phpcbf auto-fixed 112 formatting issues across 12 files (operator alignment, array double arrows, pre-increment style)
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two-Factor 0.16+

1.0.2 - 2026-03-28

  • Name: Renamed to \"Two-Factor Extended\" (hyphen added for consistency)
  • Compatibility: Two-Factor 0.16 enforcement now respects globally-disabled providers
  • i18n: Added Catalan (ca) translation (100% coverage); updated es_ES

1.0.1 - 2026-03-28

  • Compatibility: Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin
  • Code Quality: PHPStan level 9 clean, PHPCS WordPress Coding Standards clean
  • Tests: PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)

1.0.0 - 2026-02-17

  • Initial Production Release: Complete enterprise-level two-factor authentication management for WordPress
  • Core Features: Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support
  • Advanced Features: Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings
  • Security: Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention
  • Quality: 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant
  • Compatibility: WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested
", "installation": "
  1. Install the Two Factor plugin (required dependency)
  2. Upload and activate Two-Factor Extended plugin
  3. Go to Settings → Two-Factor Extended
  4. Configure grace period (7-14 days recommended)
  5. Set role-based 2FA requirements
  6. Configure provider visibility per role
  7. Monitor compliance via Compliance tab
  8. Review audit logs via Audit Log tab
", "faq": "

Does this plugin replace the Two Factor plugin?

No, Two-Factor Extended is an extension that works alongside the Two Factor plugin. Both plugins must be installed and activated.

Is this compatible with WordPress Multisite?

Yes! Two-Factor Extended fully supports WordPress Multisite with network-wide settings and site-level overrides.

Which PHP versions are supported?

Two-Factor Extended requires PHP 8.2 or higher.

Can users bypass the 2FA requirements?

No. When a 2FA method is marked as required for a user's role, they cannot disable or remove it.

" },