diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 90b6ebaf..99033a70 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -132,13 +132,67 @@ public static function add_hooks( $compat ) { } /** - * For each provider, include it and then instantiate it. + * Delete all plugin data on uninstall. * - * @since 0.1-dev + * @return void + */ + public static function uninstall() { + // Keep this updated as user meta keys are added or removed. + $user_meta_keys = array( + self::PROVIDER_USER_META_KEY, + self::ENABLED_PROVIDERS_USER_META_KEY, + self::USER_META_NONCE_KEY, + self::USER_RATE_LIMIT_KEY, + self::USER_FAILED_LOGIN_ATTEMPTS_KEY, + self::USER_PASSWORD_WAS_RESET_KEY, + ); + + $option_keys = array(); + + foreach ( self::get_providers_classes() as $provider_class ) { + // Merge with provider-specific user meta keys. + if ( method_exists( $provider_class, 'uninstall_user_meta_keys' ) ) { + try { + $user_meta_keys = array_merge( + $user_meta_keys, + call_user_func( array( $provider_class, 'uninstall_user_meta_keys' ) ) + ); + } catch ( Exception $e ) { + // Do nothing. + } + } + + // Merge with provider-specific option keys. + if ( method_exists( $provider_class, 'uninstall_options' ) ) { + try { + $option_keys = array_merge( + $option_keys, + call_user_func( array( $provider_class, 'uninstall_options' ) ) + ); + } catch ( Exception $e ) { + // Do nothing. + } + } + } + + // Delete options first since that is faster. + if ( ! empty( $option_keys ) ) { + foreach ( $option_keys as $option_key ) { + delete_option( $option_key ); + } + } + + foreach ( $user_meta_keys as $meta_key ) { + delete_metadata( 'user', null, $meta_key, null, true ); + } + } + + /** + * Get the registered providers of which some might not be enabled. * - * @return array + * @return array List of provider keys and paths to class files. */ - public static function get_providers() { + public static function get_providers_registered() { $providers = array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php', 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php', @@ -150,29 +204,29 @@ public static function get_providers() { /** * Filter the supplied providers. * - * This lets third-parties either remove providers (such as Email), or - * add their own providers (such as text message or Clef). - * * @param array $providers A key-value array where the key is the class name, and * the value is the path to the file containing the class. */ - $providers = apply_filters( 'two_factor_providers', $providers ); + $additional_providers = apply_filters( 'two_factor_providers', $providers ); - // FIDO U2F is PHP 5.3+ only. - if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { - unset( $providers['Two_Factor_FIDO_U2F'] ); - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - sprintf( - /* translators: %s: version number */ - __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - PHP_VERSION - ) - ); + // Merge them with the default providers. + if ( ! empty( $additional_providers ) ) { + return array_merge( $providers, $additional_providers ); } - /** - * For each filtered provider, - */ + return $providers; + } + + /** + * Get the classnames for all registered providers. + * + * Note some of these providers might not be enabled. + * + * @return array List of provider keys and classnames. + */ + private static function get_providers_classes() { + $providers = self::get_providers_registered(); + foreach ( $providers as $provider_key => $path ) { require_once $path; @@ -189,9 +243,56 @@ public static function get_providers() { /** * Confirm that it's been successfully included before instantiating. */ - if ( class_exists( $class ) ) { + if ( method_exists( $class, 'get_instance' ) ) { + $providers[ $provider_key ] = $class; + } else { + unset( $providers[ $provider_key ] ); + } + } + + return $providers; + } + + /** + * Get all enabled two-factor providers. + * + * @since 0.1-dev + * + * @return array + */ + public static function get_providers() { + $providers = self::get_providers_registered(); + + /** + * Filter the supplied providers. + * + * This lets third-parties either remove providers (such as Email), or + * add their own providers (such as text message or Clef). + * + * @param array $providers A key-value array where the key is the class name, and + * the value is the path to the file containing the class. + */ + $providers = apply_filters( 'two_factor_providers', $providers ); + + // FIDO U2F is PHP 5.3+ only. + if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { + unset( $providers['Two_Factor_FIDO_U2F'] ); + trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + sprintf( + /* translators: %s: version number */ + __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + PHP_VERSION + ) + ); + } + + // Map provider keys to classes so that we can instantiate them. + $providers = array_intersect_key( self::get_providers_classes(), $providers ); + + foreach ( $providers as $provider_key => $provider_class ) { + if ( method_exists( $provider_class, 'get_instance' ) ) { try { - $providers[ $provider_key ] = call_user_func( array( $class, 'get_instance' ) ); + $providers[ $provider_key ] = call_user_func( array( $provider_class, 'get_instance' ) ); } catch ( Exception $e ) { unset( $providers[ $provider_key ] ); } diff --git a/providers/class-two-factor-backup-codes.php b/providers/class-two-factor-backup-codes.php index 6b017de4..12601d5c 100644 --- a/providers/class-two-factor-backup-codes.php +++ b/providers/class-two-factor-backup-codes.php @@ -399,4 +399,15 @@ public function delete_code( $user, $code_hashed ) { // Update the backup code master list. update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes ); } + + /** + * Return user meta keys to delete during plugin uninstall. + * + * @return array + */ + public static function uninstall_user_meta_keys() { + return array( + self::BACKUP_CODES_META_KEY, + ); + } } diff --git a/providers/class-two-factor-email.php b/providers/class-two-factor-email.php index 0ab3bc18..da7a7c60 100644 --- a/providers/class-two-factor-email.php +++ b/providers/class-two-factor-email.php @@ -351,4 +351,16 @@ public function user_options( $user ) { assertCount( 1, $admin_session_manager->get_all(), 'No admin sessions are present first' ); } + + /** + * Plugin uninstall removes all user meta. + * + * @covers Two_Factor_Core::uninstall + */ + public function test_uninstall_removes_user_meta() { + $user = self::factory()->user->create_and_get(); + + // Enable a provider for the user. + Two_Factor_Core::enable_provider_for_user( $user->ID, 'Two_Factor_Totp' ); + + $this->assertContains( + 'Two_Factor_Totp', + Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), + 'Sample provider was enabled' + ); + + Two_Factor_Core::uninstall(); + + $this->assertNotContains( + 'Two_Factor_Totp', + Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), + 'Provider was disabled due to uninstall' + ); + } } diff --git a/two-factor.php b/two-factor.php index 274b6c84..fc34c749 100644 --- a/two-factor.php +++ b/two-factor.php @@ -50,3 +50,6 @@ $two_factor_compat = new Two_Factor_Compat(); Two_Factor_Core::add_hooks( $two_factor_compat ); + +// Delete our options and user meta during uninstall. +register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) );