<?php
namespace describr;

/**
 * Password class
 *
 * @package Describr
 * @since 3.0
 */
class Password {
    /**
     * Whether password is required
     * during registration 
     * 
     * @since 3.0
     */
    private $is_password_required = false;

	/**
	 * Password constructor
     * 
     * @since 3.0
	 */
	public function __construct() {
        add_action( 'wp_loaded', array( $this, 'init' ), 10 );
	}

    /**
     * Adds {@see 'login_init'} action that adds the 
     * {@see 'login_form_register'} action to validate registration password 
     * 
     * @since 3.0
     */
    public function init() {
        if ( ! is_multisite() && ! is_user_logged_in() ) {
            add_action( 'login_init', array( $this, 'login_init' ), 10 );
        }
        
        add_action( 'after_password_reset', array( __CLASS__, 'update_status' ), 10, 1 );
        add_action( 'login_init', array( $this, 'rp_init' ), 10 );
    }

    /**
     * Adds {@see 'login_form_register'} action that adds filters 
     * to validate registration password 
     * 
     * @since 3.0
     */
    public function login_init() {
        add_action( 'login_form_register', array( $this, 'password_init' ) );
    }
    
    /**
     * Adds {@see 'registration_errors'} and {@see 'registration_errors'} fitlers 
     * to validate registration password 
     * 
     * @since 3.0
     */
    public function password_init() {
        if ( get_option( 'describr_registration_password_required' ) ) {
            $this->is_password_required = true;
            add_filter( 'registration_errors', array( $this, 'validate_registration_password' ), 10, 3 );
            add_filter( 'random_password', array( $this, 'set_user_generated_password' ), 10, 1 );
        }
    }  

   /**
    * Retrieves the user-generated password
    * 
    * The password would have been validated in the {@see 'registration_errors'} filter that is applied
    * before the password is generated
    * 
    * @since 3.0
    * 
    * @static int $password_count The number of times the {@see 'random_password'} filter
    *                             is applied during script execution. The user-generated
    *                             password is returned the first time  
    * 
    * @param string $password The password generated by WordPress
    * @return string the user-generated password, otherwise the passed argument
    */
    public function set_user_generated_password( $password ) {
        static $is_user_pass_set = false;

        if ( ! $is_user_pass_set && isset( $_POST['user_pass'] ) ) {
            $password = trim( $_POST['user_pass'] );//Hashed in `wp_insert_user()`
            $is_user_pass_set = true;
        }

        return $password;
    }

    /**
     * Validates user-generated password submitted during the registration process
     * 
     * @since 3.0
     * 
     * @param WP_Error $errors     WP_Error object
     * @param string   $user_login User's username
     * @param string   $user_email User's email
     * @return WP_Error object
     */
    public function validate_registration_password( $errors, $user_login, $user_email ) {
        if ( isset( $_POST['user_pass'] ) ) {
            $user_pass = trim( $_POST['user_pass'] );
        }
        
        if ( empty( $user_pass ) ) {
            $errors->add( 'user_pass', __( '<strong>Error:</strong> Password is required.', 'describr' ) );
            return $errors;
        }

        //Check for "\" in password
        if ( str_contains( wp_unslash( $user_pass ), '\\' ) ) {
            $errors->add( 'user_pass', __( '<strong>Error:</strong> Password cannot contain the character &#34;&#92;&#34;.', 'describr' ) );
            return $errors;
        }
        
        $settings = describr_get_field( 'user_pass' );

        if ( ! empty( $settings['confirm'] ) ) {
            if ( isset( $_POST['confirm_user_pass'] ) ) {
                $confirm_user_pass = trim( $_POST['confirm_user_pass'] );
            }

            if ( empty( $confirm_user_pass ) ) {
                $errors->add( 'user_pass', __( '<strong>Error:</strong> Password confirmation is required.', 'describr' ) );
                return $errors;
            }

            if ( $confirm_user_pass !== $user_pass ) {
                $errors->add( 'user_pass', __( '<strong>Error:</strong> Passwords do not match.', 'describr' ) );
                return $errors;
            }
        }

        //According to `wp_check_password()`, passwords having more than 4096 characters are not supported.
        if ( 4096 < strlen( $user_pass ) ) {
            $errors->add( 'user_pass', __( '<strong>Error:</strong> Your password cannot contain more than 4096 characters.', 'describr' ) );
            return $errors;
        }

        if ( ! empty( $settings['strong'] ) ) {
            $user = new \WP_User();
            $user->user_login = $user_login;
            $user->user_email = $user_email;
            $user->pass = $user_pass;
            
            $this->strong_pass( $errors, $user );
        }
        
        return $errors;
    }
    
    /**
     * Adds the {@see 'login_form_resetpass'} and {@see 'login_form_rp'} actions that
     * check if strong password should be enforced  
     * 
     * @since 3.0
     */
    public function rp_init() {        
        add_action( 'login_form_resetpass', array( $this, 'validate_password_reset_init' ), 10 );
        add_action( 'login_form_rp', array( $this, 'validate_password_reset_init' ), 10 );
    }
    
    /**
     * Adds the {@see 'validate_password_reset'} action that validates for
     * strong password during password reset  
     * 
     * @since 3.0
     */
    public function validate_password_reset_init() {
        $settings = describr_get_field( 'user_pass' );
        
        if ( ! empty( $settings['strong'] ) ) {
            add_action( 'validate_password_reset', array( $this, 'validate_password_reset_strong_password' ), 10, 2 );
            add_filter( 'password_hint', array( $this, 'hint_filter' ), 10, 1 );

            if ( ! empty( self::hint() ) ) {
                $style = '.indicator-hint code {
                            font-family: Consolas, Monaco, monospace;
                            padding: 3px 5px 2px;
                            margin: 0 1px;
                            background: rgba(0, 0, 0, .07);
                            font-size: .9em;
                        }';
                $handle = 'describr-strong-pwd-hint';
                wp_register_style( $handle, false );
                wp_add_inline_style( $handle, $style );
                wp_enqueue_style( $handle );
            }
        }
    }
    
    /**
     * Validates for strong password during password reset
     *
     * @since 3.0
     *
     * @param WP_Error         $errors WP Error object
     * @param WP_User|WP_Error $user   WP_User object if the login and reset key match. WP_Error object otherwise
     */
    public function validate_password_reset_strong_password( $errors, $user ) {
        if ( ! ( $user instanceof \WP_User ) ) {
            return;
        }
        
        if ( isset( $_POST['pass1'] ) ) {
            $user->pass = $_POST['pass1'];
            $this->strong_pass( $errors, $user );
        }
    }
    
    /**
     * Validates for strong password
     *
     * @since 3.0
     *
     * @param WP_Error $errors WP Error object
     * @param WP_User  $user   WP_User object
     */
    private function strong_pass( $errors, $user ) {
        if ( ! isset( $user->pass ) ) {
            return;
        }
        
        $settings = describr_get_field( 'user_pass' );

        $min_chars = $settings['min_chars'];
        $max_chars = $settings['max_chars'];
                
        $pwd_length = mb_strlen( $user->pass );

        if ( $pwd_length < $min_chars ) {
            $errMsg = sprintf(
                /*translators: %s Minimum number of characters.*/
                    __( '<strong>Error:</strong> Your password must contain at least %s characters.', 'describr' ),
                    $min_chars
                );
                $errors->add( 'user_pass', $errMsg );
        }

        if ( $pwd_length > $max_chars ) {
            $errMsg = sprintf(
                /*translators: %s Maximum number of characters.*/
                __( '<strong>Error:</strong> Your password cannot contain more than %s characters.', 'describr' ),
                $max_chars
            );
            $errors->add( 'user_pass', $errMsg );
        }

        if ( false !== mb_stripos( $user->pass, $user->user_login ) ) {
            $errors->add( 'user_pass', __( '<strong>Error:</strong> Your password cannot contain your username.', 'describr' ) );
        }

        if ( false !== mb_stripos( $user->pass, $user->user_email ) ) {
            $errors->add( 'user_pass', __( '<strong>Error:</strong> Your password cannot contain your email address.', 'describr' ) );
        }

        if ( ! describr_is_strong_pass( $user->pass ) ) {
            $special_chars = describr_special_chars_entity_numbers();
                    
            if ( $special_chars ) {
                $errMsg = sprintf(
                    /*translators: %s: Punctation marks.*/
                    __( '<strong>Error:</strong> Your password must contain at least one lowercase letter, one uppercase letter, one number, and one character like %s.', 'describr' ),
                        '<code>' . implode( ' ', $special_chars ) . '</code>'
                );

                $errors->add( 'user_pass', $errMsg );
            } else {
                $errors->add( 'user_pass', __( '<strong>Error:</strong> Your password must contain at least one lowercase letter, one uppercase letter, and one number.', 'describr' ) );
            }
        }
        
        $_errors = new \WP_Error();

        describr_check_identifier_in_pass( $user->pass, $user, $_errors );

        if ( $_errors->has_errors() ) {
            foreach ( $_errors->get_error_codes() as $code ) {
                foreach ( $_errors->get_error_messages( $code ) as $error_message ) {
                    $errors->add( $code, '<strong>' . _x( 'Error:', 'password', 'describr' ) . '</strong> ' . $error_message );
                }
            }
        }
    }
    
    /**
     * Updates the user's status after the user's password is reset.
     *
     * @since 3.0
     *
     * @param WP_User $user The user.
     */
    public static function update_status( $user ) {
        global $wpdb;

        if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT user_activation_key FROM $wpdb->users WHERE ID = %d LIMIT 1", $user->ID ) ) 
            && 'confirm' === $user->account_status ) {
            update_user_meta( $user->ID, 'account_status', 'approved' );
        }
    }

    /**
     * Returns the `is_password_required` property value
     *
     * @since 3.0
     *
     * @param string $prop The property whose value is being retrieved
     * @return bool
     */
    public function __get( $prop ) {
        if ( 'is_password_required' === $prop ) {
            return $this->$prop;
        }
    }
    
    /**
     * Filters the text describing the site's strong password policy.
     *
     * @since 3.0
     *
     * @param string $hint The default password hint text.
     * @param string The site's strong password policy (if one exists)
     *               or the passed password complexity policy.
     */
    public function hint_filter( $hint ) {
        $hint_ = self::hint();
        
        if ( ! empty( $hint_ ) ) {
            $hint = $hint_;
        } elseif ( doing_filter( 'describr_get_field_user_pass' ) ) {
            $hint = '';
        }

        return $hint;
    }
    
    /**
     * Retrieves the text describing the site's strong password policy.
     *
     * @since 3.0
     *
     * @param string The site's strong password policy.
     */
    public static function hint() {
        $settings = describr_get_field( 'user_pass' );

        $min = $settings['min_chars'];
        $max = $settings['max_chars'];

        $special_chars = describr_special_chars_entity_numbers();

        $puncts = implode( ' ', $special_chars );

        if ( $puncts ) {
            $puncts = "<code>{$puncts}</code>";
        }

        if ( $min && $max ) {
            $hint = sprintf(
                /*translators: 1: Minimum number of characters. 2: Maximum number of characters. 3: Punctuation marks.*/
                __( 'Password must contain at least %1$s characters and at most %2$s characters and contain at least one lowercase letter, one uppercase letter, one number, and one character like %3$s.', 'describr' ),
                $min,
                $max,
                $puncts
            );
        } elseif ( $min ) {
            $hint = sprintf(
                /*translators: 1: Minimum number of characters. 2: Punctuation marks.*/
                __( 'Password must contain at least %1$s characters and contain at least one lowercase letter, one uppercase letter, one number, and one character like %2$s.', 'describr' ),
                $min,
                $puncts
            );
        } elseif ( $max ) {
            $hint = sprintf(
                /*translators: 1: Maximum number of characters. 2: Punctuation marks.*/
                __( 'Password must contain no more than %1$s characters and contain at least one lowercase letter, one uppercase letter, one number, and one character like %2$s.', 'describr' ),
                $max,
                $puncts
            );
        } else {
            $hint = sprintf(
                /*translators: %s: Punctuation marks.*/
                __( 'Password must contain at least one uppercase case letter, one lowercase letter, one number, and one character like %s.', 'describr' ),
                $puncts
            );
        }
    
        /**
         * Filters the text describing the site's password complexity policy
         *
         * @since 3.0
         *
         * @param string $hint The password hint text
         */
        return apply_filters( 'describr_password_hint', $hint );
    }
}
