diff --git a/10up-experience.php b/10up-experience.php index 0fea0e2..c514101 100644 --- a/10up-experience.php +++ b/10up-experience.php @@ -81,6 +81,13 @@ function( $plugin_info, $http_response = null ) { SupportMonitor\Monitor::instance()->setup(); SupportMonitor\Debug::instance()->setup(); Notifications\Welcome::instance()->setup(); +Notifications\Welcome::instance()->setup(); + +if( ( ! defined( 'TENUP_DISABLE_PASSWORD_POLICY' ) || ! TENUP_DISABLE_PASSWORD_POLICY ) ){ + if ( AdminCustomizations\PasswordPolicy::instance()->is_enabled() ) { + Authentication\PastPasswords::instance()->setup(); + } +} /** * We load this later to make sure there are no conflicts with other plugins. @@ -92,6 +99,16 @@ function() { } ); +/** + * Clean up when plugin is deactivated + */ +register_deactivation_hook( + __FILE__, + function() { + Authentication\PastPasswords::instance()->deactivate(); + } +); + /** * Disable plugin/theme editor */ diff --git a/README.md b/README.md index 70f1c87..10da44f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ *Configured in `Settings > General` or `Settings > Network Settings` if network activated.* +* __Password Policy__ + + Enable site wide Password Policy for administrator, editors and authors. Control how long passwords are good for, prevent users from repeating passwords and configure when to send reminder emails to users whose password will be expiring soon. The reminder email can be fully customized and dynamic information can be added via a set of replacement tags. Once a password is expired the user will be prompted to reset their password before they can login. + + *Configured in `Users > Password Policy` + + *Note:* Password Policy can be disabled by defining the constant `TENUP_DISABLE_PASSWORD_POLICY` as `true`. + * __Headers__ `X-Frame-Origins` is set to `sameorigin` to prevent click jacking. diff --git a/includes/classes/AdminCustomizations/PasswordPolicy.php b/includes/classes/AdminCustomizations/PasswordPolicy.php new file mode 100644 index 0000000..7c66723 --- /dev/null +++ b/includes/classes/AdminCustomizations/PasswordPolicy.php @@ -0,0 +1,213 @@ + [ $this, 'sanitize_settings' ], + ] + ); + + add_settings_section( + self::PASSWORD_POLICY_OPTION_NAME, + '', + '__return_empty_string', + self::PASSWORD_POLICY_OPTION_NAME + ); + + $settings = [ + 'enabled' => [ + 'label' => __( 'Enable Password Policy', 'tenup' ), + 'type' => 'checkbox', + ], + 'expires' => [ + 'label' => __( 'Password Expires', 'tenup' ), + 'type' => 'number', + 'description' => __( 'The number of days a passwords is good for before it needs to be changed.', 'tenup' ), + ], + 'reminder' => [ + 'label' => __( 'Send Password Reminder', 'tenup' ), + 'type' => 'number', + 'description' => __( 'The number of days before a password expires to send an email reminder.', 'tenup' ), + ], + 'past_passwords' => [ + 'label' => __( 'Past Passwords', 'tenup' ), + 'type' => 'number', + 'description' => __( 'The number of past passwords a user can\'t repeat.', 'tenup' ), + ], + 'reminder_email' => [ + 'label' => __( 'Reminder Email', 'tenup' ), + 'description' => __( '###USERNAME###, ###ADMIN_EMAIL###, ###EMAIL###, ###SITENAME###, ###SITEURL###, ###DAYSLEFT###, and ###EXPIRATIONDATE### are replacement tags that can be used to populate the reminder email with dynamic information.', 'tenup' ), + 'type' => 'tinymce', + ], + ]; + + foreach ( $settings as $setting_id => $setting ) { + $options = [ + 'name' => self::PASSWORD_POLICY_OPTION_NAME . "[$setting_id]", + 'id' => $setting_id, + 'type' => $setting['type'] ?? 'text', + 'description' => $setting['description'] ?? '', + ]; + + add_settings_field( + $setting_id, + $setting['label'], + [ $this, 'field' ], + self::PASSWORD_POLICY_OPTION_NAME, + self::PASSWORD_POLICY_OPTION_NAME, + $options + ); + } + } + + /** + * Output setting fields + * + * @param array $args field options + */ + public function field( $args ) { + $settings = get_option( self::PASSWORD_POLICY_OPTION_NAME, [] ); + $value = $settings[ $args['id'] ] ?? ''; + + if ( 'checkbox' === $args['type'] ) { + printf( '', esc_attr( $args['type'] ), esc_attr( $args['id'] ), esc_attr( $args['name'] ), esc_attr( checked( 'on', $value, false ) ) ); + if ( ! empty( $args['description'] ) ) { + printf( '', esc_attr( $args['id'] ), esc_html( $args['description'] ) ); + } + } elseif ( 'tinymce' === $args['type'] ) { + wp_editor( + $value, + $args['id'], + [ + 'media_buttons' => false, + 'textarea_name' => $args['name'], + ] + ); + if ( ! empty( $args['description'] ) ) { + printf( '
%s
', wp_kses_post( $args['description'] ) ); + } + } else { + printf( '', esc_attr( $args['type'] ), esc_attr( $args['id'] ), esc_attr( $args['name'] ), esc_attr( $value ) ); + + if ( ! empty( $args['description'] ) ) { + printf( '%s
', esc_html( $args['description'] ) ); + } + } + } + + /** + * Sanitize settings fields + * + * @param array $settings setting being saved + * + * @return array + */ + public function sanitize_settings( $settings ) { + $clean_settings = array(); + foreach ( $settings as $key => $setting ) { + if ( in_array( $key, [ 'reminder_email', 'token_email' ], true ) ) { + $clean_settings[ $key ] = wp_kses_post( $setting ); + } else { + $clean_settings[ $key ] = sanitize_text_field( $setting ); + } + } + + return $clean_settings; + } + + /** + * Password policy screen + * + * @return void + */ + public function password_policy_screen() { + ?> +%s
', esc_html__( 'To reset your password, visit the following address:', 'tenup' ) ); + // translators: %1$s is the URL to the reset password screen + $message .= sprintf( '', esc_url( wp_lostpassword_url() ) ); + // translators: %1$s the site url and %2$d Numbers of days a uses password is still good for. + $subject = sprintf( _n( '[%1$s] Password expires in %2$d day', '[%1$s] Password expires in %2$d days', $reminder_days, 'tenup' ), $blog_name, number_format_i18n( $reminder_days ) ); + + foreach ( $users->get_results() as $user ) { + $custom_message = $message; + $custom_message = str_replace( '###USERNAME###', $user->user_login, $custom_message ); + $custom_message = str_replace( '###ADMIN_EMAIL###', get_option( 'admin_email' ), $custom_message ); + $custom_message = str_replace( '###EMAIL###', $user->user_email, $custom_message ); + $custom_message = str_replace( '###SITENAME###', $blog_name, $custom_message ); + $custom_message = str_replace( '###SITEURL###', home_url(), $custom_message ); + $custom_message = str_replace( '###DAYSLEFT###', $reminder_days, $custom_message ); + $custom_message = str_replace( '###EXPIRATIONDATE###', $expiration_text, $custom_message ); + + wp_mail( $user->user_email, $subject, $custom_message, array( 'Content-Type: text/html; charset=UTF-8' ) ); + } + } + } + + /** + * List of roles that qualify for password policy + * + * @return array + */ + private function get_password_expire_roles() { + return apply_filters( 'tenup_password_expire_roles', array( 'administrator', 'editor', 'author' ) ); + } + + /** + * Get date for todays expired passwords + * + * @return string + */ + private function get_password_expired_date() { + $today = current_datetime(); + $days_password_is_good_for = (int) PasswordPolicy::instance()->get_setting( 'expires' ); + return $today->modify( "-$days_password_is_good_for day" )->format( 'Y-m-d' ); + } + + /** + * Get date for todays passwords reminders + * + * @return string + */ + private function get_password_reminder_date() { + $today = current_datetime(); + $reminder_days = (int) PasswordPolicy::instance()->get_setting( 'reminder' ); + $days_password_is_good_for = (int) PasswordPolicy::instance()->get_setting( 'expires' ); + $days_till_reminder = $days_password_is_good_for - $reminder_days; + return $today->modify( "-$days_till_reminder day" )->format( 'Y-m-d' ); + } + + /** + * Return the current password for the user from the Database + * + * @param object $user User object for user being edited + * + * @return mixed + */ + private function get_current_password( $user ) { + $user_data = get_user_by( 'id', $user->ID ); + + return $user_data->user_pass; + } +}