Plugin Directory

Changeset 3752373


Ignore:
Timestamp:
10/17/2026 09:24:28 PM (81 minutes ago)
Author:
rebelcode
Message:

Update to version 0.4.0 from GitHub

Location:
sigil-2fa
Files:
6 added
10 edited
1 copied

Legend:

Unmodified
Added
Removed
  • sigil-2fa/tags/0.4.0/includes/class-app-passwords.php

    r3631768 r3752373  
    1111        private static ?App_Passwords $instance = null;
    1212
     13        /** @var array<int, true> */
     14        private array $app_password_users = array();
     15
    1316        public static function instance(): App_Passwords {
    1417                if ( null === self::$instance ) {
    … …  
    2023        private function __construct() {
    2124                add_filter( 'wp_is_application_passwords_available_for_user', array( $this, 'filter_available' ), 10, 2 );
     25                add_action( 'application_password_did_authenticate', array( $this, 'note_application_password' ) );
     26                add_filter( 'authenticate', array( $this, 'refuse_api_password' ), 40 );
     27        }
     28
     29        /**
     30         * @param mixed $user
     31         */
     32        public function note_application_password( $user ): void {
     33                if ( $user instanceof \WP_User ) {
     34                        $this->app_password_users[ (int) $user->ID ] = true;
     35                }
     36        }
     37
     38        /**
     39         * Refuse the account password on XML-RPC and REST for anyone with a second
     40         * factor.
     41         *
     42         * Those requests never reach wp_login, where the challenge runs, so a
     43         * password accepted there is a sign-in with one factor. Application
     44         * passwords are the way in for API clients and are left alone.
     45         *
     46         * @param mixed $user
     47         * @return mixed
     48         */
     49        public function refuse_api_password( $user ) {
     50                if ( ! $user instanceof \WP_User || ! self::is_api_request() ) {
     51                        return $user;
     52                }
     53
     54                if ( isset( $this->app_password_users[ (int) $user->ID ] ) ) {
     55                        return $user;
     56                }
     57
     58                if ( ! Providers::instance()->has_usable( (int) $user->ID ) ) {
     59                        return $user;
     60                }
     61
     62                return new \WP_Error(
     63                        'sigil_api_password_refused',
     64                        __( 'This account uses two-factor authentication. Use an application password for API access.', 'sigil-2fa' )
     65                );
     66        }
     67
     68        public static function is_api_request(): bool {
     69                return ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST );
    2270        }
    2371
  • sigil-2fa/tags/0.4.0/includes/class-cli.php

    r3634087 r3752373  
    146146
    147147        /**
     148         * Import users' two-factor methods from another plugin.
     149         *
     150         * Reads the source plugin's data and never changes or deletes it. Anything
     151         * already set up in Sigil is left alone, so it is safe to run again.
     152         *
     153         * ## OPTIONS
     154         *
     155         * <source>
     156         * : Plugin to import from.
     157         * ---
     158         * options:
     159         *   - two-factor
     160         * ---
     161         *
     162         * [--dry-run]
     163         * : Report what would be imported without writing anything.
     164         *
     165         * [--user=<user>]
     166         * : Only this user (ID, login, or email).
     167         *
     168         * ## EXAMPLES
     169         *
     170         *     wp sigil import two-factor --dry-run
     171         *     wp sigil import two-factor
     172         *     wp sigil import two-factor --user=12
     173         *
     174         * @param list<string>               $args
     175         * @param array<string, string|bool> $assoc_args
     176         */
     177        public function import( array $args, array $assoc_args = [] ): void {
     178                if ( 'two-factor' !== ( $args[0] ?? '' ) ) {
     179                        \WP_CLI::error( 'Unknown source. Supported: two-factor' );
     180                }
     181
     182                $dry_run = ! empty( $assoc_args['dry-run'] );
     183                $ids     = null;
     184
     185                if ( isset( $assoc_args['user'] ) ) {
     186                        $user = $this->resolve_user( (string) $assoc_args['user'] );
     187                        if ( null === $user ) {
     188                                \WP_CLI::error( 'User not found.' );
     189                        }
     190                        $ids = [ (int) $user->ID ];
     191                }
     192
     193                $report = Two_Factor_Import::run( $ids, $dry_run );
     194
     195                $items = [];
     196                foreach ( $report['users'] as $row ) {
     197                        $items[] = [
     198                                'ID'        => $row['user_id'],
     199                                'login'     => $row['login'],
     200                                'status'    => $row['status'],
     201                                'imported'  => implode( ',', $row['imported'] ),
     202                                'already'   => implode( ',', $row['already'] ),
     203                                'preferred' => $row['preferred'],
     204                                'flags'     => implode( ',', $row['flags'] ),
     205                        ];
     206                }
     207
     208                if ( [] !== $items ) {
     209                        \WP_CLI\Utils\format_items( 'table', $items, [ 'ID', 'login', 'status', 'imported', 'already', 'preferred', 'flags' ] );
     210                }
     211
     212                $c = $report['counts'];
     213                \WP_CLI::log(
     214                        sprintf(
     215                                '%sUsers: %d, not using Two Factor: %d, email: %d, authenticator: %d, backup codes: %d, already in Sigil: %d, flagged: %d, needs attention: %d',
     216                                $dry_run ? '[dry run] ' : '',
     217                                $c['users'],
     218                                $c['not_using'],
     219                                $c['email'],
     220                                $c['totp'],
     221                                $c['backup'],
     222                                $c['already'],
     223                                $c['flagged'],
     224                                $c['attention']
     225                        )
     226                );
     227
     228                if ( $c['attention'] > 0 ) {
     229                        \WP_CLI::warning( 'Some users had a method Sigil could not carry over. Two Factor keeps protecting them while it is active, so leave it active until they have set up a method in Sigil.' );
     230                        return;
     231                }
     232
     233                \WP_CLI::success( $dry_run ? 'Dry run complete. Nothing was written.' : 'Import complete. Two Factor data was left in place; deactivate Two Factor once you have checked the result.' );
     234        }
     235
     236        /**
    148237         * @return list<string>
    149238         */
  • sigil-2fa/tags/0.4.0/includes/class-plugin.php

    r3635650 r3752373  
    6767                Frontend::instance();
    6868
     69                require_once SIGIL_DIR . 'includes/class-two-factor-import.php';
     70                require_once SIGIL_DIR . 'includes/class-two-factor-migration.php';
     71                Two_Factor_Migration::instance();
     72
    6973                // Pro add-on, when present. The free build strips pro/ entirely, so this
    7074                // include only fires in the Pro distribution.
  • sigil-2fa/tags/0.4.0/readme.txt

    r3635650 r3752373  
    33Tags: two-factor, 2fa, passkeys, authentication, security
    44Requires at least: 6.9
    5 Tested up to: 7.0
     5Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.3.3
     7Stable tag: 0.4.0
    88License: GPL-3.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    8383= Does this work with application passwords, the REST API, and XML-RPC? =
    8484
    85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path.
     85Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. The account password itself is refused on XML-RPC and REST for anyone with a second factor, because those requests never reach the challenge.
    8686
    8787= Which PHP version do I need for passkeys? =
    … …  
    9797Yes. Put `[sigil_2fa]` on a page. Users manage their methods from there, and anyone required to enrol is sent to that page rather than to the dashboard.
    9898
     99= Can I switch from the Two Factor plugin? =
     100
     101Yes. With both plugins active, open Settings → Sigil and use Import from Two Factor, or run `wp sigil import two-factor` (add `--dry-run` to preview). Email codes, authenticator apps and backup codes come across, so people keep signing in with the same app. Anyone with a method that cannot be carried over is listed in the report, and Two Factor keeps protecting them while it stays active. Two Factor's own data is not changed. Once everyone on that list has set up a method in Sigil, deactivate Two Factor.
     102
    99103= Can I enforce 2FA only for administrators? =
    100104
    … …  
    102106
    103107== Changelog ==
     108
     109= 0.4.0 =
     110* Moving from the Two Factor plugin no longer means everyone enrols again. Settings → Sigil and `wp sigil import two-factor` carry each user's email codes, authenticator app and backup codes across, so the same authenticator keeps working. Anyone with a method that cannot be carried over is named in the report and stays with Two Factor until they set one up in Sigil. Nobody is moved to email codes they did not already use, and Two Factor's own data is left as it was.
     111* While both plugins are active, Two Factor stands down for anyone Sigil already covers, so nobody is asked for a code twice. A notice offers the import until Two Factor is deactivated.
     112* Security: XML-RPC and REST no longer accept the account password from someone with a second factor. Those requests never reach the two-factor challenge, so a password alone was enough there. Application passwords still work.
    104113
    105114= 0.3.3 =
  • sigil-2fa/tags/0.4.0/sigil-2fa.php

    r3635650 r3752373  
    33 * Plugin Name: Sigil – Passkeys and Two-Factor Authentication
    44 * Description: Two-factor authentication for WordPress logins: passkeys, authenticator apps, backup codes and email codes, with per-role enforcement, multisite support and account recovery.
    5  * Version: 0.3.3
     5 * Version: 0.4.0
    66 * Requires at least: 6.9
    77 * Requires PHP: 7.4
    … …  
    1818defined( 'ABSPATH' ) || exit;
    1919
    20 define( 'SIGIL_VERSION', '0.3.3' );
     20define( 'SIGIL_VERSION', '0.4.0' );
    2121define( 'SIGIL_FILE', __FILE__ );
    2222define( 'SIGIL_DIR', plugin_dir_path( __FILE__ ) );
  • sigil-2fa/trunk/includes/class-app-passwords.php

    r3631768 r3752373  
    1111        private static ?App_Passwords $instance = null;
    1212
     13        /** @var array<int, true> */
     14        private array $app_password_users = array();
     15
    1316        public static function instance(): App_Passwords {
    1417                if ( null === self::$instance ) {
    … …  
    2023        private function __construct() {
    2124                add_filter( 'wp_is_application_passwords_available_for_user', array( $this, 'filter_available' ), 10, 2 );
     25                add_action( 'application_password_did_authenticate', array( $this, 'note_application_password' ) );
     26                add_filter( 'authenticate', array( $this, 'refuse_api_password' ), 40 );
     27        }
     28
     29        /**
     30         * @param mixed $user
     31         */
     32        public function note_application_password( $user ): void {
     33                if ( $user instanceof \WP_User ) {
     34                        $this->app_password_users[ (int) $user->ID ] = true;
     35                }
     36        }
     37
     38        /**
     39         * Refuse the account password on XML-RPC and REST for anyone with a second
     40         * factor.
     41         *
     42         * Those requests never reach wp_login, where the challenge runs, so a
     43         * password accepted there is a sign-in with one factor. Application
     44         * passwords are the way in for API clients and are left alone.
     45         *
     46         * @param mixed $user
     47         * @return mixed
     48         */
     49        public function refuse_api_password( $user ) {
     50                if ( ! $user instanceof \WP_User || ! self::is_api_request() ) {
     51                        return $user;
     52                }
     53
     54                if ( isset( $this->app_password_users[ (int) $user->ID ] ) ) {
     55                        return $user;
     56                }
     57
     58                if ( ! Providers::instance()->has_usable( (int) $user->ID ) ) {
     59                        return $user;
     60                }
     61
     62                return new \WP_Error(
     63                        'sigil_api_password_refused',
     64                        __( 'This account uses two-factor authentication. Use an application password for API access.', 'sigil-2fa' )
     65                );
     66        }
     67
     68        public static function is_api_request(): bool {
     69                return ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST );
    2270        }
    2371
  • sigil-2fa/trunk/includes/class-cli.php

    r3634087 r3752373  
    146146
    147147        /**
     148         * Import users' two-factor methods from another plugin.
     149         *
     150         * Reads the source plugin's data and never changes or deletes it. Anything
     151         * already set up in Sigil is left alone, so it is safe to run again.
     152         *
     153         * ## OPTIONS
     154         *
     155         * <source>
     156         * : Plugin to import from.
     157         * ---
     158         * options:
     159         *   - two-factor
     160         * ---
     161         *
     162         * [--dry-run]
     163         * : Report what would be imported without writing anything.
     164         *
     165         * [--user=<user>]
     166         * : Only this user (ID, login, or email).
     167         *
     168         * ## EXAMPLES
     169         *
     170         *     wp sigil import two-factor --dry-run
     171         *     wp sigil import two-factor
     172         *     wp sigil import two-factor --user=12
     173         *
     174         * @param list<string>               $args
     175         * @param array<string, string|bool> $assoc_args
     176         */
     177        public function import( array $args, array $assoc_args = [] ): void {
     178                if ( 'two-factor' !== ( $args[0] ?? '' ) ) {
     179                        \WP_CLI::error( 'Unknown source. Supported: two-factor' );
     180                }
     181
     182                $dry_run = ! empty( $assoc_args['dry-run'] );
     183                $ids     = null;
     184
     185                if ( isset( $assoc_args['user'] ) ) {
     186                        $user = $this->resolve_user( (string) $assoc_args['user'] );
     187                        if ( null === $user ) {
     188                                \WP_CLI::error( 'User not found.' );
     189                        }
     190                        $ids = [ (int) $user->ID ];
     191                }
     192
     193                $report = Two_Factor_Import::run( $ids, $dry_run );
     194
     195                $items = [];
     196                foreach ( $report['users'] as $row ) {
     197                        $items[] = [
     198                                'ID'        => $row['user_id'],
     199                                'login'     => $row['login'],
     200                                'status'    => $row['status'],
     201                                'imported'  => implode( ',', $row['imported'] ),
     202                                'already'   => implode( ',', $row['already'] ),
     203                                'preferred' => $row['preferred'],
     204                                'flags'     => implode( ',', $row['flags'] ),
     205                        ];
     206                }
     207
     208                if ( [] !== $items ) {
     209                        \WP_CLI\Utils\format_items( 'table', $items, [ 'ID', 'login', 'status', 'imported', 'already', 'preferred', 'flags' ] );
     210                }
     211
     212                $c = $report['counts'];
     213                \WP_CLI::log(
     214                        sprintf(
     215                                '%sUsers: %d, not using Two Factor: %d, email: %d, authenticator: %d, backup codes: %d, already in Sigil: %d, flagged: %d, needs attention: %d',
     216                                $dry_run ? '[dry run] ' : '',
     217                                $c['users'],
     218                                $c['not_using'],
     219                                $c['email'],
     220                                $c['totp'],
     221                                $c['backup'],
     222                                $c['already'],
     223                                $c['flagged'],
     224                                $c['attention']
     225                        )
     226                );
     227
     228                if ( $c['attention'] > 0 ) {
     229                        \WP_CLI::warning( 'Some users had a method Sigil could not carry over. Two Factor keeps protecting them while it is active, so leave it active until they have set up a method in Sigil.' );
     230                        return;
     231                }
     232
     233                \WP_CLI::success( $dry_run ? 'Dry run complete. Nothing was written.' : 'Import complete. Two Factor data was left in place; deactivate Two Factor once you have checked the result.' );
     234        }
     235
     236        /**
    148237         * @return list<string>
    149238         */
  • sigil-2fa/trunk/includes/class-plugin.php

    r3635650 r3752373  
    6767                Frontend::instance();
    6868
     69                require_once SIGIL_DIR . 'includes/class-two-factor-import.php';
     70                require_once SIGIL_DIR . 'includes/class-two-factor-migration.php';
     71                Two_Factor_Migration::instance();
     72
    6973                // Pro add-on, when present. The free build strips pro/ entirely, so this
    7074                // include only fires in the Pro distribution.
  • sigil-2fa/trunk/readme.txt

    r3635650 r3752373  
    33Tags: two-factor, 2fa, passkeys, authentication, security
    44Requires at least: 6.9
    5 Tested up to: 7.0
     5Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.3.3
     7Stable tag: 0.4.0
    88License: GPL-3.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    8383= Does this work with application passwords, the REST API, and XML-RPC? =
    8484
    85 Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path.
     85Application passwords bypass 2FA by design, as that is how WordPress authenticates automated requests. The settings screen documents this, and application passwords can be disabled per role to close that path. The account password itself is refused on XML-RPC and REST for anyone with a second factor, because those requests never reach the challenge.
    8686
    8787= Which PHP version do I need for passkeys? =
    … …  
    9797Yes. Put `[sigil_2fa]` on a page. Users manage their methods from there, and anyone required to enrol is sent to that page rather than to the dashboard.
    9898
     99= Can I switch from the Two Factor plugin? =
     100
     101Yes. With both plugins active, open Settings → Sigil and use Import from Two Factor, or run `wp sigil import two-factor` (add `--dry-run` to preview). Email codes, authenticator apps and backup codes come across, so people keep signing in with the same app. Anyone with a method that cannot be carried over is listed in the report, and Two Factor keeps protecting them while it stays active. Two Factor's own data is not changed. Once everyone on that list has set up a method in Sigil, deactivate Two Factor.
     102
    99103= Can I enforce 2FA only for administrators? =
    100104
    … …  
    102106
    103107== Changelog ==
     108
     109= 0.4.0 =
     110* Moving from the Two Factor plugin no longer means everyone enrols again. Settings → Sigil and `wp sigil import two-factor` carry each user's email codes, authenticator app and backup codes across, so the same authenticator keeps working. Anyone with a method that cannot be carried over is named in the report and stays with Two Factor until they set one up in Sigil. Nobody is moved to email codes they did not already use, and Two Factor's own data is left as it was.
     111* While both plugins are active, Two Factor stands down for anyone Sigil already covers, so nobody is asked for a code twice. A notice offers the import until Two Factor is deactivated.
     112* Security: XML-RPC and REST no longer accept the account password from someone with a second factor. Those requests never reach the two-factor challenge, so a password alone was enough there. Application passwords still work.
    104113
    105114= 0.3.3 =
  • sigil-2fa/trunk/sigil-2fa.php

    r3635650 r3752373  
    33 * Plugin Name: Sigil – Passkeys and Two-Factor Authentication
    44 * Description: Two-factor authentication for WordPress logins: passkeys, authenticator apps, backup codes and email codes, with per-role enforcement, multisite support and account recovery.
    5  * Version: 0.3.3
     5 * Version: 0.4.0
    66 * Requires at least: 6.9
    77 * Requires PHP: 7.4
    … …  
    1818defined( 'ABSPATH' ) || exit;
    1919
    20 define( 'SIGIL_VERSION', '0.3.3' );
     20define( 'SIGIL_VERSION', '0.4.0' );
    2121define( 'SIGIL_FILE', __FILE__ );
    2222define( 'SIGIL_DIR', plugin_dir_path( __FILE__ ) );
Note: See TracChangeset for help on using the changeset viewer.