diff --git a/.gitignore b/.gitignore index b90689b..7b7ed3c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,16 +20,6 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk - -### Composer ### -composer.phar -vendor/ - -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock - - ### SublimeText ### # cache files for sublime text *.tmlanguage.cache @@ -94,3 +84,11 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties +### Composer template +composer.phar +/vendor/ + +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock + diff --git a/.scrutinizer.yml b/.scrutinizer.yml index d383e8e..4ab0a05 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -3,6 +3,7 @@ filter: - 'vendor/*' - '*/vendor/*' - 'languages/*' + - 'asstes/js/jquery-qrcode.min.js' tools: external_code_coverage: false diff --git a/README.md b/README.md index b357064..ed5c317 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Google Authenticator for WordPress +Authenticator for WordPress ================================== [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/julien731/WP-Google-Authenticator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/julien731/WP-Google-Authenticator/?branch=master) @@ -7,8 +7,6 @@ If you are concerned about security, you should look into 2-factor authenticatio *Quick reminder:* 2-factor authentication adds an extra layer of security by requesting a one time password in addition to standard username / password credentials. -This plugin uses the Google Authenticator app. I bet you know Google, and you probably know they have some good products out there. Google Authenticator is one of them. - [Download the Google Authenticator app](https://support.google.com/accounts/answer/1066447?hl=en) on your phone (iPhone, Android or Blackberry). Install this plugin on your site. After activating it and generating a secret key, you will be able to add the site to your app by scanning a QR code. That's it! The QR code is generated with Google Charts API using HTTPS to avoid security issues while sending your secret for generation. @@ -29,10 +27,17 @@ What the plugin does: - Recovery code in case the user can't use the app ### Using Authy -You're using [Authy](https://www.authy.com/)? Google Authenticator for WordPress is fully compatible with Authy. You can add the 2-steps authentication and use Authy to generate the one time password. + +You're using [Authy](https://www.authy.com/)? Authenticator for WordPress is fully compatible with Authy. You can add the 2-steps authentication and use Authy to generate the one time password. ## Changelog ## +### 1.1.1 + +* Fix an issue with the settings page not showing up +* Contextual help deprecated bug +* Remove mentions of Google in the plugin name chore + ### 1.1.0 * Add support for apps passwords * Admins can now force 2FA by user role diff --git a/admin/admin.class.php b/admin/admin.class.php deleted file mode 100644 index 16537ed..0000000 --- a/admin/admin.class.php +++ /dev/null @@ -1,1305 +0,0 @@ - - * @package Google Authenticator for WordPress - * - * TODO - * - * - Revoke all users at once - * - How to block brute force? Authorized desynchronization at -1 for current IP? - */ -class WPGA_Admin { - - /** - * Instance of this class. - * - * @since 1.0.7 - * @var object - */ - protected static $instance = null; - - /** - * Plugin settings - * - * @var TAV_Settings - */ - protected $settings; - - public function __construct() { - - $this->settings = array(); - $this->apps_settings = array(); - $this->key_length = apply_filters( 'wpga_secret_key_length', 16 ); - $this->codelength = apply_filters( 'wpga_code_length', 6 ); - $this->qr_height = 300; - $this->qr_width = 300; - $this->def_attempt = 3; - $this->bkp_length = apply_filters( 'wpga_recovery_code_length', 24 ); - $this->log_max = apply_filters( 'wpga_apps_passwords_log_max', 50 ); - - if( is_admin() ) { - - add_action( 'wp_ajax_wpga_get_recovery', array( $this, 'ajax_callback' ) ); - add_action( 'wp_ajax_wpga_create_app_password', array( $this, 'create_app_password' ) ); - - if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) { - - if( isset( $_GET['action'] ) ) { - add_action( 'init', array( $this, 'EditSecret' ) ); - } - - add_action( 'init', array( $this, 'initSettings' ) ); - add_action( 'init', array( $this, 'registerSettings' ) ); - add_action( 'admin_menu', array( $this, 'add_app_password_menu' ) ); - add_action( 'admin_notices', array( $this, 'adminNotices' ) ); - add_action( 'admin_notices', array( $this, 'ForceSetSecret' ) ); - add_action( 'show_user_profile', array( $this, 'addUserProfileFields' ) ); - add_action( 'edit_user_profile', array( $this, 'UserAdminCustomProfileFields' ) ); - add_action( 'personal_options_update', array( $this, 'SaveCustomProfileFields' ) ); - add_action( 'admin_print_scripts', array( $this, 'load_admin_scripts' ) ); - - if ( isset( $_GET['page'] ) && $_GET['page'] == 'wpga_options' ) { - add_filter( 'contextual_help', array( $this, 'help' ), 10, 3 ); - } - - if ( isset( $_GET['page'] ) && ( 'wpga_options' == $_GET['page'] ) ) { - add_filter( 'admin_footer_text', array( $this, 'versionInFooter' ) ); - } - - } - } - - add_action( 'init', array( $this, 'load_plugin_textdomain' ), 9 ); - add_action( 'login_enqueue_scripts', array( $this, 'loadResources' ) ); - add_action( 'login_form', array( $this, 'customizeLoginForm' ) ); - add_action( 'wp_authenticate_user', array( $this, 'authenticateUser' ), 10, 3 ); - add_filter( 'authenticate', array( $this, 'checkAppPassword' ), 50, 3 ); - add_action( 'wpas_clean_totps', array( $this, 'clean_totps' ) ); - - } - - /** - * Return an instance of this class. - * - * @since 1.0.7 - * - * @return object A single instance of this class. - */ - public static function get_instance() { - - // If the single instance hasn't been set, set it now. - if ( null == self::$instance ) { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Instantiate the settings class - */ - public function initSettings() { - - if ( ! class_exists( 'TAV_Settings' ) ) { - return; - } - - /* Prepare arguments */ - $args = array( - 'name' => WPGA_PREFIX . '_options', - 'menu_name' => esc_html__( 'Authenticator', 'wpga' ), - 'parent' => 'options-general.php', - 'page_title' => esc_html__( 'WP Google Authenticator Settings', 'wpga' ), - 'slug' => WPGA_PREFIX . '_options', - 'page' => 'wpga-settings', - 'prefix' => WPGA_PREFIX, - 'row_name' => WPGA_PREFIX . '_options' - ); - - /* Instantiate the options class */ - $this->settings = new TAV_Settings( $args ); - - } - - /** - * Add required menu items - */ - public function add_app_password_menu() { - add_users_page( - esc_html__( 'Google Authenticator Applications Passwords', 'wpga' ), - esc_html__( 'My Apps Passwords', 'wpga' ), - 'read', - WPGA_PREFIX . '_apps_passwords', - 'wpga_apps_passwords_display' - ); - } - - /** - * Load the plugin text domain for translation. - * - * @since 1.0.3 - * @return bool Whether or not the translation was loaded - */ - public function load_plugin_textdomain() { - - $domain = WPGA_PREFIX; - $locale = apply_filters( 'plugin_locale', get_locale(), $domain ); - - return load_textdomain( $domain, WPGA_PATH . 'languages/' . $domain . '-' . $locale . '.mo' ); - - } - - - /** - * Load the scripts resources on the login page used for the tooltip - */ - public function loadResources() { - - global $pagenow; - - if ( in_array( $pagenow, array( 'wp-login.php', 'users.php' ) ) ) { - wp_enqueue_style( 'wpga-simple-hint', WPGA_URL . 'css/wpga.css', array(), null, 'all' ); - } - - } - - /** - * Load the plugin custom JS - * - * @since 1.0.4 - * @return void - */ - public function load_admin_scripts() { - - global $pagenow; - - if ( 'profile.php' === $pagenow || isset( $_GET['page'] ) && in_array( $_GET['page'], array( 'wpga_options', 'wpga_apps_passwords' ) ) ) { - wp_enqueue_script( 'wpga-custom', WPGA_URL . 'js/custom.js', array(), WPGA_VERSION, true ); - wp_enqueue_script( 'wpga-qrcode', WPGA_URL . 'vendor/jquery.qrcode.min.js', array( 'jquery' ), null, true ); - } - } - - /** - * Register plugin settings that will be displayed - * in the WP backend. - */ - public function registerSettings() { - - $this->settings->addSection( 'general', 'general' ); - $this->settings->addSection( 'security', 'security' ); - - $this->settings->addOption( 'general', array( - 'id' => 'active', - 'title' => __( 'Activate Plugin', 'wpga' ), - 'desc' => __( 'Do you wish to enable the 2-factor authentication for this site?', 'wpga' ), - 'field' => 'checkbox', - 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ) - ) - ); - - $this->settings->addOption( 'general', array( - 'id' => 'force_2fa', - 'title' => __( 'Force Use', 'wpga' ), - 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ), - 'field' => 'checkbox', - 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ) - ) - ); - - $this->settings->addOption( 'general', array( - 'id' => 'user_roles', - 'title' => __( 'Force Roles', 'wpga' ), - 'desc' => __( 'You can force users to use 2-factor authentication by role. Requires «Force Use» to be enabled. If no role is checked, 2FA will be forced for ALL roles.', 'wpga' ), - 'field' => 'user_roles', - 'opts' => $this->get_editable_roles() - ) - ); - - $this->settings->addOption( 'general', array( - 'id' => 'blog_name', - 'title' => __( 'Site Name', 'wpga' ), - 'desc' => __( 'Name under which this site will appear in the Google Authenticator app.', 'wpga' ), - 'field' => 'text' - ) - ); - - $this->settings->addOption( 'security', array( - 'id' => 'max_attempts', - 'title' => __( 'Max Attempts', 'wpga' ), - 'desc' => __( 'If you chose to force users to use 2-factor authentication, you can specify a maximum number of times a user can login WITHOUT setting up the 2-factor authentication (leave 0 for unlimited attempts).', 'wpga' ), - 'field' => 'smalltext' - ) - ); - - $this->settings->addOption( 'security', array( - 'id' => 'authorized_delay', - 'title' => __( 'Authorized Clock Desynchronization', 'wpga' ), - 'desc' => __( 'Must be in min (±). Avoid invalid one-time passwords issues. Please read the contextual help for more info.', 'wpga' ), - 'field' => 'smalltext' - ) - ); - - } - - /** - * Get roles list. - * - * @since 1.0.9 - * @return array List of editable roles - */ - public function get_editable_roles() { - global $wp_roles; - - $all_roles = $wp_roles->roles; - $editable_roles = apply_filters('editable_roles', $all_roles); - $list = array(); - - foreach ( $editable_roles as $role_id => $role ) { - $list[$role_id] = $role['name']; - } - - return $list; - } - - /** - * Register the contextual help for the plugin admin screen - */ - public function help() { - - if( ! isset( $_GET['page'] ) || $_GET['page'] != 'wpga_options' ) - return; - - $screen = get_current_screen(); - - $screen->add_help_tab( array( - 'id' => 'desynchronization', - 'title' => esc_html__( 'Desynchronization', 'wpga' ), - 'content' => wp_kses( __('

Authorized Clock Desynchronization

First of all, you have to understand how the 2-factor authentication works.

The Google Authenticator will generate a TOTP which stands for Time based One Time Pasword. This one time password, as you might now understand, is generated based on the current time.

If the server\'s (where your site is hosted) clock and the user\'s phone clock are not perfectly synchronized, the one time password generated won\'t work, as it will be generated on a time which is different from the server.

The authorized desynchronization will allow your users more time to use their one time password. By default, one code will be valid for 30 seconds. If you want to give them more time, you can specify a delay in minutes.

Of course, if you give users more time, the security will be lowered. It is advised to stick with the default 30 secs.

', 'wpga'), array( 'h2' => array(), 'p' => array(), 'strong' => array() ) ), - ) - ); - - } - - /** - * Edit secret key - * - * This function will process various actions on the user's - * secret key such as regenerate or revoke it. All actions - * are checked against a nonce before doing anything. - */ - public function EditSecret() { - - switch( $_GET['action'] ): - - case 'regenerate': - - if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'regenerate_key' ) ) { - return; - } - - $secret = $this->generateSecretKey(); - update_user_meta( get_current_user_id(), 'wpga_secret', $secret ); - wp_redirect( add_query_arg( array( 'update' => '10' ), admin_url( 'profile.php#wpga' ) ) ); - exit; - - break; - - case 'revoke': - - if ( ! isset( $_GET['user_id'] ) ) { - return; - } - - if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'revoke_key' ) ) { - return; - } - - if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) { - return; - } - - delete_user_meta( $_GET['user_id'], 'wpga_secret' ); - wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '11' ), admin_url( 'user-edit.php' ) ) ); - exit; - - break; - - case 'reset': - - if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'reset_key' ) ) { - return; - } - - if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) { - return; - } - - delete_user_meta( $_GET['user_id'], 'wpga_attempts' ); - wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '12' ), admin_url( 'user-edit.php' ) ) ); - exit; - - break; - - endswitch; - - } - - /** - * Add admin notices - */ - public function adminNotices() { - - if ( isset( $_GET['2fa_reset'] ) && 'true' == $_GET['2fa_reset'] ) { ?> - -
-

array( 'href' => array() ) ) ), '', '' ); ?>

-
- - esc_html__( 'Your secret key has been regenerated.', 'wpga' ), - '11' => sprintf( esc_html__( 'The key for user %s has been revoked.', 'wpga' ), $uid ), - '12' => sprintf( esc_html__( 'The attempts count has been reset.', 'wpga' ), $uid ), - ); - - if ( ! isset( $messages[ $_GET['update'] ] ) ) { - return; - } - - ?> -
-

-
- settings->getOption( 'active', array() ); - $force = $this->settings->getOption( 'force_2fa', array() ); - $roles = $this->settings->getOption( 'user_roles', array() ); - - $affected = ! empty( $roles ) ? $roles : $user->roles; - - if ( in_array( 'yes', $active ) && in_array( 'yes', $force ) ) { - - if ( 'all' === $this->settings->getOption( 'user_role_status', 'all' ) || array_intersect( $user->roles, $affected ) ) { - - $secret = esc_attr( get_the_author_meta( 'wpga_secret', $user->ID ) ); - $max_attempts = (int)$this->settings->getOption( 'max_attempts', $this->def_attempt ); - $attempts = (int)get_user_meta( $user->ID, 'wpga_attempts', true ); - $left = $max_attempts-$attempts; - - if ( '' == $secret ) { - - ?> -
-

- Please do it now.', 'wpga' ), array( 'a' => array( 'href' => array() ) ) ), admin_url( 'profile.php#wpga' ), $left ); ?> - 0 ) { printf( wp_kses( __( 'You only have %s login attempts left.', 'wpga' ), array( 'strong' => array() ) ), $left ); } ?> -

- - -
- ' . WPGA_AUTHOR . '.', 'gtsp' ), array( 'a' => array( 'href' => array() ), ) ), esc_url( WPGA_URI ) ); - - } - - /** - * Generate a secret key based on allowed chars - * base32 compatible. - * - * @return string Secret key - */ - public function generateSecretKey() { - - $validChars = $this->getValidChars(); - - unset( $validChars[32] ); - - $secret = ''; - - for ( $i = 0; $i < $this->key_length; $i ++ ) { - - $secret .= $validChars[ array_rand( $validChars ) ]; - - } - - return $secret; - } - - /** - * Generate a backup key - * - * In case the user loses his phone or cannot access the Google Authenticator app, - * we generate a unique backup key that the user can use to authenticate once. - * After one (only) authentication the key will be voided. - * - * @return string Backup key - * @since 1.0.4 - */ - public function generate_backup_key() { - - $length = $this->bkp_length; - $max = ceil( $length / 40 ); - $random = ''; - - for ( $i = 0; $i < $max; $i ++ ) { - $random .= sha1( microtime( true ) . mt_rand( 10000, 90000 ) ); - } - - return substr( $random, 0, $length ); - } - - /** - * List the base32 valid chars that can be used for - * secret key generation. - * - * @return array Valid chars - */ - private function getValidChars() { - - return array( - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 - 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 - '=' // padding char - ); - } - - /** - * Decode base32 string - * - * @param string $string String to decode - * - * @return string Decoded string - */ - function base32_decode( $string ) { - - $lut = array("A" => 0, "B" => 1, - "C" => 2, "D" => 3, - "E" => 4, "F" => 5, - "G" => 6, "H" => 7, - "I" => 8, "J" => 9, - "K" => 10, "L" => 11, - "M" => 12, "N" => 13, - "O" => 14, "P" => 15, - "Q" => 16, "R" => 17, - "S" => 18, "T" => 19, - "U" => 20, "V" => 21, - "W" => 22, "X" => 23, - "Y" => 24, "Z" => 25, - "2" => 26, "3" => 27, - "4" => 28, "5" => 29, - "6" => 30, "7" => 31 - ); - - $string = strtoupper($string); - $l = strlen($string); - $n = 0; - $j = 0; - $binary = ""; - - for ( $i = 0; $i < $l; $i ++ ) { - - $n = $n << 5; - $n = $n + $lut[ $string[ $i ] ]; - $j = $j + 5; - - if ( $j >= 8 ) { - $j = $j - 8; - $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j ); - } - } - - return $binary; - } - - /** - * Calculate the code, with given secret and point in time - * - * @param string $secret - * @param integer $timeSlice - * - * @return string Generated code - */ - public function getCode( $secret, $timeSlice = null ) { - - if( $timeSlice === null ) { - - $timeSlice = floor(time() / 30); - - } - - $secretkey = $this->base32_decode( $secret ); - - // Pack time into binary string - $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); - - // Hash it with users secret key - $hm = hash_hmac('SHA1', $time, $secretkey, true); - - // Use last nipple of result as index/offset - $offset = ord(substr($hm, -1)) & 0x0F; - - // grab 4 bytes of the result - $hashpart = substr($hm, $offset, 4); - - // Unpak binary value - $value = unpack('N', $hashpart); - $value = $value[1]; - - // Only 32 bits - $value = $value & 0x7FFFFFFF; - - $modulo = pow(10, $this->codelength); - - return str_pad($value % $modulo, $this->codelength, '0', STR_PAD_LEFT); - } - - /** - * Check if the code is correct. This will accept codes starting from $drift*30sec ago to $drift*30sec from now - * - * @package PHPGangsta_GoogleAuthenticator - * @author Michael Kliewe - * - * @param string $secret - * @param string $code - * - * @return bool - */ - public function checkTOTP( $secret, $code ) { - - $options = get_option( 'wpga_options' ); - $drift = isset( $options['authorized_delay'] ) ? (int)$options['authorized_delay']*2 : 1; - $currentTimeSlice = floor( time() / 30 ); - - for( $i = -$drift; $i <= $drift; $i++ ) { - - $calculatedCode = $this->getCode( $secret, $currentTimeSlice + $i ); - - if( $calculatedCode == $code ) { - - return true; - - } - } - - return false; - } - - /** - * Check if 2FA is enabled. - * - * Verifies if the user trying to log in has 2FA enabled. - * If not, we check if the site admin did force 2FA and if this - * user is affected. - * - * @param object $user The current user object - * @return boolean True if 2FA is enabled for this user, false otherwise - */ - public function is_2fa_enabled( $user ) { - - if ( is_wp_error( $user ) ) { - return false; - } - - /* First of all we check if 2FA is enabled for this user. */ - if ( 'yes' === get_user_meta( $user->ID, 'wpga_active', true ) ) { - return true; - } - - $options = get_option( 'wpga_options', array() ); - - /* Check if 2FA is forced by the admin */ - if ( !isset( $options['force_2fa'] ) || !in_array( 'yes', (array)$options['force_2fa'] ) ) { - return false; - } - - if ( 'all' === $options['user_role_status'] ) { - return true; - } - - /* If the forced roles list is empty, we consider it active for all users. Hence, we add the current user role in the list. */ - if ( !isset( $options['user_roles'] ) || empty( $options['user_roles'] ) ) { - $options['user_roles'] = $user->roles; - } - - /* Check if 2FA is forced for the role this user has */ - if ( ! array_intersect( $user->roles, $options['user_roles'] ) ) { - return false; - } - - return true; - - } - - /** - * Add TOTP check to WordPress authentication process - * - * @param object $user - * - * @return object User object on success or WP_Error on failure - */ - public function authenticateUser( $user ) { - - $options = get_option( 'wpga_options', array() ); - - if ( ! isset( $options['active'] ) || ! in_array( 'yes', $options['active'] ) ) { - return $user; - } - - if ( ! is_wp_error( $user ) ) { - - $secret = get_user_meta( $user->ID, 'wpga_secret', true ); - $totp = isset( $_POST['totp'] ) ? sanitize_key( $_POST['totp'] ) : null; - - /* TOTP is forced for all users */ - if ( $this->is_2fa_enabled( $user ) ) { - - /* Let's make sure the user has generated a secret */ - if( '' != $secret ) { - - if ( is_null( $totp ) ) { - return new WP_Error( 'no_totp', esc_html__( 'An error is preventing the 2-factor authentication from authenticating your session.', 'wpga' ) ); - } - - if ( empty( $totp ) ) { - return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) ); - } - - if( $this->checkTOTP( $secret, $totp ) ) { - - $used = get_option( 'wpga_used_totp', array() ); - - if( is_array( $used ) && !in_array( md5( $totp ), $used ) ) { - - array_push( $used, md5( $totp ) ); - - update_option( 'wpga_used_totp', $used ); - - return $user; - - } else { - return new WP_Error( 'expired_totp', esc_html__( 'The one time password you used has already been revoked.', 'wpga' ) ); - } - - } - - /** - * Check if the user is sending a recovery key. - * - * If the recovery key is valid, we deactivate - * 2FA for this user so that he can log-in - * without using the app. - * - * @since 1.0.4 - */ - elseif( $this->check_recovery_key( $user, $totp ) ) { - - /* Clean the 2FA data */ - delete_user_meta( $user->ID, 'wpga_active' ); - delete_user_meta( $user->ID, 'wpga_attempts' ); - delete_user_meta( $user->ID, 'wpga_secret' ); - delete_user_meta( $user->ID, 'wpga_backup_key' ); - delete_user_meta( $user->ID, 'wpga_backup_key_time' ); - - /* Add URL var to the login redirect */ - add_filter( 'login_redirect', array( $this, 'login_redirect_notify' ) ); - - return $user; - - } else { - return new WP_Error( 'totp_invalid', esc_html__( 'The Google Authenticator one time password is incorrect or expired. Please try with a newly generated password.', 'wpga' ) ); - } - - } else { - - $options = get_option( 'wpga_options', array() ); - $attempts = (int)get_user_meta( $user->ID, 'wpga_attempts', true ); - $max_attempts = ( isset( $options['max_attempts'] ) && '' != $options['max_attempts'] ) ? $options['max_attempts'] : $this->def_attempt; - - /* If the admin set the max attempts to unlimited we give us with security :( */ - if ( "0" === $max_attempts ) { - return $user; - } - - if( $attempts < $max_attempts ) { - update_user_meta( $user->ID, 'wpga_attempts', $attempts+1, $attempts ); - return $user; - } else { - return new WP_Error( '2fa_max_attempts', esc_html__( 'You have reached the maximum number of logins WITHOUT using 2-factor authentication. Please contact the admin to reset your account.', 'wpga' ) ); - } - - } - - } - - /* No TOTP check? Just return the user for standard authentification */ - else { - - return $user; - - } - - } else { - return $user; - } - - } - - /** - * Check for app password. - * - * If the user has created one or more apps passwords, - * we check if the given password is a registered one. - * - * @since 1.1.0 - * - * @param WP_User $user The user object - * @param string $username The username to authenticate - * @param string $password The user password - * - * @return WP_User A user object - */ - public function checkAppPassword( $user, $username, $password ) { - - if ( ! is_wp_error( $user ) ) { - return $user; - } - - $user_data = get_user_by( 'login', $username ); - - if ( ! is_object( $user_data ) ) { - return false; - } - - if ( $this->has_app_passwords( $user_data->ID ) ) { - - $passwords = wpga_get_app_passwords( $user_data->ID ); - $hash = md5( $password ); - $key = wpga_make_unique_key( $hash ); - - if ( array_key_exists( $key, $passwords ) ) { - - /* App password is correct. */ - if ( wp_check_password( trim( $password ), $passwords[ $key ]['hash'] ) ) { - - $new = wpga_get_app_passwords_log( $user_data->ID ); - $count = count( $new ); - $last = null; - - /* Delete the oldest entry if the limit is reached */ - if ( $count === $this->log_max ) { - foreach ( $new as $date => $data ) { - $last = $date; - } - unset( $new[ $last ] ); - } - - $time = strtotime( 'now' ); - $entry = array( - 'key' => $key, - 'last_used' => $time, - 'ip' => $_SERVER['REMOTE_ADDR'], - 'user_agent' => $_SERVER['HTTP_USER_AGENT'], - 'method' => '', - ); - - /* Update the password use count */ - $passwords[$key]['count'] = intval( $passwords[$key]['count'] ) + 1; - update_user_meta( $user_data->ID, 'wpga_apps_passwords', $passwords ); - - /* Save the log entry */ - $new[$time] = $entry; - update_user_meta( $user_data->ID, 'wpga_apps_passwords_log', $new ); - - return new WP_User( $user_data->ID ); - - } else { - return new WP_Error( 'wrong_app_password', esc_html__( 'The application password you provided is invalid.', 'wpga' ) ); - } - } else { - return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) ); - } - } else { - return $user; - } - } - - /** - * Check if the current user has app passwords. - * - * @since 1.1.0 - * - * @param int $user_id A user ID - * - * @return boolean True if has app passwords - */ - public function has_app_passwords( $user_id ) { - $passwords = wpga_get_app_passwords( $user_id ); - if ( empty( $passwords ) ) { - return false; - } else { - return true; - } - } - - /** - * Add a URL var to login redirect page - * - * @return string Redirect URL - * @since 1.0.4 - */ - public function login_redirect_notify() { - return add_query_arg( array( '2fa_reset' => 'true' ), admin_url() ); - } - - /** - * Check validity of a recovery key - * - * @param object $user User object - * @param string $key Recovery key to check - * - * @return boolean Whether or not the key is valid - * @since 1.0.4 - */ - public function check_recovery_key( $user, $key ) { - - $recovery = get_user_meta( $user->ID, 'wpga_backup_key', true ); - - if ( sanitize_key( $key ) == $recovery ) { - return true; - } else { - return false; - } - - } - - /** - * Get QR Code text - * - * Do API calls to get the data for the QR code. rawurlencode() blog_name and account - * - * @return (string) QR Code URL - */ - public function getQRCodeInfo() { - - $blogname = rawurlencode( $this->settings->getOption( 'blog_name' ) ); - $secret = esc_attr( get_the_author_meta( 'wpga_secret', get_current_user_id() ) ); - $account = get_the_author_meta( 'user_login', get_current_user_id() ); - $label = $blogname . ':' . rawurlencode( $account ); - - return 'otpauth://totp/' . $label . '?secret=' . $secret . '&issuer=' . $blogname; - - } - - /** - * Add profile custom fields - * - * @param WP_User $user The WP_User object of the user being edited. - */ - public function addUserProfileFields( $user ) { - - add_thickbox(); - $force = $this->settings->getOption( 'force_2fa' ); - $qr = true; - $width = $this->qr_width+10; - $height = $this->qr_height+10; - $secret = get_the_author_meta( 'wpga_secret', $user->ID ); - $args = array( 'action' => 'regenerate' ); - $backup = get_user_meta( $user->ID, 'wpga_backup_key', true ); - if ( isset( $_GET['user_id'] ) ) { $args['user_id'] = $_GET['user_id']; } - $regenerate = wp_nonce_url( add_query_arg( $args, admin_url( 'profile.php' ) ), 'regenerate_key' ); - - if ( '' == $secret ) { - $secret = $this->generateSecretKey(); - $qr = false; - } - ?> - -

- - - - ID ) ); - - if ( 'yes' == $active ) { - $checked = 'checked="checked"'; - } else { - $checked = ''; - } ?> - - - - - - - - - - - - - - ID, 'wpga_backup_key_time', true ); - $limit = $time + 300; // Recovery key generation time + 5 mins - ?> - - - - - - -
- />
-

-
- - - -

- - - - - -

- - -
- - - -

- - - - - - - - - -
- data->user_pass, $user->ID ) ) { - - $recovery = get_user_meta( $user_id, 'wpga_backup_key', true ); - - if ( '' != $recovery ) { - echo "
" . esc_html( $recovery ) . "

" . esc_html_e( 'Write this down and keep it safe', 'wpga' ) . "

"; - } else { - esc_html_e( 'No recovery code set yet.', 'wpga' ); - } - - } else { - ?>ID, 'wpga_apps_passwords', true ) ) ? $p : array(); - $pwd = $this->generate_backup_key(); - $hash = md5( esc_attr( $pwd ) ); - $key = wpga_make_unique_key( $hash ); - $return = json_encode( array( 'desc' => sanitize_text_field( $_POST['description'] ), 'pwd' => esc_attr( $pwd ) ) ); - $new[$key] = array( 'description' => sanitize_text_field( $_POST['description'] ), 'hash' => $hash, 'count' => 0 ); - - update_user_meta( $current_user->ID, 'wpga_apps_passwords', $new, $passwords ); - - echo esc_attr( urlencode( $return ) ); - die(); - } - - /** - * Add admin control fields in user profile - */ - public function UserAdminCustomProfileFields() { - - if( !current_user_can( 'edit_users' ) || !isset( $_GET['user_id'] ) ) - return; - - $options = get_option( 'wpga_options', array() ); - $secret = esc_attr( get_the_author_meta( 'wpga_secret', $_GET['user_id'] ) ); - $args = array( 'action' => 'revoke', 'user_id' => $_GET['user_id'] ); - $rst_arg = array( 'action' => 'reset', 'user_id' => $_GET['user_id'] ); - $revoke = wp_nonce_url( add_query_arg( $args, admin_url( 'user-edit.php' ) ), 'revoke_key' ); - $rst = wp_nonce_url( add_query_arg( $rst_arg, admin_url( 'user-edit.php' ) ), 'reset_key' ); - $attempts = (int)get_user_meta( $_GET['user_id'], 'wpga_attempts', true ); - $max_attempts = ( isset( $options['max_attempts'] ) && '' != $options['max_attempts'] ) ? (int)$options['max_attempts'] : $this->def_attempt; - ?> -

- - - - - - - - - - - - -
- -

- -

- -
- - - $max_attempts ) { echo '' . esc_html_e( '(This user is locked out)', 'wpga' ) . ''; } ?> -

-
- generate_backup_key(); - - /* Save the backup key */ - update_user_meta( $user_id, 'wpga_backup_key', sanitize_key( $key ) ); - - /** - * Set a session var to allow user seeing the backup key - * without having to enter his password. This will only happen once - */ - update_user_meta( $user_id, 'wpga_backup_key_time', time() ); - - } - } - - /** - * Add verification code field to login form. - */ - public function customizeLoginForm() { - - $options = get_option( 'wpga_options', array() ); - - if( !isset( $options['active'] ) || !in_array( 'yes', $options['active'] ) ) - return; - - ?> -

- -

- 'wpga_apps_passwords' ), admin_url( 'users.php') ) ); - exit; - - } - -} diff --git a/admin/functions-apps-passwords.php b/admin/functions-apps-passwords.php deleted file mode 100644 index 2625d4c..0000000 --- a/admin/functions-apps-passwords.php +++ /dev/null @@ -1,106 +0,0 @@ - $entry ) { - if ( $key === $entry['key'] ) { - array_push( $last, $entry ); - } - } - - if ( empty( $last ) ) { - return false; - } - - $count = count( $last ) - 1; - - return $last[$count]; - -} - -function wpga_get_app_passwords( $user_id = null ) { - - if ( is_null( $user_id ) ) { - global $current_user; - $user_id = $current_user->ID; - } - - $passwords = is_array( $p = get_user_meta( $user_id, 'wpga_apps_passwords', true ) ) ? $p : array(); - return $passwords; -} - -function wpga_get_app_passwords_log( $user_id = null ) { - - if ( is_null( $user_id ) ) { - global $current_user; - $user_id = $current_user->ID; - } - - $log = is_array( $p = get_user_meta( $user_id, 'wpga_apps_passwords_log', true ) ) ? $p : array(); - krsort( $log ); - - return $log; - -} - -function wpga_delete_app_password( $key ) { - - global $current_user; - - $passwords = $new = wpga_get_app_passwords(); - - if ( array_key_exists( $key, $passwords ) ) { - unset( $new[$key] ); - update_user_meta( $current_user->ID, 'wpga_apps_passwords', $new, $passwords ); - } - -} - -function wpga_reset_app_passwords() { - global $current_user; - delete_user_meta( $current_user->ID, 'wpga_apps_passwords' ); -} - -function wpga_clear_log() { - global $current_user; - delete_user_meta( $current_user->ID, 'wpga_apps_passwords_log' ); -} \ No newline at end of file diff --git a/admin/settings.class.php b/admin/settings.class.php deleted file mode 100644 index 1efb776..0000000 --- a/admin/settings.class.php +++ /dev/null @@ -1,349 +0,0 @@ -settings = array(); - $this->page = $init['page']; - $this->group = $init['prefix']; - $this->option = isset( $init['name'] ) ? $init['name'] : 'tav_options'; - $this->menu_name = isset( $init['menu_name'] ) ? $init['menu_name'] : 'Settings'; - $this->slug = isset( $init['slug'] ) ? $init['slug'] : 'tav-settings'; - $this->parent = isset( $init['parent'] ) ? $init['parent'] : false; - $this->page_title = isset( $init['page_title'] ) ? $init['page_title'] : 'Settings'; - $this->icon = isset( $init['icon'] ) ? $init['icon'] : 'icon-options-general'; - $this->capability = isset( $init['capability'] ) ? $init['capability'] : 'administrator'; - $this->callback = isset( $init['callback'] ) ? $init['callback'] : 'settingsPage'; - - add_action( 'admin_menu', array( $this, 'addMenuItems' ) ); - add_action( 'admin_init', array( $this, 'registerSettings' ), 10 ); - add_action( 'admin_init', array( $this, 'parseOptions' ), 11 ); - - } - - /** - * Get the settings - * - * @return (array) defined settings - */ - public function getSettings() { - return $this->settings; - } - - public function getOption( $opt = false, $default = false ) { - - if( !$opt ) - return $default; - - /* Get the serialized values */ - $options = get_option( $this->option, $default ); - - if( $options && is_array( $options ) && !empty( $options ) ) { - - if( isset( $options[$opt] ) ) { - return $options[$opt]; - } else { - return $default; - } - - } else { - return $default; - } - - } - - /** - * Add required menu items - */ - public function addMenuItems() { - - $callback = method_exists( $this, $this->callback ) ? array( $this, $this->callback ) : array( $this, 'settingsPage' ); - - if( $this->parent ) { - add_submenu_page( $this->parent, $this->page_title, $this->menu_name, $this->capability, $this->slug, $callback ); - } else { - add_menu_page( $this->page_title, $this->menu_name, $this->capability, $this->slug, $callback ); - } - - } - - /** - * Register the global settings as we use - * only one row in the database for each options - * set (framework & theme). - */ - public function registerSettings() { - - register_setting( $this->page, $this->option ); - - } - - /** - * Add a new tabbed section - * in the option pannel. - */ - public function addSection( $id = false, $group = false, $title = false ) { - - if( !$id ) - return false; - - if( isset( $this->settings[$id] ) ) - return false; - - /* Declare the new section and its options */ - $section = array( - 'id' => $id, - 'options' => array() - ); - - /* Add the title if declared */ - if( $title ) { - $section['title'] = $title; - } else { - $section['title'] = ucwords( $id ); - } - - if( $group ) - $section['group'] = $group; - - /* Add the new section */ - $this->settings[$id] = $section; - - } - - public function addOption( $section_id = false, $args = array() ) { - - /* We check if this option can be assigned to a metabox */ - if( !$section_id || !isset( $this->settings[$section_id] ) || empty( $args ) ) - return false; - - /* Check if the required information is set */ - if( !isset( $args['id'] ) || !isset( $args['title'] ) || !isset( $args['field'] ) ) - return false; - - $option = array( - 'id' => $args['id'], - 'title' => $args['title'], - 'field' => $args['field'] - ); - - isset( $args['desc'] ) ? $option['desc'] = $args['desc'] : false; // Optional field description - isset( $args['opts'] ) ? $option['opts'] = $args['opts'] : false; // Available options for radio / checkbox / select - isset( $args['validate'] ) ? $option['validate'] = $args['validate'] : false; // Adds HTML5 validation pattern - isset( $args['limit'] ) ? $option['limit'] = $args['limit'] : false; // Limits field content lenght - - /* Add the new option */ - $this->settings[$section_id]['options'][] = $option; - - } - - public function parseOptions() { - - /* Iterate through the options */ - foreach( $this->settings as $section ) { - - /** - * Register a new section - */ - - /* Define section ID */ - $section_id = $this->page; - - /* Define title */ - isset( $section['title'] ) ? $title = $section['title'] : $title = ''; - - /* Add the section */ - add_settings_section( 'tav_' . $section['id'], $title, false, $this->page ); - - /* Assign options to current section */ - foreach( $section['options'] as $key => $option ) { - - /* Check if we have a callback */ - if( !isset( $option['field'] ) ) - continue; - - /** - * Register the section's fields - */ - - /* Checking for dependencies */ - if( isset( $option['dependency'] ) ) - $dep = tav_get_theme_options( $option['dependency'][0] ); - - /* Displaying option */ - if( !isset( $option['dependency'] ) || isset( $option['dependency'] ) && $dep == $option['dependency'][1] ) { - - /* Preparing data for later validation */ - if( isset( $option['validate'] ) && '' != $option['validate'] ) { - $this->to_validate[$option['id']] = $option['validate']; - } - - /** - * We prepare the arguments - */ - $args = array(); - $args['id'] = $option['id']; // Field ID - $args['title'] = $option['title']; // Field title - $args['group'] = $this->group . '_options'; // Option group (used to identify how to retrieve the options) - $args['type'] = $option['field']; - if( isset( $option['desc'] ) ) $args['desc'] = $option['desc']; // Field description - if( isset( $option['opts'] ) ) $args['opts'] = $option['opts']; // Options available (select, checkboxes, radio) - if( isset( $option['validate'] ) ) $args['validate'] = $option['validate']; // Validation type (for HTML5 patterns) - if( isset( $option['std'] ) ) $args['std'] = $option['std']; // Default option - if( isset( $option['level'] ) ) $args['level'] = $option['level']; // Level required to edit option - - /* We finally add the field */ - add_settings_field( $option['id'], $option['title'], array( $this, 'filedsCallbacks' ), $this->page, TAV_SHORTNAME . '_' . $section['id'], $args ); - } - } - } - } - - /** - * Display the settings page - */ - public function settingsPage() { - - ?> -
-
-

- -
- page ); - do_settings_sections( $this->page ); - ?> -

- -

- -
-
- getOption( $field['id'], '' ); - - switch ( $field['type'] ): - - /** - * Markup for regular text fields - */ - case 'text': ?> - - -

array() ) ); ?>

- - -

array() ) ); ?>

$title ) : - - $checked = ( is_array( $value ) && in_array( $val, $value ) ) ? 'checked="checked"' : ''; - $id = $field['id'] . '_' . $val; ?> - - - -

getOption( 'user_role_status', 'all' ); - $checked_all = ( 'all' === $status ) ? 'checked="checked"' : ''; - $checked_custom = ( 'custom' === $status ) ? 'checked="checked"' : ''; - ?> - -
- -
- -
- -
- - -
- -
- - $title ): - - $checked = ( is_array( $value ) && in_array( $val, $value ) ) ? 'checked="checked"' : ''; - $id = $field['id'] . '_' . $val; ?> - -
- - - -
- -

- -
- - - - roles; - $editable_roles = apply_filters('editable_roles', $all_roles); - - return $editable_roles; - } - -} \ No newline at end of file diff --git a/css/simple-hint.min.css b/assets/css/simple-hint.min.css similarity index 100% rename from css/simple-hint.min.css rename to assets/css/simple-hint.min.css diff --git a/assets/css/wpga.css b/assets/css/wpga.css new file mode 100644 index 0000000..cb7d036 --- /dev/null +++ b/assets/css/wpga.css @@ -0,0 +1,54 @@ +@import 'simple-hint.min.css'; + +/* Required styles for the tooltip */ +.login form { + overflow: visible; +} + +.wpga-label { + position: relative; +} + +/* Style the tooltip */ +.wpga-label span:after { + line-height: 1.6; + font-size: 14px; + padding: 10px 15px; +} + +/* Reset the link style */ +.wpga-hint { + text-decoration: none; +} + +.wpga-hint:focus { + box-shadow: none; +} + +/* Hide the tooltip on mobiles */ +@media only screen and (max-width: 768px) { + .wpga-label span { + display: none; + } +} + +/* Tags */ +.wpga-tag { + display: inline-block; + margin: 0 .3em .3em 0; + max-width: 80px; + height: 20px; + line-height: 20px; + padding: 0 1em; + background-color: #2e8ece; + border: 1px solid #aaa; + border-radius: 3px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + color: #2c3e50; + font-size: 13px; + text-decoration: none; + -webkit-transition: .2s; + transition: .2s; +} \ No newline at end of file diff --git a/js/custom.js b/assets/js/custom.js similarity index 100% rename from js/custom.js rename to assets/js/custom.js diff --git a/assets/js/jquery-qrcode.min.js b/assets/js/jquery-qrcode.min.js new file mode 100644 index 0000000..f718c22 --- /dev/null +++ b/assets/js/jquery-qrcode.min.js @@ -0,0 +1,2 @@ +/*! jquery-qrcode v0.14.0 - https://larsjung.de/jquery-qrcode/ */ +!function(r){"use strict";function t(t,e,n,o){function a(r,t){return r-=o,t-=o,0>r||r>=c||0>t||t>=c?!1:f.isDark(r,t)}function i(r,t,e,n){var o=u.isDark,a=1/l;u.isDark=function(i,u){var f=u*a,c=i*a,l=f+a,g=c+a;return o(i,u)&&(r>l||f>e||t>g||c>n)}}var u={},f=r(n,e);f.addData(t),f.make(),o=o||0;var c=f.getModuleCount(),l=f.getModuleCount()+2*o;return u.text=t,u.level=e,u.version=n,u.moduleCount=l,u.isDark=a,u.addBlank=i,u}function e(r,e,n,o,a){n=Math.max(1,n||1),o=Math.min(40,o||40);for(var i=n;o>=i;i+=1)try{return t(r,e,i,a)}catch(u){}}function n(r,t,e){var n=e.size,o="bold "+e.mSize*n+"px "+e.fontname,a=w("")[0].getContext("2d");a.font=o;var i=a.measureText(e.label).width,u=e.mSize,f=i/n,c=(1-f)*e.mPosX,l=(1-u)*e.mPosY,g=c+f,s=l+u,v=.01;1===e.mode?r.addBlank(0,l-v,n,s+v):r.addBlank(c-v,l-v,g+v,s+v),t.fillStyle=e.fontcolor,t.font=o,t.fillText(e.label,c*n,l*n+.75*e.mSize*n)}function o(r,t,e){var n=e.size,o=e.image.naturalWidth||1,a=e.image.naturalHeight||1,i=e.mSize,u=i*o/a,f=(1-u)*e.mPosX,c=(1-i)*e.mPosY,l=f+u,g=c+i,s=.01;3===e.mode?r.addBlank(0,c-s,n,g+s):r.addBlank(f-s,c-s,l+s,g+s),t.drawImage(e.image,f*n,c*n,u*n,i*n)}function a(r,t,e){w(e.background).is("img")?t.drawImage(e.background,0,0,e.size,e.size):e.background&&(t.fillStyle=e.background,t.fillRect(e.left,e.top,e.size,e.size));var a=e.mode;1===a||2===a?n(r,t,e):(3===a||4===a)&&o(r,t,e)}function i(r,t,e,n,o,a,i,u){r.isDark(i,u)&&t.rect(n,o,a,a)}function u(r,t,e,n,o,a,i,u,f,c){i?r.moveTo(t+a,e):r.moveTo(t,e),u?(r.lineTo(n-a,e),r.arcTo(n,e,n,o,a)):r.lineTo(n,e),f?(r.lineTo(n,o-a),r.arcTo(n,o,t,o,a)):r.lineTo(n,o),c?(r.lineTo(t+a,o),r.arcTo(t,o,t,e,a)):r.lineTo(t,o),i?(r.lineTo(t,e+a),r.arcTo(t,e,n,e,a)):r.lineTo(t,e)}function f(r,t,e,n,o,a,i,u,f,c){i&&(r.moveTo(t+a,e),r.lineTo(t,e),r.lineTo(t,e+a),r.arcTo(t,e,t+a,e,a)),u&&(r.moveTo(n-a,e),r.lineTo(n,e),r.lineTo(n,e+a),r.arcTo(n,e,n-a,e,a)),f&&(r.moveTo(n-a,o),r.lineTo(n,o),r.lineTo(n,o-a),r.arcTo(n,o,n-a,o,a)),c&&(r.moveTo(t+a,o),r.lineTo(t,o),r.lineTo(t,o-a),r.arcTo(t,o,t+a,o,a))}function c(r,t,e,n,o,a,i,c){var l=r.isDark,g=n+a,s=o+a,v=e.radius*a,h=i-1,d=i+1,w=c-1,m=c+1,y=l(i,c),T=l(h,w),p=l(h,c),B=l(h,m),A=l(i,m),E=l(d,m),k=l(d,c),M=l(d,w),C=l(i,w);y?u(t,n,o,g,s,v,!p&&!C,!p&&!A,!k&&!A,!k&&!C):f(t,n,o,g,s,v,p&&C&&T,p&&A&&B,k&&A&&E,k&&C&&M)}function l(r,t,e){var n,o,a=r.moduleCount,u=e.size/a,f=i;for(e.radius>0&&e.radius<=.5&&(f=c),t.beginPath(),n=0;a>n;n+=1)for(o=0;a>o;o+=1){var l=e.left+o*u,g=e.top+n*u,s=u;f(r,t,e,l,g,s,n,o)}if(w(e.fill).is("img")){t.strokeStyle="rgba(0,0,0,0.5)",t.lineWidth=2,t.stroke();var v=t.globalCompositeOperation;t.globalCompositeOperation="destination-out",t.fill(),t.globalCompositeOperation=v,t.clip(),t.drawImage(e.fill,0,0,e.size,e.size),t.restore()}else t.fillStyle=e.fill,t.fill()}function g(r,t){var n=e(t.text,t.ecLevel,t.minVersion,t.maxVersion,t.quiet);if(!n)return null;var o=w(r).data("qrcode",n),i=o[0].getContext("2d");return a(n,i,t),l(n,i,t),o}function s(r){var t=w("").attr("width",r.size).attr("height",r.size);return g(t,r)}function v(r){return w("").attr("src",s(r)[0].toDataURL("image/png"))}function h(r){var t=e(r.text,r.ecLevel,r.minVersion,r.maxVersion,r.quiet);if(!t)return null;var n,o,a=r.size,i=r.background,u=Math.floor,f=t.moduleCount,c=u(a/f),l=u(.5*(a-c*f)),g={position:"relative",left:0,top:0,padding:0,margin:0,width:a,height:a},s={position:"absolute",padding:0,margin:0,width:c,height:c,"background-color":r.fill},v=w("
").data("qrcode",t).css(g);for(i&&v.css("background-color",i),n=0;f>n;n+=1)for(o=0;f>o;o+=1)t.isDark(n,o)&&w("
").css(s).css({left:l+o*c,top:l+n*c}).appendTo(v);return v}function d(r){return m&&"canvas"===r.render?s(r):m&&"image"===r.render?v(r):h(r)}var w=window.jQuery,m=function(){var r=document.createElement("canvas");return!(!r.getContext||!r.getContext("2d"))}(),y={render:"canvas",minVersion:1,maxVersion:40,ecLevel:"L",left:0,top:0,size:200,fill:"#000",background:null,text:"no text",radius:0,quiet:0,mode:0,mSize:.1,mPosX:.5,mPosY:.5,label:"no label",fontname:"sans",fontcolor:"#000",image:null};w.fn.qrcode=function(r){var t=w.extend({},y,r);return this.each(function(r,e){"canvas"===e.nodeName.toLowerCase()?g(e,t):w(e).append(d(t))})}}(function(){var r=function(){function r(t,e){if("undefined"==typeof t.length)throw new Error(t.length+"/"+e);var n=function(){for(var r=0;re;e+=1){t[e]=new Array(r);for(var n=0;r>n;n+=1)t[e][n]=null}return t}(v),T(0,0),T(v-7,0),T(0,v-7),A(),B(),k(r,t),l>=7&&E(r),null==d&&(d=D(l,g,w)),M(d,t)},T=function(r,t){for(var e=-1;7>=e;e+=1)if(!(-1>=r+e||r+e>=v))for(var n=-1;7>=n;n+=1)-1>=t+n||t+n>=v||(e>=0&&6>=e&&(0==n||6==n)||n>=0&&6>=n&&(0==e||6==e)||e>=2&&4>=e&&n>=2&&4>=n?s[r+e][t+n]=!0:s[r+e][t+n]=!1)},p=function(){for(var r=0,t=0,e=0;8>e;e+=1){y(!0,e);var n=a.getLostPoint(m);(0==e||r>n)&&(r=n,t=e)}return t},B=function(){for(var r=8;v-8>r;r+=1)null==s[r][6]&&(s[r][6]=r%2==0);for(var t=8;v-8>t;t+=1)null==s[6][t]&&(s[6][t]=t%2==0)},A=function(){for(var r=a.getPatternPosition(l),t=0;t=i;i+=1)for(var u=-2;2>=u;u+=1)-2==i||2==i||-2==u||2==u||0==i&&0==u?s[n+i][o+u]=!0:s[n+i][o+u]=!1}},E=function(r){for(var t=a.getBCHTypeNumber(l),e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[Math.floor(e/3)][e%3+v-8-3]=n}for(var e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[e%3+v-8-3][Math.floor(e/3)]=n}},k=function(r,t){for(var e=g<<3|t,n=a.getBCHTypeInfo(e),o=0;15>o;o+=1){var i=!r&&1==(n>>o&1);6>o?s[o][8]=i:8>o?s[o+1][8]=i:s[v-15+o][8]=i}for(var o=0;15>o;o+=1){var i=!r&&1==(n>>o&1);8>o?s[8][v-o-1]=i:9>o?s[8][15-o-1+1]=i:s[8][15-o-1]=i}s[v-8][8]=!r},M=function(r,t){for(var e=-1,n=v-1,o=7,i=0,u=a.getMaskFunction(t),f=v-1;f>0;f-=2)for(6==f&&(f-=1);;){for(var c=0;2>c;c+=1)if(null==s[n][f-c]){var l=!1;i>>o&1));var g=u(n,f-c);g&&(l=!l),s[n][f-c]=l,o-=1,-1==o&&(i+=1,o=7)}if(n+=e,0>n||n>=v){n-=e,e=-e;break}}},C=function(t,e){for(var n=0,o=0,i=0,u=new Array(e.length),f=new Array(e.length),c=0;c=0?d.getAt(w):0}}for(var m=0,s=0;ss;s+=1)for(var c=0;cs;s+=1)for(var c=0;c8*s)throw new Error("code length overflow. ("+c.getLengthInBits()+">"+8*s+")");for(c.getLengthInBits()+4<=8*s&&c.put(0,4);c.getLengthInBits()%8!=0;)c.putBit(!1);for(;;){if(c.getLengthInBits()>=8*s)break;if(c.put(o,8),c.getLengthInBits()>=8*s)break;c.put(i,8)}return C(c,n)};return m.addData=function(r){var t=c(r);w.push(t),d=null},m.isDark=function(r,t){if(0>r||r>=v||0>t||t>=v)throw new Error(r+","+t);return s[r][t]},m.getModuleCount=function(){return v},m.make=function(){y(!1,p())},m.createTableTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e="";e+='";for(var o=0;o';e+=""}return e+="",e+="
"},m.createImgTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e=m.getModuleCount()*r+2*t,n=t,o=e-t;return h(e,e,function(t,e){if(t>=n&&o>t&&e>=n&&o>e){var a=Math.floor((t-n)/r),i=Math.floor((e-n)/r);return m.isDark(i,a)?0:1}return 1})},m};t.stringToBytes=function(r){for(var t=new Array,e=0;ea)t.push(a);else{var i=e[r.charAt(o)];"number"==typeof i?(255&i)==i?t.push(i):(t.push(i>>>8),t.push(255&i)):t.push(n)}}return t}};var e={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},n={L:1,M:0,Q:3,H:2},o={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},a=function(){var t=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],n=1335,a=7973,u=21522,f={},c=function(r){for(var t=0;0!=r;)t+=1,r>>>=1;return t};return f.getBCHTypeInfo=function(r){for(var t=r<<10;c(t)-c(n)>=0;)t^=n<=0;)t^=a<n;n+=1)e=e.multiply(r([1,i.gexp(n)],0));return e},f.getLengthInBits=function(r,t){if(t>=1&&10>t)switch(r){case e.MODE_NUMBER:return 10;case e.MODE_ALPHA_NUM:return 9;case e.MODE_8BIT_BYTE:return 8;case e.MODE_KANJI:return 8;default:throw new Error("mode:"+r)}else if(27>t)switch(r){case e.MODE_NUMBER:return 12;case e.MODE_ALPHA_NUM:return 11;case e.MODE_8BIT_BYTE:return 16;case e.MODE_KANJI:return 10;default:throw new Error("mode:"+r)}else{if(!(41>t))throw new Error("type:"+t);switch(r){case e.MODE_NUMBER:return 14;case e.MODE_ALPHA_NUM:return 13;case e.MODE_8BIT_BYTE:return 16;case e.MODE_KANJI:return 12;default:throw new Error("mode:"+r)}}},f.getLostPoint=function(r){for(var t=r.getModuleCount(),e=0,n=0;t>n;n+=1)for(var o=0;t>o;o+=1){for(var a=0,i=r.isDark(n,o),u=-1;1>=u;u+=1)if(!(0>n+u||n+u>=t))for(var f=-1;1>=f;f+=1)0>o+f||o+f>=t||(0!=u||0!=f)&&i==r.isDark(n+u,o+f)&&(a+=1);a>5&&(e+=3+a-5)}for(var n=0;t-1>n;n+=1)for(var o=0;t-1>o;o+=1){var c=0;r.isDark(n,o)&&(c+=1),r.isDark(n+1,o)&&(c+=1),r.isDark(n,o+1)&&(c+=1),r.isDark(n+1,o+1)&&(c+=1),(0==c||4==c)&&(e+=3)}for(var n=0;t>n;n+=1)for(var o=0;t-6>o;o+=1)r.isDark(n,o)&&!r.isDark(n,o+1)&&r.isDark(n,o+2)&&r.isDark(n,o+3)&&r.isDark(n,o+4)&&!r.isDark(n,o+5)&&r.isDark(n,o+6)&&(e+=40);for(var o=0;t>o;o+=1)for(var n=0;t-6>n;n+=1)r.isDark(n,o)&&!r.isDark(n+1,o)&&r.isDark(n+2,o)&&r.isDark(n+3,o)&&r.isDark(n+4,o)&&!r.isDark(n+5,o)&&r.isDark(n+6,o)&&(e+=40);for(var l=0,o=0;t>o;o+=1)for(var n=0;t>n;n+=1)r.isDark(n,o)&&(l+=1);var g=Math.abs(100*l/t/t-50)/5;return e+=10*g},f}(),i=function(){for(var r=new Array(256),t=new Array(256),e=0;8>e;e+=1)r[e]=1<e;e+=1)r[e]=r[e-4]^r[e-5]^r[e-6]^r[e-8];for(var e=0;255>e;e+=1)t[r[e]]=e;var n={};return n.glog=function(r){if(1>r)throw new Error("glog("+r+")");return t[r]},n.gexp=function(t){for(;0>t;)t+=255;for(;t>=256;)t-=255;return r[t]},n}(),u=function(){var r=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],t=function(r,t){var e={};return e.totalCount=r,e.dataCount=t,e},e={},o=function(t,e){switch(e){case n.L:return r[4*(t-1)+0];case n.M:return r[4*(t-1)+1];case n.Q:return r[4*(t-1)+2];case n.H:return r[4*(t-1)+3];default:return}};return e.getRSBlocks=function(r,e){var n=o(r,e);if("undefined"==typeof n)throw new Error("bad rs block @ typeNumber:"+r+"/errorCorrectLevel:"+e);for(var a=n.length/3,i=new Array,u=0;a>u;u+=1)for(var f=n[3*u+0],c=n[3*u+1],l=n[3*u+2],g=0;f>g;g+=1)i.push(t(c,l));return i},e}(),f=function(){var r=new Array,t=0,e={};return e.getBuffer=function(){return r},e.getAt=function(t){var e=Math.floor(t/8);return 1==(r[e]>>>7-t%8&1)},e.put=function(r,t){for(var n=0;t>n;n+=1)e.putBit(1==(r>>>t-n-1&1))},e.getLengthInBits=function(){return t},e.putBit=function(e){var n=Math.floor(t/8);r.length<=n&&r.push(0),e&&(r[n]|=128>>>t%8),t+=1},e},c=function(r){var n=e.MODE_8BIT_BYTE,o=t.stringToBytes(r),a={};return a.getMode=function(){return n},a.getLength=function(r){return o.length},a.write=function(r){for(var t=0;t>>8)},t.writeBytes=function(r,e,n){e=e||0,n=n||r.length;for(var o=0;n>o;o+=1)t.writeByte(r[o+e])},t.writeString=function(r){for(var e=0;e0&&(t+=","),t+=r[e];return t+="]"},t},g=function(){var r=0,t=0,e=0,n="",o={},a=function(r){n+=String.fromCharCode(i(63&r))},i=function(r){if(0>r);else{if(26>r)return 65+r;if(52>r)return 97+(r-26);if(62>r)return 48+(r-52);if(62==r)return 43;if(63==r)return 47}throw new Error("n:"+r)};return o.writeByte=function(n){for(r=r<<8|255&n,t+=8,e+=1;t>=6;)a(r>>>t-6),t-=6},o.flush=function(){if(t>0&&(a(r<<6-t),r=0,t=0),e%3!=0)for(var o=3-e%3,i=0;o>i;i+=1)n+="="},o.toString=function(){return n},o},s=function(r){var t=r,e=0,n=0,o=0,a={};a.read=function(){for(;8>o;){if(e>=t.length){if(0==o)return-1;throw new Error("unexpected end of file./"+o)}var r=t.charAt(e);if(e+=1,"="==r)return o=0,-1;r.match(/^\s$/)||(n=n<<6|i(r.charCodeAt(0)),o+=6)}var a=n>>>o-8&255;return o-=8,a};var i=function(r){if(r>=65&&90>=r)return r-65;if(r>=97&&122>=r)return r-97+26;if(r>=48&&57>=r)return r-48+52;if(43==r)return 62;if(47==r)return 63;throw new Error("c:"+r)};return a},v=function(r,t){var e=r,n=t,o=new Array(r*t),a={};a.setPixel=function(r,t,n){o[t*e+r]=n},a.write=function(r){r.writeString("GIF87a"),r.writeShort(e),r.writeShort(n),r.writeByte(128),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(255),r.writeByte(255),r.writeByte(255),r.writeString(","),r.writeShort(0),r.writeShort(0),r.writeShort(e),r.writeShort(n),r.writeByte(0);var t=2,o=u(t);r.writeByte(t);for(var a=0;o.length-a>255;)r.writeByte(255),r.writeBytes(o,a,255),a+=255;r.writeByte(o.length-a),r.writeBytes(o,a,o.length-a),r.writeByte(0),r.writeString(";")};var i=function(r){var t=r,e=0,n=0,o={};return o.write=function(r,o){if(r>>>o!=0)throw new Error("length over");for(;e+o>=8;)t.writeByte(255&(r<>>=8-e,n=0,e=0;n=r<0&&t.writeByte(n)},o},u=function(r){for(var t=1<u;u+=1)a.add(String.fromCharCode(u));a.add(String.fromCharCode(t)),a.add(String.fromCharCode(e));var c=l(),g=i(c);g.write(t,n);var s=0,v=String.fromCharCode(o[s]);for(s+=1;sa;a+=1)for(var i=0;r>i;i+=1)o.setPixel(i,a,e(i,a));var u=l();o.write(u);for(var f=g(),c=u.toByteArray(),s=0;sn?t.push(n):2048>n?t.push(192|n>>6,128|63&n):55296>n||n>=57344?t.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&r.charCodeAt(e)),t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return t}return t(r)}}(r),r}()); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..758fd64 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "julien731/wp-dismissible-notices-handler": "1.x" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c62b0dc --- /dev/null +++ b/composer.lock @@ -0,0 +1,63 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "74b82c97a6218853ebdff019f454ba00", + "packages": [ + { + "name": "julien731/wp-dismissible-notices-handler", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/julien731/WP-Dismissible-Notices-Handler.git", + "reference": "062b1472ddbac450f87a85fc4b979e04cde3a344" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/julien731/WP-Dismissible-Notices-Handler/zipball/062b1472ddbac450f87a85fc4b979e04cde3a344", + "reference": "062b1472ddbac450f87a85fc4b979e04cde3a344", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "type": "library", + "autoload": { + "files": [ + "handler.php", + "includes/helper-functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GNU GPL" + ], + "authors": [ + { + "name": "Julien Liabeuf", + "email": "julien@liabeuf.fr", + "homepage": "https://julienliabeuf.com", + "role": "Lead Developer" + } + ], + "description": "A simple library to handle Ajax-dismissible admin notices for WordPress", + "homepage": "https://github.com/julien731/WP-Dismissible-Notices-Handler", + "support": { + "issues": "https://github.com/julien731/WP-Dismissible-Notices-Handler/issues", + "source": "https://github.com/julien731/WP-Dismissible-Notices-Handler/tree/1.2.2" + }, + "time": "2021-02-17T15:56:18+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/css/wpga.css b/css/wpga.css deleted file mode 100644 index 9b7dbea..0000000 --- a/css/wpga.css +++ /dev/null @@ -1,31 +0,0 @@ -@import 'simple-hint.min.css'; - -/* Required styles for the tooltip */ -.login form { - overflow: visible; -} -.wpga-label { - position: relative; -} - -/* Style the tooltip */ -.wpga-label span:after { - line-height: 1.6; - font-size: 14px; - padding: 10px 15px; -} - -/* Reset the link style */ -.wpga-hint { - text-decoration: none; -} -.wpga-hint:focus { - box-shadow: none; -} - -/* Hide the tooltip on mobiles */ -@media only screen and (max-width: 768px) { - .wpga-label span { - display: none; - } -} \ No newline at end of file diff --git a/includes/admin/class-settings.php b/includes/admin/class-settings.php new file mode 100644 index 0000000..c28af96 --- /dev/null +++ b/includes/admin/class-settings.php @@ -0,0 +1,512 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ +final class WPGA_Settings { + + /** + * Plugin Options + * + * This is the array that contains all our registered options with their parameters such as option type and default + * value. + * + * @var array + * @since 1.2.0 + */ + protected $_options = null; + + /** + * Plugin Current Values + * + * These are the values that are stored in the database. + * + * @since 0.1.0 + * @var array + */ + protected $_values_current = null; + + /** + * Plugin Default Values + * + * An array of key/value pairs containing the default values for all registered options as declared in the options + * array (also accessible through $_options in this instance). + * + * @since 1.2 + * @var array + */ + protected $_values_default = null; + + /** + * Priority pattern + * + * Because this framework is designed to be used seamlessly in a multisite environment, we need to know what to + * prioritize when the plugin/theme is network-enabled but the options are being worked with on one specific site + * of the network and not in the network administration. + * + * @var string + * @since 0.1.0 + */ + protected $_priority = 'site'; + + /** + * Error Messages + * + * @var array + * @since 0.0.1 + * @access public + */ + public $errors = array(); + + /** + * Class Constructor + * + * @param string $priority Defines who to give the priority to in the case of a MS activation when options are + * handled by one site of the network + */ + public function __construct( $priority = 'site' ) { + + $this->_priority = in_array( trim( $priority ), array( + 'site', + 'network', + ) ) ? trim( $priority ) : 'site'; + + // Hook the menu item + add_action( 'network_admin_menu', array( $this, 'network_settings_page' ) ); + add_action( 'admin_menu', array( $this, 'network_settings_page' ) ); + add_action( 'admin_init', array( $this, 'save_options' ) ); + + } + + /** + * Register the options page + * + * @since 1.2 + * @return void + */ + function network_settings_page() { + + // Network admin menu. + if ( true === is_multisite() && true === WPGA()->is_network_enabled() ) { + $menu_page = add_submenu_page( 'settings.php', esc_html__( 'Authenticator Network Settings', 'wpga' ), esc_html__( 'Authenticator', 'wpga' ), 'administrator', 'wpga-settings', array( + $this, + 'settings_page', + ) ); + } + + // Standalone admin menu. + else { + $menu_page = add_submenu_page( 'options-general.php', sprintf( esc_html__( '%1$s Settings', 'wpga' ), WPGA_NAME ), esc_html__( 'Authenticator', 'wpga' ), 'administrator', 'wpga-settings', array( + $this, + 'settings_page', + ) ); + } + + // Adds my_help_tab when my_admin_page loads + add_action( 'load-' . $menu_page, 'wpga_contextual_help' ); + + } + + /** + * Set Options + * + * Get the plugin options that can be defined throughout the plugin and save them in the _options property of this + * instance. + * + * @since 1.2 + * @return void + */ + protected function _set_options() { + + if ( 'site' === $this->_get_context() ) { + $this->_options = get_option( $key, $this->get_defaults() ); + } else { + $this->_options = get_site_option( $key, $this->get_defaults() ); + } + + $this->_options = apply_filters( 'wpga_options', $this->get_option(), $this->_get_context() ); + } + + /** + * Get Options + * + * Get the plugins options as defined in the settings functions file. + * + * @since 1.2 + * @return array + */ + protected function _get_options() { + + if ( is_null( $this->_options ) ) { + $this->_set_options(); + } + + return $this->_options; + + } + + /** + * Get Options + * + * @since 0.1.0 + * @return array + */ + public function get_options() { + return apply_filters( 'wpga_get_options', $this->_get_values(), $this->_get_context() ); + } + + /** + * Get Option + * + * This method is used to retrieve a plugin option from database. It imitates WordPress' get_option() + * function but it is limited to the plugin's options. This method also returns the scoped value based on the + * current context. + * + * If $default is set to null, the method will return the option's default value as declared in the _options + * property. + * + * @since 1.2 + * + * @param string $option The ID of the option to retrieve + * @param mixed $default The default value to return if no value is found in database. Set to null to get the + * option's default value as declared in $_options + * + * @return mixed Option value if found, null otherwise + */ + public function get_option( $option, $default = null ) { + + $value = $default; + + if ( array_key_exists( $option, $this->get_options() ) ) { + $value = $this->get_options()[ $option ]; + } elseif ( is_null( $value ) ) { + $value = $this->get_default( $option ); + } + + return apply_filters( 'wpga_get_option_' . $option, $value, $this->_get_context() ); + + } + + /** + * Get Settings + * + * The settings are the list of options organized by section defined by the developers. + * + * @since 0.1.0 + * @return array + */ + public function get_settings() { + return apply_filters( 'wpga_get_settings', array(), $this->_get_context() ); + } + + /** + * Get Settings Options + * + * Get the list of options out of the defined settings. This is basically the full list of options without the + * sections. + * + * @since 1.2 + * + * @return array + */ + public function get_settings_options() { + + $options = array(); + + foreach ( $this->get_settings() as $section ) { + foreach ( $section['options'] as $option ) { + array_push( $options, $option ); + } + } + + return $options; + + } + + /** + * Set Current Values + * + * Lookup values in the database and store them in the _values_current of this instance. + * + * @since 1.2 + * @return array + */ + protected function _set_values() { + $key = 'wpga_options'; + $this->_values_current = 'site' === $this->_get_context() ? get_option( $key, false ) : get_site_option( $key, false ); + + if ( false === $this->_values_current ) { + $this->_setup_options(); + $this->_values_current = $this->get_defaults(); + } + } + + /** + * Get Current Values + * + * Get the options values stored in the database. + * + * @since 1.2 + * @return array + */ + protected function _get_values() { + + if ( is_null( $this->_values_current ) ) { + $this->_set_values(); + } + + return $this->_values_current; + + } + + /** + * Set Defaults + * + * Loop through all registered options and retrieve their declared default value. If no default value is declared + * an empty one is used instead. + * + * @since 1.2 + * @return array + */ + protected function _set_defaults() { + + $defaults = array(); + + foreach ( $this->get_settings_options() as $option ) { + if ( ! array_key_exists( $option['id'], $defaults ) ) { + $defaults[ $option['id'] ] = isset( $option['default'] ) ? $option['default'] : ''; + } + } + + $this->_values_default = $defaults; + + } + + /** + * Get Default Values + * + * Return all the options' default values as declared in $_options. + * + * @since 1.2 + * + * @return array + */ + public function get_defaults() { + + if ( is_null( $this->_values_default ) ) { + $this->_set_defaults(); + } + + return apply_filters( 'wpga_get_defaults', $this->_values_default, $this->_get_context() ); + + } + + /** + * Gt Default Value + * + * Get the default value for a specific option as defined in the options array accessible through $_options. + * + * @since 1.2 + * + * @param string $option ID of the option to retrieve the default value for + * + * @return mixed Option default value, false if the option doesn't exist + */ + public function get_default( $option ) { + + $value = false; + + if ( array_key_exists( $option, $this->get_defaults() ) ) { + $value = $this->get_defaults()[ $option ]; + } + + return apply_filters( 'wpga_get_default', $value, $this->_get_context() ); + + } + + /** + * Get the current context based on the $priority property + * + * @since 0.1.0 + * @return string + */ + protected function _get_context() { + + if ( ! is_multisite() || is_multisite() && ! is_network_admin() && 'site' === $this->_priority ) { + return 'site'; + } else { + return 'network'; + } + } + + /** + * Setup Options + * + * Save all options and their default values when it is being used for the very first time and the entry does not + * yet exist in the database. + * + * @since 1.2 + * @return void + */ + protected function _setup_options() { + $this->add_option( 'wpga_options', $this->get_defaults() ); + } + + /** + * Update Plugin Settings + * + * @since 1.2 + * @return void + */ + public function save_options() { + + if ( ! isset( $_POST['wpga_nonce'] ) || ! wp_verify_nonce( $_POST['wpga_nonce'], 'save_options' ) ) { + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $options = $this->get_settings_options(); + $new = array(); + + foreach ( $options as $option ) { + if ( isset( $_POST[ WPGA()->settings->get_field_name( $option['id'] ) ] ) ) { + $new[ $option['id'] ] = $_POST[ WPGA()->settings->get_field_name( $option['id'] ) ]; + } else { + if ( isset( $options[ $option['id'] ] ) ) { + + } + } + } + + $this->update_option( 'wpga_options', $new ); + + // Read-only redirect + wp_safe_redirect( add_query_arg( 'updated', 'true', wpga_get_option_page_link() ) ); + exit; + + } + + /** + * Add Option Based on Context + * + * @since 1.2 + * + * @param string $name Option ID + * @param mixed $value Option value + * + * @return void + */ + public function add_option( $name, $value ) { + if ( 'site' === $this->_get_context() ) { + add_option( $name, $value ); + } else { + add_site_option( $name, $value ); + } + } + + /** + * Update Option Based on Context + * + * @since 1.2 + * + * @param string $name Option ID + * @param mixed $value Option value + * + * @return void + */ + public function update_option( $name, $value ) { + if ( 'site' === $this->_get_context() ) { + update_option( $name, $value ); + } else { + update_site_option( $name, $value ); + } + } + + public function delete_option() {} + + /** + * Settings Page + * + * Display the settings page contents. + * + * @since 1.2 + * @return void + */ + public function settings_page() { + + if ( isset( $_GET['updated'] ) && 'true' === $_GET['updated'] && is_network_admin() ) { ?> +
+

Settings saved.

+
+ +
+ +

+ +
+ get_settings() as $section ) { + + printf( '

%1$s

', esc_attr( $section['title'] ) ); + echo ''; + + foreach ( $section['options'] as $option ) { + + // If no callback is defined we skip this option + if ( ! isset( $option['callback'] ) || ! function_exists( $option['callback'] ) ) { + continue; + } + + // Open a new row and print the option title + printf( '' ); + + } + + echo '
%1$s', esc_attr( $option['title'] ) ); + + // Run the option callback + call_user_func( $option['callback'], $option ); + + // Possibly add a description + if ( isset( $option['desc'] ) ) { + printf( '

%1$s

', wp_kses( $option['desc'], array( 'code' => array() ) ) ); + } + + // Close the row + printf( '
'; + + } + ?> +

+ + +

+ +
+
+ + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_action( 'admin_notices', 'wpga_admin_notices' ); +/** + * Add admin notices + */ +function wpga_admin_notices() { + + if ( isset( $_GET['2fa_reset'] ) && 'true' == $_GET['2fa_reset'] ) { ?> + +
+

array( 'href' => array() ) ) ), '', '' ); ?>

+
+ + esc_html__( 'Your secret key has been regenerated.', 'wpga' ), + '11' => sprintf( esc_html__( 'The key for user %s has been revoked.', 'wpga' ), $uid ), + '12' => sprintf( esc_html__( 'The attempts count has been reset.', 'wpga' ), $uid ), + ); + + if ( ! isset( $messages[ $_GET['update'] ] ) ) { + return; + } + + ?> +
+

+
+ roles; + + if ( in_array( 'yes', $active ) && in_array( 'yes', $force ) ) { + + if ( 'all' === wpga_get_option( 'user_role_status', 'all' ) || array_intersect( $user->roles, $affected ) ) { + + $secret = esc_attr( get_user_meta( $user->ID, 'wpga_secret', true ) ); + $max_attempts = (int) wpga_get_option( 'max_attempts' ); + $attempts = (int) get_user_meta( $user->ID, 'wpga_attempts', true ); + $left = $max_attempts - $attempts; + + if ( '' == $secret ) { + + ?> +
+

+ Please do it now.', 'wpga' ), array( 'a' => array( 'href' => array() ) ) ), admin_url( 'profile.php#wpga' ), $left ); ?> + 0 ) { + printf( wp_kses( __( 'You only have %s login attempts left.', 'wpga' ), array( 'strong' => array() ) ), $left ); + } ?> +

+
+ add_help_tab( array( + 'id' => 'desynchronization', + 'title' => esc_html__( 'Desynchronization', 'wpga' ), + 'content' => wp_kses( __( '

Authorized Clock Desynchronization

First of all, you have to understand how the 2-factor authentication works.

The Google Authenticator will generate a TOTP which stands for Time based One Time Pasword. This one time password, as you might now understand, is generated based on the current time.

If the server\'s (where your site is hosted) clock and the user\'s phone clock are not perfectly synchronized, the one time password generated won\'t work, as it will be generated on a time which is different from the server.

The authorized desynchronization will allow your users more time to use their one time password. By default, one code will be valid for 30 seconds. If you want to give them more time, you can specify a delay in minutes.

Of course, if you give users more time, the security will be lowered. It is advised to stick with the default 30 secs.

', 'wpga' ), array( 'h2' => array(), + 'p' => array(), + 'strong' => array() + ) ), + ) + ); + +} + +add_filter( 'admin_footer_text', 'wpga_version_footer' ); +/** + * Add version number in footer + */ +function wpga_version_footer() { + + if ( ! isset( $_GET['page'] ) OR isset( $_GET['page'] ) && 'wpga_options' !== $_GET['page'] ) { + return; + } + + printf( wp_kses( __( WPGA_NAME . ' version ' . WPGA_VERSION . ' by ' . WPGA_AUTHOR . '.', 'gtsp' ), array( 'a' => array( 'href' => array() ), ) ), esc_url( WPGA_URI ) ); + +} + +/** + * Wrapper function for dnh_register_notice() + * + * The function dnh_register_notice() comes with the WP Dismissible Notices Handler library + * + * @since 1.2 + * + * @param string $id Notice ID, used to identify it + * @param string $type Type of notice to display + * @param string $content Notice content + * @param array $args Additional parameters + * + * @return bool + */ +function wpga_register_notice( $id, $type, $content, $args = array() ) { + + if ( ! function_exists( 'dnh_register_notice' ) ) { + + $file = WPGA_PATH . 'vendor/julien731/wp-dismissible-notices-handler/handler.php'; + + if ( file_exists( $file ) ) { + require_once( $file ); + } + + } + + if ( function_exists( 'dnh_register_notice' ) ) { + dnh_register_notice( $id, $type, $content, $args ); + } + +} + +/** + * Get Options Page URL + * + * Return the URL to the option page whether the plugin is network activated or only activated on a single site. + * + * @since 1.2 + * @return string + */ +function wpga_get_option_page_link() { + + $base = is_network_admin() ? admin_url( 'network/settings.php' ) : admin_url( 'options-general.php' ); + $url = add_query_arg( 'page', 'wpga-settings', $base ); + + return esc_url( $url ); + +} + +add_filter( 'plugin_action_links_' . WPGA_BASENAME, 'wpga_settings_page_link' ); +add_filter( 'network_admin_plugin_action_links_' . WPGA_BASENAME, 'wpga_settings_page_link' ); +/** + * Add a link to the settings page + * + * @since 1.2 + * + * @param array $links Plugin links + * + * @return array Links with the settings + */ +function wpga_settings_page_link( $links ) { + $link = wpga_get_option_page_link(); + $links[] = "" . esc_html__( 'Settings', 'wpga' ) . ""; + + return $links; +} diff --git a/includes/admin/functions-secret.php b/includes/admin/functions-secret.php new file mode 100644 index 0000000..cfa7a37 --- /dev/null +++ b/includes/admin/functions-secret.php @@ -0,0 +1,126 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Generate a secret key based on allowed chars + * base32 compatible. + * + * @return string Secret key + */ +function wpga_generate_secret_key() { + + $validChars = wpga_get_valid_chars(); + $key_length = apply_filters( 'wpga_secret_key_length', 16 ); + + unset( $validChars[32] ); + + $secret = ''; + + for ( $i = 0; $i < $key_length; $i ++ ) { + + $secret .= $validChars[ array_rand( $validChars ) ]; + + } + + return $secret; + +} + +/** + * List the base32 valid chars that can be used for + * secret key generation. + * + * @return array Valid chars + */ +function wpga_get_valid_chars() { + + return array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 + 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 + '=' // padding char + ); +} + +add_action( 'init', 'wpga_edit_secret' ); +/** + * Edit secret key + * + * This function will process various actions on the user's + * secret key such as regenerate or revoke it. All actions + * are checked against a nonce before doing anything. + */ +function wpga_edit_secret() { + + if( ! isset( $_GET['action'] ) ) { + return; + } + + switch( $_GET['action'] ): + + case 'regenerate': + + if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'regenerate_key' ) ) { + return; + } + + delete_user_meta( 'get_current_user_id()', 'wpga_secret' ); + update_user_meta( get_current_user_id(), 'wpga_secret', wpga_generate_secret_key() ); + wp_redirect( add_query_arg( array( 'update' => '10' ), admin_url( 'profile.php#wpga' ) ) ); + exit; + + break; + + case 'revoke': + + if ( ! isset( $_GET['user_id'] ) ) { + return; + } + + if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'revoke_key' ) ) { + return; + } + + if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) { + return; + } + + delete_user_meta( $_GET['user_id'], 'wpga_secret' ); + delete_user_meta( $_GET['user_id'], 'wpga_backup_key' ); + wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '11' ), admin_url( 'user-edit.php' ) ) ); + exit; + + break; + + case 'reset': + + if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'reset_key' ) ) { + return; + } + + if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) { + return; + } + + delete_user_meta( $_GET['user_id'], 'wpga_attempts' ); + delete_user_meta( $_GET['user_id'], 'wpga_backup_key' ); + wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '12' ), admin_url( 'user-edit.php' ) ) ); + exit; + + break; + + endswitch; + +} \ No newline at end of file diff --git a/includes/admin/functions-settings.php b/includes/admin/functions-settings.php new file mode 100644 index 0000000..47708fc --- /dev/null +++ b/includes/admin/functions-settings.php @@ -0,0 +1,125 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_filter( 'wpga_get_settings', 'wpga_get_settings' ); +/** + * Plugin Settings + * + * Register all the core settings for the plugin. + * + * @since 1.0 + * @return array + */ +function wpga_get_settings( $options ) { + + $options['general'] = array( + 'title' => esc_html__( 'General', 'wpga' ), + 'options' => array( + array( + 'id' => 'active', + 'title' => __( 'Activate Plugin', 'wpga' ), + 'desc' => __( 'Do you wish to enable the 2-factor authentication for this site?', 'wpga' ), + 'callback' => 'wpga_option_callback_checkbox', + 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ), + 'default' => '', + ), + array( + 'id' => 'force_2fa', + 'title' => __( 'Force Use', 'wpga' ), + 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ), + 'callback' => 'wpga_option_callback_checkbox', + 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ), + ), + array( + 'id' => 'user_role_status', + 'title' => '', + 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ), + 'callback' => '', + 'default' => 'all', + ), + array( + 'id' => 'user_roles', + 'title' => __( 'Force Roles', 'wpga' ), + 'desc' => __( 'You can force users to use 2-factor authentication by role. Requires «Force Use» to be enabled. If no role is checked, 2FA will be forced for ALL roles.', 'wpga' ), + 'callback' => 'wpga_option_callback_user_roles', + 'opts' => wpga_get_editable_roles(), + ), + array( + 'id' => 'blog_name', + 'title' => __( 'Site Name', 'wpga' ), + 'desc' => __( 'Name under which this site will appear in the Google Authenticator app.', 'wpga' ), + 'callback' => 'wpga_option_callback_text', + 'default' => get_bloginfo( 'name' ), + ), + ), + ); + + $options['security'] = array( + 'title' => esc_html__( 'Security', 'wpga' ), + 'options' => array( + array( + 'id' => 'max_attempts', + 'title' => __( 'Max Attempts', 'wpga' ), + 'desc' => __( 'If you chose to force users to use 2-factor authentication, you can specify a maximum number of times a user can login WITHOUT setting up the 2-factor authentication (leave 0 for unlimited attempts).', 'wpga' ), + 'callback' => 'wpga_option_callback_text_small', + 'default' => 3, + ), + array( + 'id' => 'authorized_delay', + 'title' => __( 'Authorized Clock Desynchronization', 'wpga' ), + 'desc' => __( 'Must be in min (±). Avoid invalid one-time passwords issues. Please read the contextual help for more info.', 'wpga' ), + 'callback' => 'wpga_option_callback_text_small', + 'default' => 0, + ), + ), + ); + + return apply_filters( 'wpga_settings', $options ); + +} + +/** + * Get roles list. + * + * @since 1.0.9 + * @return array List of editable roles + */ +function wpga_get_editable_roles() { + global $wp_roles; + + $all_roles = $wp_roles->roles; + $editable_roles = apply_filters( 'editable_roles', $all_roles ); + $list = array(); + + foreach ( $editable_roles as $role_id => $role ) { + $list[ $role_id ] = $role['name']; + } + + return $list; + +} + +/** + * Get plugin option + * + * @since 1.2.0 + * + * @param string $option ID of the option to lookup + * @param mixed $default Default value to return + * + * @return mixed + */ +function wpga_get_option( $option, $default = null ) { + return WPGA()->settings->get_option( $option, $default ); +} \ No newline at end of file diff --git a/includes/admin/functions-user-profile.php b/includes/admin/functions-user-profile.php new file mode 100644 index 0000000..98b5f77 --- /dev/null +++ b/includes/admin/functions-user-profile.php @@ -0,0 +1,258 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_action( 'show_user_profile', 'wpga_user_profile_fields' ); +/** + * Add profile custom fields + * + * @param WP_User $user The WP_User object of the user being edited. + * @return void + */ +function wpga_user_profile_fields( $user ) { + + // Register thickbox + add_thickbox(); + + $force = wpga_get_option( 'force_2fa' ); + $qr = true; + $width = apply_filters( 'wpga_user_profile_qr_width', 300 + 10 ); + $height = apply_filters( 'wpga_user_profile_qr_height', 300 + 10 ); + $secret = get_user_meta( $user->ID, 'wpga_secret', true ); + $args = array( 'action' => 'regenerate' ); + $backup = get_user_meta( $user->ID, 'wpga_backup_key', true ); + + if ( isset( $_GET['user_id'] ) ) { + $args['user_id'] = (int) $_GET['user_id']; + } + + $regenerate = wp_nonce_url( add_query_arg( $args, admin_url( 'profile.php' ) ), 'regenerate_key' ); + + if ( '' == $secret ) { + $secret = wpga_generate_secret_key(); + $qr = false; + } + ?> + +

+ + + + ID, 'wpga_active', true ) ); + + if ( 'yes' == $active ) { + $checked = 'checked="checked"'; + } else { + $checked = ''; + } ?> + + + + + + + + + + + + + + ID, 'wpga_backup_key_time', true ); + $limit = $time + 300; // Recovery key generation time + 5 mins + ?> + + + + + + +
+ />
+

+
+ + + +

+ + + + + +

+ + +
+ + + +

+ + + + + + + + + +
+ 'revoke', 'user_id' => $user_id ); + $rst_arg = array( 'action' => 'reset', 'user_id' => $user_id ); + $revoke = wp_nonce_url( add_query_arg( $args, admin_url( 'user-edit.php' ) ), 'revoke_key' ); + $rst = wp_nonce_url( add_query_arg( $rst_arg, admin_url( 'user-edit.php' ) ), 'reset_key' ); + $attempts = (int) get_user_meta( $user_id, 'wpga_attempts', true ); + $max_attempts = apply_filters( 'wpga_totp_max_attempts', WPGA()->settings->get_option( 'max_attempts' ) ); + ?> +

+ + + + + + + + + + + + +
+ +

+ +

+ +
+ + + $max_attempts ) { echo '' . esc_html_e( '(This user is locked out)', 'wpga' ) . ''; } ?> +

+
+ + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +register_activation_hook( WPGA_BASENAME, 'wpga_installPlugin' ); +/** + * Register settings on plugin activation + */ +function wpga_installPlugin() { + + $defaults = array( + 'blog_name' => get_bloginfo( 'name' ), + 'max_attempts' => 3, + 'authorized_delay' => 0, + ); + + add_option( WPGA_PREFIX . '_options', $defaults ); + + // Add a new cron hook + if ( ! wp_next_scheduled( 'wpas_clean_totps' ) ) { + wp_schedule_event( time(), 'daily', 'wpas_clean_totps' ); + } + + // Create custom database tables + wpga_recovery_keys_create_table(); + wpga_apps_access_log_create_table(); + +} \ No newline at end of file diff --git a/admin/views/apps-passwords.php b/includes/admin/views/apps-passwords.php similarity index 98% rename from admin/views/apps-passwords.php rename to includes/admin/views/apps-passwords.php index 30258c4..767c4d0 100644 --- a/admin/views/apps-passwords.php +++ b/includes/admin/views/apps-passwords.php @@ -46,11 +46,11 @@ $last_ip = false !== $last ? $last['ip'] : '-'; ?> > - + - + diff --git a/includes/admin/views/options/option-checkbox.php b/includes/admin/views/options/option-checkbox.php new file mode 100644 index 0000000..66dc1a0 --- /dev/null +++ b/includes/admin/views/options/option-checkbox.php @@ -0,0 +1,36 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +/** + * Option Checkbox + * + * @since 1.2 + * + * @param array $option Current option parameters + * + * @return void + */ +function wpga_option_callback_checkbox( $option ) { + + if ( ! isset( $option['opts'] ) ) { + return; + } + + $option_id = esc_attr( $option['id'] ); + + foreach ( (array) $option['opts'] as $val => $title ) { + + $id = $option_id . '_' . $val; ?> + + + + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +/** + * Option Text Small + * + * @since 1.2 + * + * @param array $option Current option parameters + * + * @return void + */ +function wpga_option_callback_text_small( $option ) { + wpga_option_callback_text( $option, 'small-text' ); +} \ No newline at end of file diff --git a/includes/admin/views/options/option-text.php b/includes/admin/views/options/option-text.php new file mode 100644 index 0000000..5df4de9 --- /dev/null +++ b/includes/admin/views/options/option-text.php @@ -0,0 +1,23 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +/** + * Option Text + * + * @since 1.2 + * + * @param array $option Current option parameters + * @param string $class An optional class to add to the field. Useful for printing small and large inputs + * + * @return void + */ +function wpga_option_callback_text( $option, $class = 'regular-text' ) { + $option_id = esc_attr( $option['id'] ); ?> + + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +/** + * Option User Roles + * + * @since 1.2 + * + * @param array $option Current option parameters + * + * @return void + */ +function wpga_option_callback_user_roles( $option ) { + + $option_id = esc_attr( $option['id'] ); + $value = WPGA()->settings->get_option( $option_id, array() ); + $status = WPGA()->settings->get_option( 'user_role_status', 'all' ); + $checked_all = ( 'all' === $status ) ? 'checked="checked"' : ''; + $checked_custom = ( 'custom' === $status ) ? 'checked="checked"' : ''; + ?> + +
+ +
+ +
+ +
+ + +
+ +
+ + $title ): + + $id = $option_id . '_' . $val; ?> + +
+ + + +
+ + +

+ +
+ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +class WPGA_Access_Log { + + /** + * Log a new access using an app password + * + * @since 1.2 + * @return false|int + */ + public function log_access( $user_id, $key_id, $time = '', $ip, $user_agent, $method = '' ) { + + $user = get_user_by( 'id', $user_id ); + + // Make sure the user exists + if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) { + return false; + } + + if ( empty( $data['time'] ) || '0000-00-00 00:00:00' == $data['time'] ) { + $data['time'] = current_time( 'mysql' ); + } + + global $wpdb; + + $data = array( + 'ID' => false, + 'user_id' => (int) $user_id, + 'key_id' => (int) $key_id, + 'time' => $time, + 'ip' => $ip, + 'user_agent' => sanitize_text_field( $user_agent ), + 'method' => sanitize_text_field( $method ), + ); + + $insert = $wpdb->insert( wpga_apps_access_log_table, $data, array( '%s', '%s', '%d', '%s', '%s', '%s', '%s' ) ); + + return false === $insert ? false : $wpdb->insert_id; + + } + + /** + * Get log entries + * + * @since 1.2 + * @return array + */ + public function get_entries() {} + + /** + * Get log entries by field + * + * @since 1.2 + * @return array + */ + public function get_entries_by() {} + +} \ No newline at end of file diff --git a/includes/class-authenticate.php b/includes/class-authenticate.php new file mode 100644 index 0000000..b0c477f --- /dev/null +++ b/includes/class-authenticate.php @@ -0,0 +1,259 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +class WPGA_Authenticate { + + /** + * Defines if the plugin is active + * + * @since 1.2.0 + * @var bool + */ + protected $is_2fa_active = null; + + /** + * Submitted TOTP + * + * @since 1.2.0 + * @var string + */ + public $totp; + + /** + * The user secret key + * + * @since 1.2.0 + * @var string + */ + protected $secret; + + /** + * The user object + * + * @since 1.2 + * @var WPGA_User + */ + protected $user; + + public function __construct() { + add_action( 'wp_authenticate_user', array( $this, 'authenticate' ), 10, 3 ); + add_filter( 'authenticate', array( $this, 'checkAppPassword' ), 50, 3 ); + } + + /** + * Get the user TOTP + * + * @since 1.2.0 + * @return null|string + */ + public function get_totp() { + + if ( is_null( $this->totp ) ) { + $this->totp = isset( $_POST['totp'] ) ? sanitize_key( $_POST['totp'] ) : null; + } + + return $this->totp; + + } + + /** + * Add TOTP check to WordPress authentication process + * + * @param WP_User|WP_Error $user + * + * @return object User object on success or WP_Error on failure + */ + public function authenticate( $user ) { + + if ( true !== wpga_is_2fa_active( $user ) ) { + return $user; + } + + if ( ! is_wp_error( $user ) ) { + + // Instantiate our user class for easy access to user data + if ( is_null( $this->user ) ) { + $this->user = new WPGA_User( $user ); + } + + $secret = $this->user->get_secret(); + $totp = $this->get_totp(); + + /* Let's make sure the user has generated a secret */ + if ( '' !== $secret ) { + + if ( is_null( $totp ) ) { + return new WP_Error( 'no_totp', esc_html__( 'An error is preventing the 2-factor authentication from authenticating your session.', 'wpga' ) ); + } + + if ( empty( $totp ) ) { + return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) ); + } + + $totp_valid = $this->user->is_otp_valid( $totp ); + + if ( is_wp_error( $totp_valid ) ) { + return $totp_valid; + } + + // If TOTP is valid we revoke it and continue loggin in + if ( true === $totp_valid ) { + + // Revoke the TOTP + wpga_revoke_totp( $totp ); + + return $user; + + } + + /** + * Check if the user is sending a recovery key. + * + * If the recovery key is valid, we deactivate + * 2FA for this user so that he can log-in + * without using the app. + * + * @since 1.0.4 + */ + elseif ( $this->user->is_recovery_key( $totp ) ) { + + // Disable 2FA for this user + $this->user->deactivate_2fa(); + + /* Add URL var to the login redirect */ + add_filter( 'login_redirect', 'wpga_login_redirect_notify' ); + + return $user; + + } else { + return new WP_Error( 'totp_invalid', esc_html__( 'The one time password is incorrect or expired. Please try with a newly generated password.', 'wpga' ) ); + } + + } else { + + /* TOTP is forced for all users */ + if ( wpga_is_2fa_forced( $user->roles ) ) { + + /* If the admin set the max attempts to unlimited we give up on security :( */ + if ( - 1 === $this->user->remaining_attempts() ) { + return $user; + } + + if ( $this->user->remaining_attempts() > 0 ) { + + $this->user->add_attempt(); + + return $user; + } else { + return new WP_Error( '2fa_max_attempts', esc_html__( 'You have reached the maximum number of logins WITHOUT using 2-factor authentication. Please contact the admin to reset your account.', 'wpga' ) ); + } + + } else { + // No TOTP check? Just return the user for standard authentication + return $user; + } + + } + + } else { + return $user; + } + + } + + /** + * Check for app password. + * + * If the user has created one or more apps passwords, + * we check if the given password is a registered one. + * + * @since 1.1.0 + * + * @param WP_User $user The user object + * @param string $username The username to authenticate + * @param string $password The user password + * + * @return null|WP_User|WP_Error A user object on success or an error message + */ + public function checkAppPassword( $user, $username, $password ) { + + // Only do our work if the authentication failed. + if ( ! is_wp_error( $user ) ) { + return $user; + } + + $user_data = get_user_by( 'login', $username ); + + if ( ! is_object( $user_data ) || ! is_a( $user_data, 'WP_User' ) ) { + return null; + } + + // Get the WPGA user object + $wpga_user = new WPGA_User( $user_data ); + + if ( $wpga_user->has_app_passwords() ) { + + $passwords = $wpga_user->get_app_passwords_codes(); // Get all user app passwords + $hash = md5( $password ); // Hash the given supposed app password + + if ( in_array( $hash, $passwords ) ) { + + $pwd_keys = array_flip( $passwords ); + + WPGA()->access_log->log_access( $user_data->ID, $pwd_keys[ $hash ], current_time( 'mysql' ), $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ); + + // INCREMENT COUNT + + return new WP_User( $user_data->ID ); + + } else { + return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) ); + } + } else { + return $user; + } + } + + /** + * Check if the current user has app passwords. + * + * @since 1.1.0 + * + * @param int $user_id A user ID + * + * @return boolean True if has app passwords + */ + public function has_app_passwords( $user_id ) { + + $passwords = wpga_get_app_passwords( $user_id ); + + if ( empty( $passwords ) ) { + return false; + } else { + return true; + } + + } + +} + +/** + * Add a URL var to login redirect page + * + * @return string Redirect URL + * @since 1.0.4 + */ +function wpga_login_redirect_notify() { + return add_query_arg( array( '2fa_reset' => 'true' ), admin_url() ); +} \ No newline at end of file diff --git a/includes/class-recovery-key.php b/includes/class-recovery-key.php new file mode 100644 index 0000000..59bc940 --- /dev/null +++ b/includes/class-recovery-key.php @@ -0,0 +1,315 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +class WPGA_Recovery_Key { + + /** + * Generate a new, unique recovery key + * + * @since 1.2 + * @return string + */ + public function generate_key() { + + $key = $this->generate_random_key(); + + if ( true === $this->key_exists( $key ) ) { + do { + $key = $this->generate_random_key(); + } while ( true === $this->key_exists( $key ) ); + } + + return $key; + + } + + /** + * Generate a random recovery key + * + * @since 1.2 + * @return string + */ + private function generate_random_key() { + + $length = apply_filters( 'wpga_recovery_code_length', 24 ); + $max = ceil( $length / 40 ); + $random = ''; + + for ( $i = 0; $i < $max; $i ++ ) { + $random .= sha1( microtime( true ) . mt_rand( 10000, 90000 ) ); + } + + return substr( $random, 0, $length ); + + } + + /** + * Add a new recovery key + * + * @since 1.2 + * + * @param int $user_id The ID of the user whose key it is + * @param string $key The key to add + * @param string $name An optional name for the key to be added + * @param string $type The type of entry + * + * @return int|false New row ID if insertion is successful, false otherwise + */ + public function add_key( $user_id, $key, $name = '', $type = 'recovery_key' ) { + + $user = get_user_by( 'id', $user_id ); + + // Make sure the user exists + if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) { + return false; + } + + // Sanitize the entry type + if ( ! $this->type_exists( $type ) ) { + $type = 'recovery_key'; + } + + global $wpdb; + + $data = array( + 'ID' => false, + 'user_id' => (int) $user_id, + 'time' => current_time( 'mysql' ), + 'code' => md5( sanitize_key( $key ) ), + 'name' => sanitize_text_field( $name ), + 'type' => $type, + 'count' => 0, + ); + + $insert = $wpdb->insert( wpga_recovery_keys_table, $data, array( '%s', '%s', '%d', '%s', '%s', '%s', '%s' ) ); + + return false === $insert ? false : $wpdb->insert_id; + + } + + /** + * Check if a given key type is valid + * + * @since 1.2 + * + * @param string $type Key type + * + * @return bool + */ + public function type_exists( $type ) { + return in_array( $type, array( 'recovery_key', 'app_password' ) ) ? true : false; + } + + /** + * Get a recovery key + * + * @since 1.2 + * + * @param string $field What field to use to lookup the key + * @param mixed $value Value of the lookup field + * @param bool $single Should the method return a single result or not + * @param string $type Optional specify what type of key should be looked up + * + * @return array|WP_Error An array containing the key information on success + */ + public function get_key_by( $field = 'ID', $value, $single = false, $type = '' ) { + + // Sanitize field + if ( ! in_array( $field, array( 'ID', 'code', 'user_id', 'type' ) ) ) { + return new WP_Error( 'invalid_field', esc_html__( 'The field you are trying to lookup is invalid', 'wpga' ) ); + } + + // Make sure the user exists + if ( 'user_id' === $field ) { + + $user = get_user_by( 'id', $value ); + + if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) { + return new WP_Error( 'invalid_user', esc_html__( 'The user is invalid', 'wpga' ) ); + } + + } + + // Make sure the id field is uppercase + if ( 'id' === $field ) { + $field = 'ID'; + } + + if ( 'code' === $field ) { + $value = md5( $value ); + } + + // Set the base query arguments + $args = array( 'where' => "$field = '$value'" ); + + // Possibly add the key type + if ( ! empty( $type ) && $this->type_exists( $type ) ) { + $args['where'] .= " AND type = '$type'"; + } + + $result = $this->get( $args ); + + if ( is_array( $result ) && count( $result ) > 1 && true === $single ) { + $result = $result[0]; + } + + return $result; + + } + + /** + * Check if a given key exists + * + * @since 1.2 + * + * @param string $key The key to check + * + * @return bool + */ + public function key_exists( $key ) { + + $key = $this->get_key_by( 'code', $key, true ); + + if ( is_array( $key ) && ! empty( $key ) ) { + return true; + } + + return false; + + } + + /** + * Get all the recovery keys + * + * @since 1.2 + * @return array + */ + public function get_keys() { + return $this->get(); + } + + /** + * Get all keys for a particular user + * + * @since 1.2 + * + * @param int $user_id ID of the user whose keys we want to retrieve + * + * @return array + */ + public function get_user_keys( $user_id ) { + return $this->get_key_by( 'user_id', $user_id ); + } + + /** + * Get a user's recovery keys + * + * @since 1.2 + * + * @param $user_id + * + * @return bool|string + */ + public function get_recovery_keys( $user_id ) { + + $keys = array(); + $recovery = $this->get_key_by( 'user_id', $user_id ); + + if ( is_array( $recovery ) ) { + foreach ( $recovery as $key ) { + if ( 'recovery_key' === $key['type'] ) { + $keys[] = $key['code']; + } + } + } + + return $keys; + + } + + /** + * Get the actual recovery code from a key entry + * + * @since 1.2 + * + * @param array $key A user's key + * + * @return false|string + */ + public function get_key_code( $key ) { + + if ( ! is_array( $key ) || ! isset( $key['code'] ) ) { + return false; + } + + return $key['code']; + + } + + /** + * Delete a key from the database + * + * @since 1.2 + * + * @param int $id ID of the key to delete + * + * @return bool + */ + public function delete_key( $id ) { + + $key = $this->get_key_by( 'ID', $id, true ); + + if ( false === $key || is_wp_error( $key ) ) { + return false; + } + + global $wpdb; + + $wpdb->delete( wpga_recovery_keys_table, array( 'ID' => (int) $id ), array( '%d' ) ); + + return true; + + } + + /** + * Run a query on the recovery keys table + * + * @since 1.2 + * + * @global $wpdb + * + * @param array $args Query arguments + * @param string $output Desired output format + * + * @return array + */ + private function get( $args = array(), $output = 'ARRAY_A' ) { + + global $wpdb; + + $query = 'SELECT * FROM ' . wpga_recovery_keys_table; + + if ( isset( $args['where'] ) ) { + $query .= ' WHERE ' . $args['where']; + } else { + $query .= ' WHERE 1'; + } + + $row = $wpdb->get_results( $query, $output ); + + return $row; + + } + +} \ No newline at end of file diff --git a/includes/class-user.php b/includes/class-user.php new file mode 100644 index 0000000..8500cda --- /dev/null +++ b/includes/class-user.php @@ -0,0 +1,343 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +class WPGA_User { + + /** + * The user ID + * + * @since 1.2 + * @var int + */ + public $user_id; + + /** + * The standard WordPress user object + * + * @since 1.2 + * @var WP_User + */ + private $user; + + /** + * The 2FA status for the user + * + * Whether or not the user has 2FA enabled and setup. If 2FA is enabled but the user hasn't (yet) set it up, the + * property remains false. + * + * @since 1.2 + * @var bool + */ + protected $has_2fa; + + /** + * Holds the number of failed login attempts + * + * @since 1.2 + * @var int + */ + protected $login_attempts; + + /** + * Holds the number of remaining login attempts before account lockout + * + * @since 1.2 + * @var int + */ + protected $remaining_attempts; + + /** + * User secret key + * + * @since 1.2 + * @var string + */ + protected $secret; + + /** + * A list of the user's app passwords + * + * @since 1.2 + * @var array + */ + protected $app_passwords; + + /** + * WPGA_User constructor + * + * @since 1.2 + * + * @param int|WP_User $user + */ + public function __construct( $user ) { + + if ( is_object( $user ) && is_a( $user, 'WP_User' ) ) { + $this->user_id = $user->ID; + $this->user = $user; + } elseif ( is_numeric( $user ) ) { + + $this->user = get_user_by( 'id', $user ); + + if ( is_object( $this->user ) && is_a( $this->user, 'WP_User' ) ) { + $this->user_id = $this->user->ID; + } + + } + + } + + /** + * Check if user has 2FA enabled + * + * @since 1.2 + * @return bool + */ + public function has_2fa() { + + if ( ! is_null( $this->has_2fa ) && is_bool( $this->has_2fa ) ) { + return $this->has_2fa; + } + + $this->has_2fa = false; + + if ( 'yes' === get_user_meta( $this->user_id, 'wpga_active', true ) ) { + $this->has_2fa = true; + } + + return $this->has_2fa; + + } + + /** + * Get user secret key + * + * @since 1.2 + * @return string + */ + public function get_secret() { + + if ( ! is_null( $this->secret ) ) { + return $this->secret; + } + + return $this->secret = get_user_meta( $this->user_id, 'wpga_secret', true ); + + } + + /** + * Get the number of failed login attempts + * + * THis method returns the number of failed attempts or false if there is no failed attempts. + * + * @since 1.2 + * @return int|false + */ + public function login_attempts() { + + if ( ! is_null( $this->login_attempts ) && is_int( $this->login_attempts ) ) { + return $this->login_attempts; + } + + $this->login_attempts = (int) get_user_meta( $this->user_id, 'wpga_attempts', true ); + + if ( empty( $this->login_attempts ) ) { + $this->login_attempts = 0; + } + + return $this->login_attempts; + + } + + /** + * Get the number of attempts remaining + * + * Based on the number of failed attempts, calculate the number of attempts the user can make before locking his + * account. + * + * @since 1.2 + * @return int + */ + public function remaining_attempts() { + + if ( ! is_null( $this->remaining_attempts ) && is_int( $this->remaining_attempts ) ) { + return $this->remaining_attempts; + } + + $options = get_option( 'wpga_options', array() ); + $max_attempts = ( isset( $options['max_attempts'] ) && '' != $options['max_attempts'] ) ? (int) $options['max_attempts'] : 3; + + if ( - 1 === $max_attempts ) { + return $max_attempts; + } + + $this->remaining_attempts = 0; // Set the remaining attempts number ot 0 for security + $attempts = $this->login_attempts(); + + if ( $attempts < $max_attempts ) { + $this->remaining_attempts = $max_attempts - $attempts; + } + + return $this->remaining_attempts; + + } + + /** + * Add a new attempt to the number of failed attempts + * + * @since 1.2 + * @return int The new number of failed attempts + */ + public function add_attempt() { + + $attempts = $this->login_attempts(); + + // Increment in database + update_user_meta( $this->user_id, 'wpga_attempts', $attempts + 1, $attempts ); + + // Update the internal property + $this->login_attempts = $attempts + 1; + + return $this->login_attempts; + + } + + /** + * Deactivate 2FA for the current user + * + * @since 1.2 + * @return void + */ + public function deactivate_2fa() { + /* Clean the 2FA data */ + delete_user_meta( $this->user_id, 'wpga_active' ); + delete_user_meta( $this->user_id, 'wpga_attempts' ); + delete_user_meta( $this->user_id, 'wpga_secret' ); + delete_user_meta( $this->user_id, 'wpga_backup_key' ); + delete_user_meta( $this->user_id, 'wpga_backup_key_time' ); + } + + /** + * Check the validity of a one-time password for the current user + * + * @since 1.2 + * + * @param string $otp The OTP to check with this user + * + * @return bool|WP_Error + */ + public function is_otp_valid( $otp ) { + + // Set the validity to false for the start + $valid = false; + + // Get the authorized discrepancy delay and calculate the allowed time range + $options = get_option( 'wpga_options' ); + $drift = isset( $options['authorized_delay'] ) ? (int) $options['authorized_delay'] * 2 : 1; + $currentTimeSlice = floor( time() / 30 ); + + // Generate all OTPs for the allowed time range and compare them to the OTP we got + for ( $i = - $drift; $i <= $drift; $i ++ ) { + + // Generate a new valid OTP + $currently_valid_otp = wpga_get_code( $this->get_secret(), $currentTimeSlice + $i ); + + if ( $currently_valid_otp === $otp ) { + $valid = true; + break; // If we get a match no need to generate other OTPs + } + + } + + // If we didn't find a valid OTP no need to go any further. The login attempt is simply invalid. + if ( false === $valid ) { + return false; + } + + // Make sure that the OTP hasn't been used yet, in which case we do not accept it + if ( true === wpga_was_otp_used( $otp ) ) { + $valid = new WP_Error( 'expired_totp', esc_html__( 'The one time password you used has already been revoked.', 'wpga' ) ); + } + + return $valid; + + } + + /** + * Check if the given key is one of the user's recovery keys + * + * @since 1.2 + * + * @param string $key + * + * @return bool + */ + public function is_recovery_key( $key ) { + + $is_key = false; + $keys = wpga_get_user_recovery_keys( $this->user_id ); + + if ( in_array( md5( sanitize_key( $key ) ), $keys ) ) { + $is_key = true; + } + + return $is_key; + + } + + /** + * Get the user's app passwords if any + * + * @since 1.2 + * @return array + */ + public function get_app_passwords() { + + if ( is_array( $this->app_passwords ) ) { + return $this->app_passwords; + } + + return WPGA()->recovery->get_key_by( 'user_id', $this->user_id, false, 'app_password' ); + + } + + /** + * Get the actual list of the user's app passwords + * + * @since 1.2 + * @return array + */ + public function get_app_passwords_codes() { + + $passwords = $this->get_app_passwords(); + $codes = array(); + + foreach ( $passwords as $key ) { + $codes[ $key['ID'] ] = $key['code']; + } + + return $codes; + + } + + /** + * Check if the user has any app passwords set + * + * @since 1.2 + * @return bool + */ + public function has_app_passwords() { + return empty( $this->get_app_passwords() ) ? false : true; + } + +} \ No newline at end of file diff --git a/includes/functions-apps-passwords.php b/includes/functions-apps-passwords.php new file mode 100644 index 0000000..5791c7a --- /dev/null +++ b/includes/functions-apps-passwords.php @@ -0,0 +1,288 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Generate a unique key. + * + * The key is used to identify the app password. + * It is an unchangeable unique value. + * + * @since 1.1.0 + * @param string $hash Hash of the newly created password + * @return string Unique identifying key + */ +function wpga_make_unique_key( $hash ) { + + $passwords = wpga_get_app_passwords(); + $key = substr( $hash, 0, 5 ); + + if ( !array_key_exists( $key, $passwords ) ) { + return $key; + } + + $index = 0; + $key = $key . $index; + + while ( array_key_exists( $key, $passwords ) ) { + ++$index; + $key = substr( $key, 0, 5 ) . $index; + } + + return $key; + +} + +function wpga_get_last_access( $key ) { + + global $current_user; + + $log = wpga_get_app_passwords_log(); + $last = array(); + + if ( empty( $log ) ) { + return false; + } + + foreach ( $log as $date => $entry ) { + if ( $key === $entry['key'] ) { + array_push( $last, $entry ); + } + } + + if ( empty( $last ) ) { + return false; + } + + $count = count( $last ) - 1; + + return $last[$count]; + +} + +/** + * Get app passwords for a user + * + * @since 1.1 + * + * @param null|int $user_id + * + * @return array + */ +function wpga_get_app_passwords( $user_id = null ) { + + if ( is_null( $user_id ) ) { + global $current_user; + $user_id = $current_user->ID; + } + + $user = new WPGA_User( $user_id ); + + return $user->get_app_passwords(); + +} + +function wpga_get_app_passwords_log( $user_id = null ) { + + if ( is_null( $user_id ) ) { + global $current_user; + $user_id = $current_user->ID; + } + + $log = is_array( $p = get_user_meta( $user_id, 'wpga_apps_passwords_log', true ) ) ? $p : array(); + krsort( $log ); + + return $log; + +} + +/** + * Delete a user's app password + * + * @since 1.1 + * + * @param string $id ID of the key to delete + * + * @return bool + */ +function wpga_delete_app_password( $id ) { + return WPGA()->recovery->delete_key( $id ); +} + +/** + * Delete all of a user's app passwords at one + * + * @since 1.1 + * @return bool + */ +function wpga_reset_app_passwords() { + + global $current_user; + + $result = true; + + $keys = WPGA()->recovery->get_key_by( 'user_id', $current_user->ID, false, 'app_password' ); + + if ( is_array( $keys ) ) { + foreach ( $keys as $key ) { + if ( ! wpga_delete_app_password( $key['ID'] ) ) { + $result = false; + } + } + } + + return $result; + +} + +function wpga_clear_log() { + global $current_user; + delete_user_meta( $current_user->ID, 'wpga_apps_passwords_log' ); +} + +add_action( 'wp_ajax_wpga_create_app_password', 'wpga_create_app_password' ); +/** + * Create a new app password. + * + * @since 1.1.0 + */ +function wpga_create_app_password() { + + if ( ! isset( $_POST['description'] ) || empty( $_POST['description'] ) ) { + die(); + } + + global $current_user; + + $pwd = WPGA()->recovery->generate_key(); + $app_pwd = WPGA()->recovery->add_key( $current_user->ID, $pwd, sanitize_text_field( $_POST['description'] ), 'app_password' ); + + if ( false !== $app_pwd ) { + $return = json_encode( array( + 'desc' => sanitize_text_field( $_POST['description'] ), + 'pwd' => esc_attr( $pwd ), + ) ); + } else { + $return = json_encode( array( 'error' ) ); + } + + echo esc_attr( urlencode( $return ) ); + die(); + +} + +add_action( 'admin_menu', 'wpga_add_app_password_menu' ); +/** + * Add required menu items + */ +function wpga_add_app_password_menu() { + add_users_page( + esc_html__( 'Google Authenticator Applications Passwords', 'wpga' ), + esc_html__( 'My Apps Passwords', 'wpga' ), + 'read', + WPGA_PREFIX . '_apps_passwords', + 'wpga_apps_passwords_display' + ); +} + +/** + * Display the applications passwords apge. + * + * @since 1.1.0 + */ +function wpga_apps_passwords_display() { + require_once( WPGA_PATH . 'includes/admin/views/apps-passwords.php' ); +} + +add_action( 'admin_init', 'wpas_apps_passwords_actions' ); +/** + * Run app passwords related actions. + * + * Run the actions and redirect to the user's page + * in "read only" mode, without the URL vars that can cause + * undesired actions (like clearing the log again). + * + * @since 1.1.0 + * @return void + */ +function wpas_apps_passwords_actions() { + + if ( isset( $_GET['action'] ) && isset( $_GET['wpga_nonce'] ) ) { + + if ( wp_verify_nonce( $_GET['wpga_nonce'], 'wpga_action' ) ) { + + switch ( $_GET['action'] ) { + case 'delete': + + if ( isset( $_GET['key'] ) ) { + $delete_key = sanitize_key( $_GET['key'] ); + wpga_delete_app_password( $delete_key ); + } + + break; + + case 'delete_all': + wpga_reset_app_passwords(); + break; + + case 'clear_log': + wpga_clear_log(); + break; + + } + + } + + wp_redirect( add_query_arg( array( 'page' => 'wpga_apps_passwords' ), admin_url( 'users.php') ) ); + exit; + + } + +} + +/** + * Create the custom database table to store recovery keys + * + * @since 1.2 + * @return void + */ +function wpga_apps_access_log_create_table() { + + global $wpdb; + + $table = wpga_apps_access_log_table; + $charset_collate = $wpdb->get_charset_collate(); + + // Prepare DB structure if not already existing + if ( $wpdb->get_var( "show tables like '$table'" ) != $table ) { + + $sql = "CREATE TABLE $table ( + ID mediumint(9) NOT NULL AUTO_INCREMENT, + user_id mediumint(9) NOT NULL, + key_id mediumint(9) NOT NULL, + time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + ip VARCHAR(255) NOT NULL, + user_agent VARCHAR(255), + method VARCHAR(20), + UNIQUE KEY ID (ID) + ) $charset_collate;"; + + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + dbDelta( $sql ); + + // Save database version. Useful for upgrades. + add_option( 'wpga_db_version', WPGA_DB_VERSION ); + + } + +} \ No newline at end of file diff --git a/includes/functions-deprecated.php b/includes/functions-deprecated.php new file mode 100644 index 0000000..fcab24a --- /dev/null +++ b/includes/functions-deprecated.php @@ -0,0 +1,140 @@ + + * @license GPL-2.0+ + * @link http://themeavenue.net + * @copyright 2016 Julien Liabeuf + */ + +/* Exit if accessed directly */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Get the number of login attempts done by a user + * + * @since 1.2.0 + * + * @param int $user_id + * + * @deprecated 1.2 + * + * @return int + */ +function wpas_get_login_attempts( $user_id ) { + + /** + * @var WPGA_User + */ + $user = new WPGA_User( $user_id ); + + return $user->login_attempts(); + +} + +/** + * Get the number of remaining login attempts for a user + * + * @since 1.2.0 + * + * @param $user_id + * + * @deprecated 1.2 + * + * @return int + */ +function wpas_get_remaining_login_attempts( $user_id ) { + + /** + * @var WPGA_User + */ + $user = new WPGA_User( $user_id ); + + return $user->remaining_attempts(); + +} + +/** + * Increment the number of login attempts done by a user + * + * @since 1.2.0 + * + * @param $user_id + * + * @deprecated 1.2 + * + * @return int + */ +function wpas_increment_attempts( $user_id ) { + + /** + * @var WPGA_User + */ + $user = new WPGA_User( $user_id ); + + return $user->add_attempt(); + +} + +/** + * Disable 2FA for a particular user + * + * @since 1.2.0 + * + * @param $user_id + * + * @deprecated 1.2 + * + * @return void + */ +function wpga_disable_2fa( $user_id ) { + + /** + * @var WPGA_User + */ + $user = new WPGA_User( $user_id ); + $user->deactivate_2fa(); + +} + +/** + * Check validity of a recovery key + * + * @since 1.0.4 + * + * @param object $user User object + * @param string $key Recovery key to check + * + * @deprecated 1.2 + * + * @return boolean Whether or not the key is valid + */ +function wpga_check_recovery_key( $user, $key ) { + + /** + * @var WPGA_User + */ + $user = new WPGA_User( $user ); + + return $user->is_recovery_key( $key ); + +} + +/** + * Generate a backup key + * + * In case the user loses his phone or cannot access the Google Authenticator app, + * we generate a unique backup key that the user can use to authenticate once. + * After one (only) authentication the key will be voided. + * + * @since 1.0.4 + * @deprecated 1.2 + * @return string Backup key + */ +function wpga_generate_backup_key() { + return WPGA()->recovery->generate_key(); +} \ No newline at end of file diff --git a/includes/functions-login.php b/includes/functions-login.php new file mode 100644 index 0000000..e49d107 --- /dev/null +++ b/includes/functions-login.php @@ -0,0 +1,36 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_action( 'login_form', 'wpga_customize_login_form' ); +/** + * Add verification code field to login form. + */ +function wpga_customize_login_form() { + + if ( ! wpga_is_2fa_active() ) { + return; + } + + ?> +

+ +

+ + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_action( 'wp_ajax_wpga_get_recovery', 'wpga_ajax_callback' ); +/** + * Get recovery code + * + * The function will check the user's password and, + * if the password is correct, it will return + * the recovery code. + * + * @return void + * @since 1.0.4 + */ +function wpga_ajax_callback() { + + if ( ! isset( $_POST['pwd'] ) ) { + return; + } + + /* Password to check */ + $pwd = sanitize_text_field( $_POST['pwd'] ); + + $user_id = get_current_user_id(); + $user = get_user_by( 'id', $user_id ); + + if ( $user && wp_check_password( $pwd, $user->data->user_pass, $user->ID ) ) { + + $recovery = get_user_meta( $user_id, 'wpga_backup_key', true ); + + if ( '' != $recovery ) { + echo "
" . esc_html( $recovery ) . "

" . esc_html_e( 'Write this down and keep it safe', 'wpga' ) . "

"; + } else { + esc_html_e( 'No recovery code set yet.', 'wpga' ); + } + + } else { + ?>recovery->get_recovery_keys( $user_id ); +} + +/** + * Create the custom database table to store recovery keys + * + * @since 1.2 + * @return void + */ +function wpga_recovery_keys_create_table() { + + global $wpdb; + + $table = wpga_recovery_keys_table; + $charset_collate = $wpdb->get_charset_collate(); + + // Prepare DB structure if not already existing + if ( $wpdb->get_var( "show tables like '$table'" ) != $table ) { + + $sql = "CREATE TABLE $table ( + ID mediumint(9) NOT NULL AUTO_INCREMENT, + user_id mediumint(9) NOT NULL, + time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + code VARCHAR(255) NOT NULL, + name VARCHAR(100), + type VARCHAR(20) NOT NULL, + count VARCHAR(20), + UNIQUE KEY ID (ID) + ) $charset_collate;"; + + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + dbDelta( $sql ); + + // Save database version. Useful for upgrades. + add_option( 'wpga_db_version', WPGA_DB_VERSION ); + + } + +} \ No newline at end of file diff --git a/includes/functions-totp.php b/includes/functions-totp.php new file mode 100644 index 0000000..2caf81b --- /dev/null +++ b/includes/functions-totp.php @@ -0,0 +1,246 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Revoke a TOTP + * + * @since 1.2.0 + * + * @param string $totp TOTP to invalidate + * + * @return bool + */ +function wpga_revoke_totp( $totp ) { + + $used = get_option( 'wpga_used_totp', array() ); + + if ( is_array( $used ) && ! in_array( md5( $totp ), $used ) ) { + + array_push( $used, md5( $totp ) ); + + update_option( 'wpga_used_totp', $used ); + + return true; + + } + + return false; + +} + +/** + * Calculate the code, with given secret and point in time + * + * @param string $secret + * @param integer $timeSlice + * + * @return string Generated code + */ +function wpga_get_code( $secret, $timeSlice = null ) { + + if ( $timeSlice === null ) { + $timeSlice = floor( time() / 30 ); + } + + $code_length = apply_filters( 'wpga_code_length', 6 ); + $secretkey = wpga_base32_decode( $secret ); + + // Pack time into binary string + $time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $timeSlice ); + + // Hash it with users secret key + $hm = hash_hmac( 'SHA1', $time, $secretkey, true ); + + // Use last nipple of result as index/offset + $offset = ord( substr( $hm, - 1 ) ) & 0x0F; + + // grab 4 bytes of the result + $hashpart = substr( $hm, $offset, 4 ); + + // Unpak binary value + $value = unpack( 'N', $hashpart ); + $value = $value[1]; + + // Only 32 bits + $value = $value & 0x7FFFFFFF; + + $modulo = pow( 10, $code_length ); + + return str_pad( $value % $modulo, $code_length, '0', STR_PAD_LEFT ); +} + +/** + * Decode base32 string + * + * @param string $string String to decode + * + * @return string Decoded string + */ +function wpga_base32_decode( $string ) { + + $lut = array( + "A" => 0, + "B" => 1, + "C" => 2, + "D" => 3, + "E" => 4, + "F" => 5, + "G" => 6, + "H" => 7, + "I" => 8, + "J" => 9, + "K" => 10, + "L" => 11, + "M" => 12, + "N" => 13, + "O" => 14, + "P" => 15, + "Q" => 16, + "R" => 17, + "S" => 18, + "T" => 19, + "U" => 20, + "V" => 21, + "W" => 22, + "X" => 23, + "Y" => 24, + "Z" => 25, + "2" => 26, + "3" => 27, + "4" => 28, + "5" => 29, + "6" => 30, + "7" => 31 + ); + + $string = strtoupper( $string ); + $l = strlen( $string ); + $n = 0; + $j = 0; + $binary = ""; + + for ( $i = 0; $i < $l; $i ++ ) { + + $n = $n << 5; + $n = $n + $lut[ $string[ $i ] ]; + $j = $j + 5; + + if ( $j >= 8 ) { + $j = $j - 8; + $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j ); + } + } + + return $binary; +} + +add_action( 'wpas_clean_totps', 'wpga_clean_totps' ); +/** + * Delete all TOTPs from DB. + * + * As TOTPs expire after a defined amount of time + * per definition, there is no need to store them + * in the database forever. + * + * @since 1.0.7 + */ +function wpga_clean_totps() { + delete_option( 'wpga_used_totp' ); +} + +/** + * Check if 2FA is enabled on this site + * + * If a user object is passed, we check if 2FA is enabled on the site and for this particular user. + * + * @since 1.2.0 + * + * @param WP_User|bool $user Object of the user who's trying to login + * + * @return bool + */ +function wpga_is_2fa_active( $user = false ) { + + $active = wpga_get_option( 'active', false ); + + if ( $active && is_array( $active ) ) { + $active = in_array( 'yes', $active ) ? true : false; + } + + // If 2FA is enabled on the site, make sure the current user, if any, has it enabled for his account + if ( true === $active && is_object( $user ) && is_a( $user, 'WP_User' ) ) { + $wpga_user = new WPGA_User( $user ); + $active = $wpga_user->has_2fa(); + } + + return $active; + +} + +/** + * Check if 2FA is being forced by the admin + * + * @since 1.2.0 + * + * @param array $roles Roles to check for forced 2FA + * + * @return bool + */ +function wpga_is_2fa_forced( $roles = array() ) { + + $options = get_option( 'wpga_options', array() ); + + /* Check if 2FA is forced by the admin */ + if ( ! isset( $options['force_2fa'] ) || ! in_array( 'yes', (array) $options['force_2fa'] ) ) { + return false; + } + + if ( 'all' === $options['user_role_status'] ) { + return true; + } + + /* If the forced roles list is empty, we consider it active for all users. Hence, we add the current user role in the list. */ + if ( ! isset( $options['user_roles'] ) || empty( $options['user_roles'] ) ) { + $options['user_roles'] = $roles; + } + + /* Check if 2FA is forced for the role this user has */ + if ( array_intersect( $roles, $options['user_roles'] ) ) { + return true; + } + + return false; + +} + +/** + * Check if a OPT has already been used + * + * @since 1.2 + * + * @param string $otp The OPT to check + * + * @return bool + */ +function wpga_was_otp_used( $otp ) { + + $used = get_option( 'wpga_used_totp', array() ); + + if ( is_array( $used ) && ! in_array( md5( $totp ), $used ) ) { + return false; + } else { + return true; + } + +} \ No newline at end of file diff --git a/includes/functions-users.php b/includes/functions-users.php new file mode 100644 index 0000000..5579bac --- /dev/null +++ b/includes/functions-users.php @@ -0,0 +1,67 @@ + + * @license GPL-2.0+ + * @link http://themeavenue.net + * @copyright 2016 Julien Liabeuf + */ +/* Exit if accessed directly */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +add_filter( 'manage_users_columns', 'wpga_add_user_2fa_column' ); +/** + * Add a new column to the users list screen for displaying the 2FA status + * + * @since 1.2.0 + * + * @param array $columns Existing columns + * + * @return array + */ +function wpga_add_user_2fa_column( $columns ) { + + $new = array(); + + foreach ( $columns as $column => $title ) { + + $new[$column] = $title; + + if ( 'role' === $column ) { + $new['2fa'] = esc_html__( '2FA', 'wpga' ); + } + + } + + return $new; +} + +add_action( 'manage_users_custom_column', 'wpga_2fa_usr_column_content', 10, 3 ); +/** + * Display the content of the 2FA column + * + * Checks if 2FA is enabled for the current user and displays a tag if it it. + * + * @since 1.2.0 + * + * @param string $value The column output + * @param string $column_name Column ID + * @param int $user_id Current user ID + * + * @return string + */ +function wpga_2fa_usr_column_content( $value, $column_name, $user_id ) { + + $user = get_user_by( 'ID', $user_id ); + $enabled = wpga_is_2fa_active( $user ); + + if ( true === $enabled ) { + $value = sprintf( '%2$s', 'wpga-tag', esc_html__( '2FA', 'wpga' ), esc_html__( '2 Factor Authentication is enabled for this user', 'wpga' ) ); + } + + return $value; +} \ No newline at end of file diff --git a/includes/scripts-styles.php b/includes/scripts-styles.php new file mode 100644 index 0000000..74c62e8 --- /dev/null +++ b/includes/scripts-styles.php @@ -0,0 +1,45 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +add_action( 'admin_print_scripts', 'wpga_load_admin_scripts' ); +/** + * Load the plugin custom JS + * + * @since 1.0.4 + * @return void + */ +function wpga_load_admin_scripts() { + + global $pagenow; + + if ( 'profile.php' === $pagenow || isset( $_GET['page'] ) && in_array( $_GET['page'], array( 'wpga_apps_passwords', 'wpga-settings' ) ) ) { + wp_enqueue_script( 'wpga-custom', WPGA_URL . 'assets/js/custom.js', array(), WPGA_VERSION, true ); + wp_enqueue_script( 'wpga-qrcode', WPGA_URL . 'assets/js/jquery-qrcode.min.js', array( 'jquery' ), '0.14.0', true ); + } +} + +add_action( 'login_enqueue_scripts', 'wpga_load_styles' ); +add_action( 'admin_enqueue_scripts', 'wpga_load_styles' ); +/** + * Load the scripts resources on the login page used for the tooltip + */ +function wpga_load_styles() { + + global $pagenow; + + if ( in_array( $pagenow, array( 'wp-login.php', 'users.php' ) ) ) { + wp_enqueue_style( 'wpga-simple-hint', WPGA_URL . 'assets/css/wpga.css', array(), null, 'all' ); + } + +} \ No newline at end of file diff --git a/languages/wpga-fi_FI.po b/languages/wpga-fi_FI.po index cfc2d49..aad3f86 100644 --- a/languages/wpga-fi_FI.po +++ b/languages/wpga-fi_FI.po @@ -304,7 +304,7 @@ msgstr "Google Authenticator" #: C:\wamp\www\plugins-dev\wp-content\plugins\wp-google-authenticator/admin/admin.class.php:957 msgid "" -"If you do not have configured the 2-factor authentication,
just leave " +"If you have not configured 2-factor authentication,
just leave " "this field blank and you will be logged-in as usual.

If you can't use " "the Google Authenticator app for whatever reason,
you can use your " "recovery code instead." diff --git a/languages/wpga-fr_FR.po b/languages/wpga-fr_FR.po index f9ccaec..4473b00 100644 --- a/languages/wpga-fr_FR.po +++ b/languages/wpga-fr_FR.po @@ -340,7 +340,7 @@ msgstr "Google Authenticator" #: ../admin/admin.class.php:1229 msgid "" -"If you do not have configured the 2-factor authentication, just leave this " +"If you have not configured 2-factor authentication, just leave this " "field blank and you will be logged-in as usual. If you can't use the Google " "Authenticator app for whatever reason, you can use your recovery code " "instead." @@ -549,7 +549,7 @@ msgstr "Nettoyer" #~ msgstr "QR Code" #~ msgid "" -#~ "If you do not have configured the 2-factor authentication,
just leave " +#~ "If you have not configured 2-factor authentication,
just leave " #~ "this field blank and you will be logged-in as usual.

If you can't " #~ "use the Google Authenticator app for whatever reason,
you can use your " #~ "recovery code instead." diff --git a/languages/wpga.pot b/languages/wpga.pot index 921a96d..8639bd6 100644 --- a/languages/wpga.pot +++ b/languages/wpga.pot @@ -296,7 +296,7 @@ msgstr "" #: ../admin/admin.class.php:1229 msgid "" -"If you do not have configured the 2-factor authentication, just leave this " +"If you have not configured 2-factor authentication, just leave this " "field blank and you will be logged-in as usual. If you can't use the Google " "Authenticator app for whatever reason, you can use your recovery code " "instead." diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..701a81d --- /dev/null +++ b/uninstall.php @@ -0,0 +1,65 @@ + + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + */ + +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} + +wpga_uninstallPlugin(); +/** + * Remove plugin data from database + */ +function wpga_uninstallPlugin() { + + /* Plugin main options */ + delete_option( WPGA_PREFIX . '_options' ); + delete_option( WPGA_PREFIX . '_used_totp' ); + + $args = array( 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'wpga_attempts', + 'value' => '', + 'compare' => '!=' + ), + array( + 'key' => 'wpga_secret', + 'value' => '', + 'compare' => '!=' + ) + ) + ); + + $users = new WP_User_Query( $args ); + + /* Delete all user metas */ + if ( ! empty( $users->results ) ) { + + foreach( $users->results as $key => $user ) { + + delete_user_meta( $user->ID, 'wpga_active' ); + delete_user_meta( $user->ID, 'wpga_attempts' ); + delete_user_meta( $user->ID, 'wpga_secret' ); + delete_user_meta( $user->ID, 'wpga_backup_key' ); + delete_user_meta( $user->ID, 'wpga_backup_key_time' ); + delete_user_meta( $user->ID, 'wpga_apps_passwords' ); + delete_user_meta( $user->ID, 'wpga_apps_passwords_log' ); + + } + + } + + /** + * Remove cron task + */ + $timestamp = wp_next_scheduled( 'wpas_clean_totps' ); + wp_unschedule_event( $timestamp, 'wpas_clean_totps' ); + +} \ No newline at end of file diff --git a/vendor/jquery.qrcode.min.js b/vendor/jquery.qrcode.min.js deleted file mode 100644 index fdc4642..0000000 --- a/vendor/jquery.qrcode.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* jQuery.qrcode 0.12.0 - http://larsjung.de/jquery-qrcode/ - uses //github.com/kazuhikoarase/qrcode-generator (MIT) */ -!function(r){"use strict";function t(t,e,n,o){function i(r,t){return r-=o,t-=o,0>r||r>=u||0>t||t>=u?!1:a.isDark(r,t)}var a=r(n,e);a.addData(t),a.make(),o=o||0;var u=a.getModuleCount(),f=a.getModuleCount()+2*o,c=function(r,t,e,n){var o=this.isDark,i=1/f;this.isDark=function(a,u){var f=u*i,c=a*i,l=f+i,g=c+i;return o(a,u)&&(r>l||f>e||t>g||c>n)}};this.text=t,this.level=e,this.version=n,this.moduleCount=f,this.isDark=i,this.addBlank=c}function e(r,e,n,o,i){n=Math.max(1,n||1),o=Math.min(40,o||40);for(var a=n;o>=a;a+=1)try{return new t(r,e,a,i)}catch(u){}}function n(r,t,e){var n=e.size,o="bold "+e.mSize*n+"px "+e.fontname,i=w("")[0].getContext("2d");i.font=o;var a=i.measureText(e.label).width,u=e.mSize,f=a/n,c=(1-f)*e.mPosX,l=(1-u)*e.mPosY,g=c+f,s=l+u,h=.01;1===e.mode?r.addBlank(0,l-h,n,s+h):r.addBlank(c-h,l-h,g+h,s+h),t.fillStyle=e.fontcolor,t.font=o,t.fillText(e.label,c*n,l*n+.75*e.mSize*n)}function o(r,t,e){var n=e.size,o=e.image.naturalWidth||1,i=e.image.naturalHeight||1,a=e.mSize,u=a*o/i,f=(1-u)*e.mPosX,c=(1-a)*e.mPosY,l=f+u,g=c+a,s=.01;3===e.mode?r.addBlank(0,c-s,n,g+s):r.addBlank(f-s,c-s,l+s,g+s),t.drawImage(e.image,f*n,c*n,u*n,a*n)}function i(r,t,e){w(e.background).is("img")?t.drawImage(e.background,0,0,e.size,e.size):e.background&&(t.fillStyle=e.background,t.fillRect(e.left,e.top,e.size,e.size));var i=e.mode;1===i||2===i?n(r,t,e):(3===i||4===i)&&o(r,t,e)}function a(r,t,e,n,o,i,a,u){r.isDark(a,u)&&t.rect(n,o,i,i)}function u(r,t,e,n,o,i,a,u,f,c){a?r.moveTo(t+i,e):r.moveTo(t,e),u?(r.lineTo(n-i,e),r.arcTo(n,e,n,o,i)):r.lineTo(n,e),f?(r.lineTo(n,o-i),r.arcTo(n,o,t,o,i)):r.lineTo(n,o),c?(r.lineTo(t+i,o),r.arcTo(t,o,t,e,i)):r.lineTo(t,o),a?(r.lineTo(t,e+i),r.arcTo(t,e,n,e,i)):r.lineTo(t,e)}function f(r,t,e,n,o,i,a,u,f,c){a&&(r.moveTo(t+i,e),r.lineTo(t,e),r.lineTo(t,e+i),r.arcTo(t,e,t+i,e,i)),u&&(r.moveTo(n-i,e),r.lineTo(n,e),r.lineTo(n,e+i),r.arcTo(n,e,n-i,e,i)),f&&(r.moveTo(n-i,o),r.lineTo(n,o),r.lineTo(n,o-i),r.arcTo(n,o,n-i,o,i)),c&&(r.moveTo(t+i,o),r.lineTo(t,o),r.lineTo(t,o-i),r.arcTo(t,o,t+i,o,i))}function c(r,t,e,n,o,i,a,c){var l=r.isDark,g=n+i,s=o+i,h=e.radius*i,v=a-1,d=a+1,w=c-1,m=c+1,p=l(a,c),y=l(v,w),T=l(v,c),B=l(v,m),A=l(a,m),E=l(d,m),k=l(d,c),M=l(d,w),C=l(a,w);p?u(t,n,o,g,s,h,!T&&!C,!T&&!A,!k&&!A,!k&&!C):f(t,n,o,g,s,h,T&&C&&y,T&&A&&B,k&&A&&E,k&&C&&M)}function l(r,t,e){var n,o,i=r.moduleCount,u=e.size/i,f=a;for(p&&e.radius>0&&e.radius<=.5&&(f=c),t.beginPath(),n=0;i>n;n+=1)for(o=0;i>o;o+=1){var l=e.left+o*u,g=e.top+n*u,s=u;f(r,t,e,l,g,s,n,o)}if(w(e.fill).is("img")){t.strokeStyle="rgba(0,0,0,0.5)",t.lineWidth=2,t.stroke();var h=t.globalCompositeOperation;t.globalCompositeOperation="destination-out",t.fill(),t.globalCompositeOperation=h,t.clip(),t.drawImage(e.fill,0,0,e.size,e.size),t.restore()}else t.fillStyle=e.fill,t.fill()}function g(r,t){var n=e(t.text,t.ecLevel,t.minVersion,t.maxVersion,t.quiet);if(!n)return null;var o=w(r).data("qrcode",n),a=o[0].getContext("2d");return i(n,a,t),l(n,a,t),o}function s(r){var t=w("").attr("width",r.size).attr("height",r.size);return g(t,r)}function h(r){return w("").attr("src",s(r)[0].toDataURL("image/png"))}function v(r){var t=e(r.text,r.ecLevel,r.minVersion,r.maxVersion,r.quiet);if(!t)return null;var n,o,i=r.size,a=r.background,u=Math.floor,f=t.moduleCount,c=u(i/f),l=u(.5*(i-c*f)),g={position:"relative",left:0,top:0,padding:0,margin:0,width:i,height:i},s={position:"absolute",padding:0,margin:0,width:c,height:c,"background-color":r.fill},h=w("
").data("qrcode",t).css(g);for(a&&h.css("background-color",a),n=0;f>n;n+=1)for(o=0;f>o;o+=1)t.isDark(n,o)&&w("
").css(s).css({left:l+o*c,top:l+n*c}).appendTo(h);return h}function d(r){return m&&"canvas"===r.render?s(r):m&&"image"===r.render?h(r):v(r)}var w=jQuery,m=function(){var r=document.createElement("canvas");return Boolean(r.getContext&&r.getContext("2d"))}(),p="[object Opera]"!==Object.prototype.toString.call(window.opera),y={render:"canvas",minVersion:1,maxVersion:40,ecLevel:"L",left:0,top:0,size:200,fill:"#000",background:null,text:"no text",radius:0,quiet:0,mode:0,mSize:.1,mPosX:.5,mPosY:.5,label:"no label",fontname:"sans",fontcolor:"#000",image:null};w.fn.qrcode=function(r){var t=w.extend({},y,r);return this.each(function(){"canvas"===this.nodeName.toLowerCase()?g(this,t):w(this).append(d(t))})}}(function(){var r=function(){function r(t,e){if("undefined"==typeof t.length)throw new Error(t.length+"/"+e);var n=function(){for(var r=0;re;e+=1){t[e]=new Array(r);for(var n=0;r>n;n+=1)t[e][n]=null}return t}(h),y(0,0),y(h-7,0),y(0,h-7),A(),B(),k(r,t),l>=7&&E(r),null==d&&(d=D(l,g,w)),M(d,t)},y=function(r,t){for(var e=-1;7>=e;e+=1)if(!(-1>=r+e||r+e>=h))for(var n=-1;7>=n;n+=1)-1>=t+n||t+n>=h||(e>=0&&6>=e&&(0==n||6==n)||n>=0&&6>=n&&(0==e||6==e)||e>=2&&4>=e&&n>=2&&4>=n?s[r+e][t+n]=!0:s[r+e][t+n]=!1)},T=function(){for(var r=0,t=0,e=0;8>e;e+=1){p(!0,e);var n=i.getLostPoint(m);(0==e||r>n)&&(r=n,t=e)}return t},B=function(){for(var r=8;h-8>r;r+=1)null==s[r][6]&&(s[r][6]=r%2==0);for(var t=8;h-8>t;t+=1)null==s[6][t]&&(s[6][t]=t%2==0)},A=function(){for(var r=i.getPatternPosition(l),t=0;t=a;a+=1)for(var u=-2;2>=u;u+=1)-2==a||2==a||-2==u||2==u||0==a&&0==u?s[n+a][o+u]=!0:s[n+a][o+u]=!1}},E=function(r){for(var t=i.getBCHTypeNumber(l),e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[Math.floor(e/3)][e%3+h-8-3]=n}for(var e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[e%3+h-8-3][Math.floor(e/3)]=n}},k=function(r,t){for(var e=g<<3|t,n=i.getBCHTypeInfo(e),o=0;15>o;o+=1){var a=!r&&1==(n>>o&1);6>o?s[o][8]=a:8>o?s[o+1][8]=a:s[h-15+o][8]=a}for(var o=0;15>o;o+=1){var a=!r&&1==(n>>o&1);8>o?s[8][h-o-1]=a:9>o?s[8][15-o-1+1]=a:s[8][15-o-1]=a}s[h-8][8]=!r},M=function(r,t){for(var e=-1,n=h-1,o=7,a=0,u=i.getMaskFunction(t),f=h-1;f>0;f-=2)for(6==f&&(f-=1);;){for(var c=0;2>c;c+=1)if(null==s[n][f-c]){var l=!1;a>>o&1));var g=u(n,f-c);g&&(l=!l),s[n][f-c]=l,o-=1,-1==o&&(a+=1,o=7)}if(n+=e,0>n||n>=h){n-=e,e=-e;break}}},C=function(t,e){for(var n=0,o=0,a=0,u=new Array(e.length),f=new Array(e.length),c=0;c=0?d.getAt(w):0}}for(var m=0,s=0;ss;s+=1)for(var c=0;cs;s+=1)for(var c=0;c8*s)throw new Error("code length overflow. ("+c.getLengthInBits()+">"+8*s+")");for(c.getLengthInBits()+4<=8*s&&c.put(0,4);c.getLengthInBits()%8!=0;)c.putBit(!1);for(;;){if(c.getLengthInBits()>=8*s)break;if(c.put(o,8),c.getLengthInBits()>=8*s)break;c.put(a,8)}return C(c,n)};return m.addData=function(r){var t=c(r);w.push(t),d=null},m.isDark=function(r,t){if(0>r||r>=h||0>t||t>=h)throw new Error(r+","+t);return s[r][t]},m.getModuleCount=function(){return h},m.make=function(){p(!1,T())},m.createTableTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e="";e+='";for(var o=0;o';e+=""}return e+="",e+="
"},m.createImgTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e=m.getModuleCount()*r+2*t,n=t,o=e-t;return v(e,e,function(t,e){if(t>=n&&o>t&&e>=n&&o>e){var i=Math.floor((t-n)/r),a=Math.floor((e-n)/r);return m.isDark(a,i)?0:1}return 1})},m};t.stringToBytes=function(r){for(var t=new Array,e=0;ei)t.push(i);else{var a=e[r.charAt(o)];"number"==typeof a?(255&a)==a?t.push(a):(t.push(a>>>8),t.push(255&a)):t.push(n)}}return t}};var e={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},n={L:1,M:0,Q:3,H:2},o={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},i=function(){var t=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],n=1335,i=7973,u=21522,f={},c=function(r){for(var t=0;0!=r;)t+=1,r>>>=1;return t};return f.getBCHTypeInfo=function(r){for(var t=r<<10;c(t)-c(n)>=0;)t^=n<=0;)t^=i<n;n+=1)e=e.multiply(r([1,a.gexp(n)],0));return e},f.getLengthInBits=function(r,t){if(t>=1&&10>t)switch(r){case e.MODE_NUMBER:return 10;case e.MODE_ALPHA_NUM:return 9;case e.MODE_8BIT_BYTE:return 8;case e.MODE_KANJI:return 8;default:throw new Error("mode:"+r)}else if(27>t)switch(r){case e.MODE_NUMBER:return 12;case e.MODE_ALPHA_NUM:return 11;case e.MODE_8BIT_BYTE:return 16;case e.MODE_KANJI:return 10;default:throw new Error("mode:"+r)}else{if(!(41>t))throw new Error("type:"+t);switch(r){case e.MODE_NUMBER:return 14;case e.MODE_ALPHA_NUM:return 13;case e.MODE_8BIT_BYTE:return 16;case e.MODE_KANJI:return 12;default:throw new Error("mode:"+r)}}},f.getLostPoint=function(r){for(var t=r.getModuleCount(),e=0,n=0;t>n;n+=1)for(var o=0;t>o;o+=1){for(var i=0,a=r.isDark(n,o),u=-1;1>=u;u+=1)if(!(0>n+u||n+u>=t))for(var f=-1;1>=f;f+=1)0>o+f||o+f>=t||(0!=u||0!=f)&&a==r.isDark(n+u,o+f)&&(i+=1);i>5&&(e+=3+i-5)}for(var n=0;t-1>n;n+=1)for(var o=0;t-1>o;o+=1){var c=0;r.isDark(n,o)&&(c+=1),r.isDark(n+1,o)&&(c+=1),r.isDark(n,o+1)&&(c+=1),r.isDark(n+1,o+1)&&(c+=1),(0==c||4==c)&&(e+=3)}for(var n=0;t>n;n+=1)for(var o=0;t-6>o;o+=1)r.isDark(n,o)&&!r.isDark(n,o+1)&&r.isDark(n,o+2)&&r.isDark(n,o+3)&&r.isDark(n,o+4)&&!r.isDark(n,o+5)&&r.isDark(n,o+6)&&(e+=40);for(var o=0;t>o;o+=1)for(var n=0;t-6>n;n+=1)r.isDark(n,o)&&!r.isDark(n+1,o)&&r.isDark(n+2,o)&&r.isDark(n+3,o)&&r.isDark(n+4,o)&&!r.isDark(n+5,o)&&r.isDark(n+6,o)&&(e+=40);for(var l=0,o=0;t>o;o+=1)for(var n=0;t>n;n+=1)r.isDark(n,o)&&(l+=1);var g=Math.abs(100*l/t/t-50)/5;return e+=10*g},f}(),a=function(){for(var r=new Array(256),t=new Array(256),e=0;8>e;e+=1)r[e]=1<e;e+=1)r[e]=r[e-4]^r[e-5]^r[e-6]^r[e-8];for(var e=0;255>e;e+=1)t[r[e]]=e;var n={};return n.glog=function(r){if(1>r)throw new Error("glog("+r+")");return t[r]},n.gexp=function(t){for(;0>t;)t+=255;for(;t>=256;)t-=255;return r[t]},n}(),u=function(){var r=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12,7,37,13],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],t=function(r,t){var e={};return e.totalCount=r,e.dataCount=t,e},e={},o=function(t,e){switch(e){case n.L:return r[4*(t-1)+0];case n.M:return r[4*(t-1)+1];case n.Q:return r[4*(t-1)+2];case n.H:return r[4*(t-1)+3];default:return void 0}};return e.getRSBlocks=function(r,e){var n=o(r,e);if("undefined"==typeof n)throw new Error("bad rs block @ typeNumber:"+r+"/errorCorrectLevel:"+e);for(var i=n.length/3,a=new Array,u=0;i>u;u+=1)for(var f=n[3*u+0],c=n[3*u+1],l=n[3*u+2],g=0;f>g;g+=1)a.push(t(c,l));return a},e}(),f=function(){var r=new Array,t=0,e={};return e.getBuffer=function(){return r},e.getAt=function(t){var e=Math.floor(t/8);return 1==(r[e]>>>7-t%8&1)},e.put=function(r,t){for(var n=0;t>n;n+=1)e.putBit(1==(r>>>t-n-1&1))},e.getLengthInBits=function(){return t},e.putBit=function(e){var n=Math.floor(t/8);r.length<=n&&r.push(0),e&&(r[n]|=128>>>t%8),t+=1},e},c=function(r){var n=e.MODE_8BIT_BYTE,o=t.stringToBytes(r),i={};return i.getMode=function(){return n},i.getLength=function(r){return o.length},i.write=function(r){for(var t=0;t>>8)},t.writeBytes=function(r,e,n){e=e||0,n=n||r.length;for(var o=0;n>o;o+=1)t.writeByte(r[o+e])},t.writeString=function(r){for(var e=0;e0&&(t+=","),t+=r[e];return t+="]"},t},g=function(){var r=0,t=0,e=0,n="",o={},i=function(r){n+=String.fromCharCode(a(63&r))},a=function(r){if(0>r);else{if(26>r)return 65+r;if(52>r)return 97+(r-26);if(62>r)return 48+(r-52);if(62==r)return 43;if(63==r)return 47}throw new Error("n:"+r)};return o.writeByte=function(n){for(r=r<<8|255&n,t+=8,e+=1;t>=6;)i(r>>>t-6),t-=6},o.flush=function(){if(t>0&&(i(r<<6-t),r=0,t=0),e%3!=0)for(var o=3-e%3,a=0;o>a;a+=1)n+="="},o.toString=function(){return n},o},s=function(r){var t=r,e=0,n=0,o=0,i={};i.read=function(){for(;8>o;){if(e>=t.length){if(0==o)return-1;throw new Error("unexpected end of file./"+o)}var r=t.charAt(e);if(e+=1,"="==r)return o=0,-1;r.match(/^\s$/)||(n=n<<6|a(r.charCodeAt(0)),o+=6)}var i=n>>>o-8&255;return o-=8,i};var a=function(r){if(r>=65&&90>=r)return r-65;if(r>=97&&122>=r)return r-97+26;if(r>=48&&57>=r)return r-48+52;if(43==r)return 62;if(47==r)return 63;throw new Error("c:"+r)};return i},h=function(r,t){var e=r,n=t,o=new Array(r*t),i={};i.setPixel=function(r,t,n){o[t*e+r]=n},i.write=function(r){r.writeString("GIF87a"),r.writeShort(e),r.writeShort(n),r.writeByte(128),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(0),r.writeByte(255),r.writeByte(255),r.writeByte(255),r.writeString(","),r.writeShort(0),r.writeShort(0),r.writeShort(e),r.writeShort(n),r.writeByte(0);var t=2,o=u(t);r.writeByte(t);for(var i=0;o.length-i>255;)r.writeByte(255),r.writeBytes(o,i,255),i+=255;r.writeByte(o.length-i),r.writeBytes(o,i,o.length-i),r.writeByte(0),r.writeString(";")};var a=function(r){var t=r,e=0,n=0,o={};return o.write=function(r,o){if(r>>>o!=0)throw new Error("length over");for(;e+o>=8;)t.writeByte(255&(r<>>=8-e,n=0,e=0;n=r<0&&t.writeByte(n)},o},u=function(r){for(var t=1<u;u+=1)i.add(String.fromCharCode(u));i.add(String.fromCharCode(t)),i.add(String.fromCharCode(e));var c=l(),g=a(c);g.write(t,n);var s=0,h=String.fromCharCode(o[s]);for(s+=1;si;i+=1)for(var a=0;r>a;a+=1)o.setPixel(a,i,e(a,i));var u=l();o.write(u);for(var f=g(),c=u.toByteArray(),s=0;sn?t.push(n):2048>n?t.push(192|n>>6,128|63&n):55296>n||n>=57344?t.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&r.charCodeAt(e)),t.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return t}return t(r)}}(r),r}()); \ No newline at end of file diff --git a/wp-google-authenticator.php b/wp-google-authenticator.php index 70bbcb8..388951e 100644 --- a/wp-google-authenticator.php +++ b/wp-google-authenticator.php @@ -1,101 +1,445 @@ + * @license GPL-2.0+ + * @link https://julienliabeuf.com + * @copyright 2016 Julien Liabeuf + * + * @wordpress-plugin + * Plugin Name: WP Authenticator + * Plugin URI: https://wordpress.org/plugins/wp-google-authenticator/ + * Description: WP Authenticator provides a safe way to add 2-factor authentication to your WordPress site using the Google 2FA system with the Google Authenticator app. + * Version: 1.1.1 + * Author: Julien Liabeuf + * Author URI: https://julienliabeuf.com + * Text Domain: wpga + * License: GPL-2.0+ + * License URI: http://www.gnu.org/licenses/gpl-2.0.txt + * Domain Path: /languages */ -/* Define all the plugin constants */ -define( 'WPGA_VERSION', '1.1.0' ); -define( 'WPGA_NAME', 'WP Google Authenticator' ); -define( 'WPGA_AUTHOR', 'Julien Liabeuf' ); -define( 'WPGA_URI', 'http://julienliabeuf.com' ); -define( 'WPGA_URL', plugin_dir_url( __FILE__ ) ); -define( 'WPGA_PATH', plugin_dir_path( __FILE__ ) ); -define( 'WPGA_PREFIX', 'wpga' ); -define( 'WPGA_BASENAME', plugin_basename(__FILE__) ); -define( 'WPGA_LOG', false ); -define( 'TAV_SHORTNAME', 'tav' ); - -require( WPGA_PATH . 'admin/admin.class.php' ); -require( WPGA_PATH . 'admin/settings.class.php' ); -require( WPGA_PATH . 'admin/functions-apps-passwords.php' ); -add_action( 'plugins_loaded', array( 'WPGA_Admin', 'get_instance' ) ); - -register_activation_hook( __FILE__, 'wpga_installPlugin' ); -/** - * Register settings on plugin activation - */ -function wpga_installPlugin() { +// If this file is called directly, abort. +if ( ! defined( 'WPINC' ) ) { + die; +} - $defaults = array( - 'blog_name' => get_bloginfo( 'name' ), - 'max_attempts' => 3, - 'authorized_delay' => 0, - ); +if ( ! class_exists( 'WP_Google_Authenticator' ) ): - if ( ! get_option( WPGA_PREFIX . '_options' ) ) - update_option( WPGA_PREFIX . '_options', $defaults ); + /** + * Main WP Google Authenticator class + * + * This class is the one and only instance of the plugin. It is used + * to load the core and all its components. + * + * @since 1.2.0 + */ + final class WP_Google_Authenticator { - /* Add a new cron hook */ - if ( ! wp_next_scheduled( 'wpas_clean_totps' ) ) { - wp_schedule_event( time(), 'daily', 'wpas_clean_totps' ); - } + /** + * @var WP_Google_Authenticator Holds the unique instance of WP Google Authenticator + * @since 1.2.0 + */ + private static $instance; -} + /** + * Possible error message. + * + * @since 1.2.0 + * @var null|WP_Error + */ + protected $error = null; -register_uninstall_hook( __FILE__, 'wpga_uninstallPlugin' ); -/** - * Remove plugin data from database - */ -function wpga_uninstallPlugin() { - - /* Plugin main options */ - delete_option( WPGA_PREFIX . '_options' ); - delete_option( WPGA_PREFIX . '_used_totp' ); - - $args = array( 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => 'wpga_attempts', - 'value' => '', - 'compare' => '!=' - ), - array( - 'key' => 'wpga_secret', - 'value' => '', - 'compare' => '!=' - ) - ) - ); - - $users = new WP_User_Query( $args ); - - /* Delete all user metas */ - if ( ! empty( $users->results ) ) { - - foreach( $users->results as $key => $user ) { - - delete_user_meta( $user->ID, 'wpga_active' ); - delete_user_meta( $user->ID, 'wpga_attempts' ); - delete_user_meta( $user->ID, 'wpga_secret' ); - delete_user_meta( $user->ID, 'wpga_backup_key' ); - delete_user_meta( $user->ID, 'wpga_backup_key_time' ); - delete_user_meta( $user->ID, 'wpga_apps_passwords' ); - delete_user_meta( $user->ID, 'wpga_apps_passwords_log' ); + /** + * Minimum version of WordPress required ot run the plugin + * + * @since 1.2.0 + * @var string + */ + public $wordpress_version_required = '3.8'; + + /** + * Required version of PHP. + * + * Follow WordPress latest requirements and require + * PHP version 5.2 at least. + * + * @since 1.2.0 + * @var string + */ + public $php_version_required = '5.2'; + + /** + * Holds the instance of our authentication class + * + * @since 1.2.0 + * @var WPGA_Authenticate + */ + public $authenticate; + + /** + * Holds the settings class + * + * @since 1.2 + * @var WPGA_Settings + */ + public $settings; + + /** + * Holds the recovery key instance + * + * @since 1.2 + * @var WPGA_Recovery_Key + */ + public $recovery; + + /** + * Holds the access log class instance + * + * @since 1.2 + * @var WPGA_Access_Log + */ + public $access_log; + + /** + * Instantiate and return the unique WP Google Authenticator object + * + * @since 1.2.0 + * @return object WP_Google_Authenticator Unique instance of WP Google Authenticator + */ + public static function instance() { + + if ( ! isset( self::$instance ) && ! ( self::$instance instanceof WP_Google_Authenticator ) ) { + self::$instance = new WP_Google_Authenticator; + self::$instance->init(); + } + + return self::$instance; + + } + + /** + * Instantiate the plugin + * + * @since 1.2.0 + * @return void + */ + private function init() { + + // First of all we need the constants + self::$instance->setup_constants(); + + // Make sure the WordPress version is recent enough + if ( ! self::$instance->is_version_compatible() ) { + self::$instance->add_error( sprintf( __( 'WP Google Authenticator requires WordPress version %s or above. Please update WordPress to run this plugin.', 'wpga' ), self::$instance->wordpress_version_required ) ); + } + + // Make sure we have a version of PHP that's not too old + if ( ! self::$instance->is_php_version_enough() ) { + self::$instance->add_error( sprintf( __( 'WP Google Authenticator requires PHP version %s or above. Read more information about how you can update.', 'wpga' ), self::$instance->wordpress_version_required, 'a href="http://www.wpupdatephp.com/update/" target="_blank"' ) ); + } + + // If we have any error, don't load the plugin + if ( is_a( self::$instance->error, 'WP_Error' ) ) { + add_action( 'admin_notices', array( self::$instance, 'display_error' ), 10, 0 ); + return; + } + + self::$instance->setup_database_constants(); + self::$instance->includes(); + self::$instance->authenticate = new WPGA_Authenticate(); + self::$instance->recovery = new WPGA_Recovery_Key(); + self::$instance->access_log = new WPGA_Access_Log(); + + if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) { + self::$instance->settings = new WPGA_Settings( 'network' ); + } + + add_action( 'plugins_loaded', array( self::$instance, 'load_plugin_textdomain' ) ); + + // Check for network activation + add_action( 'admin_notices', array( self::$instance, 'multisite_check' ), 5 ); + + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @since 1.2.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden + _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'wpga' ), '1.2.0' ); + } + + /** + * Disable unserializing of the class + * + * @since 1.2.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden + _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'wpga' ), '1.2.0' ); + } + + /** + * Setup all plugin constants + * + * @since 1.2.0 + * @return void + */ + private function setup_constants() { + define( 'WPGA_VERSION', '1.1.1' ); + define( 'WPGA_DB_VERSION', '1' ); + define( 'WPGA_NAME', 'WP Authenticator' ); + define( 'WPGA_AUTHOR', 'Julien Liabeuf' ); + define( 'WPGA_URI', 'https://julienliabeuf.com' ); + define( 'WPGA_URL', plugin_dir_url( __FILE__ ) ); + define( 'WPGA_PATH', plugin_dir_path( __FILE__ ) ); + define( 'WPGA_ROOT', trailingslashit( dirname( plugin_basename( __FILE__ ) ) ) ); + define( 'WPGA_PREFIX', 'wpga' ); + define( 'WPGA_BASENAME', plugin_basename( __FILE__ ) ); + define( 'WPGA_LOG', false ); + } + + /** + * Setup the custom database table constants + * + * @since 2.0 + * @return void + */ + private function setup_database_constants() { + + global $wpdb; + + define( 'wpga_recovery_keys_table_name', 'wpga_recovery_keys' ); + define( 'wpga_apps_access_log_table_name', 'wpga_apps_access_log' ); + define( 'wpga_recovery_keys_table', $wpdb->prefix . wpga_recovery_keys_table_name ); + define( 'wpga_apps_access_log_table', $wpdb->prefix . wpga_apps_access_log_table_name ); + + } + + /** + * Check if the core version is compatible with this plugin. + * + * @since 1.2.0 + * @return boolean + */ + private function is_version_compatible() { + + if ( empty( self::$instance->wordpress_version_required ) ) { + return true; + } + + if ( version_compare( get_bloginfo( 'version' ), self::$instance->wordpress_version_required, '<' ) ) { + return false; + } + + return true; + + } + + /** + * Check if the version of PHP is compatible with this plugin. + * + * @since 1.2.0 + * @return boolean + */ + private function is_php_version_enough() { + + /** + * No version set, we assume everything is fine. + */ + if ( empty( self::$instance->php_version_required ) ) { + return true; + } + + if ( version_compare( phpversion(), self::$instance->php_version_required, '<' ) ) { + return false; + } + + return true; + + } + + /** + * Check if the site is a multisite network and, if so, if the plugin is network activated or not + * + * If the plugin is not network activated, we display a warning message highlighting the security issue related to having it active on a per-site basis. + * + * @since 1.2 + * @return void + */ + public function multisite_check() { + if ( true === is_multisite() && false === self::$instance->is_network_enabled() ) { + wpga_register_notice( 'wpga_not_network_activated', 'error', sprintf( __( '%2$s is only active on the current site of your network. This introduces a security risk. It is strongly advised that you network-activate the plugin for maximum security. Read more about this.', 'wpga' ), '#', WPGA_NAME ) ); + } + } + + /** + * Check if the plugin is network-enabled + * + * @since 1.2 + * @return bool + */ + public function is_network_enabled() { + + if ( false === is_multisite() ) { + return false; + } + + if ( ! function_exists( 'is_plugin_active_for_network' ) ) { + require_once( ABSPATH . '/wp-admin/includes/plugin.php' ); + } + + return is_plugin_active_for_network( WPGA_BASENAME ); + + } + + /** + * Add error. + * + * Add a new error to the WP_Error object + * and create the object if it doesn't exist yet. + * + * @since 1.2.0 + * + * @param string $message Error message to add + * + * @return void + */ + private function add_error( $message ) { + + if ( ! is_object( $this->error ) || ! is_a( $this->error, 'WP_Error' ) ) { + $this->error = new WP_Error(); + } + + $this->error->add( 'addon_error', $message ); + + } + + /** + * Display error. + * + * Get all the error messages and display them + * in the admin notices. + * + * @since 1.2.0 + * @return void + */ + public function display_error() { + + if ( ! is_a( $this->error, 'WP_Error' ) ) { + return; + } + + $message = self::$instance->error->get_error_messages(); ?> + +
+

+ 1 ) { + echo '

    '; + foreach ( $message as $msg ) { + echo "
  • $msg
  • "; + } + echo ''; + } else { + echo $message[0]; + } + ?> +

    +
+