<?php

namespace WBCR\Titan\Tweaks;

use ZxcvbnPhp\Zxcvbn;

final class Strong_Passwords {

	const STRENGTH_KEY = 'titan-password-strength';

	public function __construct() {

		add_action( 'titan_register_password_requirements', [ $this, 'register_requirements' ] );

		add_action( 'admin_enqueue_scripts', [ $this, 'add_scripts' ] );
		add_action( 'resetpass_form', [ $this, 'add_scripts_to_wp_login' ] );
		add_action( 'titan_password_requirements_change_form', [ $this, 'add_scripts_to_wp_login' ] );
	}

	/**
	 * Register the Strong Passwords requirement.
	 */
	public function register_requirements() {
		Password_Requirements_Base::register(
			'strength',
			[
				'evaluate'                => [ $this, 'evaluate' ],
				'validate'                => [ $this, 'validate' ],
				'reason'                  => [ $this, 'reason' ],
				'meta'                    => self::STRENGTH_KEY,
				'evaluate_if_not_enabled' => true,
				'defaults'                => [ 'role' => 'administrator' ],
				'settings_config'         => [ $this, 'get_settings_config' ],
			] 
		);
	}

	/**
	 * Enqueue script to hide the acknowledge weak password checkbox.
	 *
	 * @return void
	 */
	public function add_scripts() {

		global $pagenow;

		if ( 'profile.php' !== $pagenow ) {
			return;
		}

		if ( ! Password_Requirements_Base::is_requirement_enabled( 'strength' ) ) {
			return;
		}

		$settings = Password_Requirements_Base::get_requirement_settings( 'strength' );
		$role     = isset( $settings['role'] ) ? $settings['role'] : 'administrator';

		require_once WTITAN_PLUGIN_DIR . '/includes/tweaks/password-requirements/class-canonical-roles.php';

		if ( Canonical_Roles::is_user_at_least( $role ) ) {
			wp_enqueue_script( 'titan_strong_passwords', WTITAN_PLUGIN_URL . '/includes/tweaks/password-requirements/assets/js/script.js', [ 'jquery' ], WTITAN_PLUGIN_VERSION );
		}
	}

	/**
	 * On the reset password and login interstitial form, render the Strong Passwords JS to hide the acknowledge weak password checkbox.
	 *
	 * We have to do this in these late actions so we have access to the correct user data.
	 *
	 * @param \WP_User $user
	 */
	public function add_scripts_to_wp_login( $user ) {

		if ( ! Password_Requirements_Base::is_requirement_enabled( 'strength' ) ) {
			return;
		}

		$settings = Password_Requirements_Base::get_requirement_settings( 'strength' );
		$role     = isset( $settings['role'] ) ? $settings['role'] : 'administrator';

		require_once WTITAN_PLUGIN_DIR . '/includes/tweaks/password-requirements/class-canonical-roles.php';

		if ( Canonical_Roles::is_user_at_least( $role, $user ) ) {
			wp_enqueue_script( 'titan_strong_passwords', WTITAN_PLUGIN_URL . '/includes/tweaks/password-requirements/assets/js/script.js', [ 'jquery' ], WTITAN_PLUGIN_VERSION );
		}
	}

	/**
	 * Provide the reason string displayed to users on the change password form.
	 *
	 * @param $evaluation
	 *
	 * @return string
	 */
	public function reason( $evaluation ) {
		return esc_html__( 'Due to site rules, a strong password is required for your account. Please choose a new password that rates as strong on the meter.', 'anti-spam' );
	}

	/**
	 * Evaluate the strength of a password.
	 *
	 * @param string   $password
	 * @param \WP_User $user
	 *
	 * @return int
	 */
	public function evaluate( $password, $user ) {
		return $this->get_password_strength( $user, $password );
	}

	/**
	 * Validate whether a password strength is acceptable for a given user.
	 *
	 * @param int                $strength
	 * @param \WP_User|\stdClass $user
	 * @param array              $settings
	 * @param array              $args
	 *
	 * @return bool
	 */
	public function validate( $strength, $user, $settings, $args ) {

		if ( (int) $strength === 4 ) {
			return true;
		}

		require_once WTITAN_PLUGIN_DIR . '/includes/tweaks/password-requirements/class-canonical-roles.php';

		$role = isset( $args['canonical'] ) ? $args['canonical'] : Canonical_Roles::get_user_role( $user );

		if ( ! Canonical_Roles::is_canonical_role_at_least( $settings['role'], $role ) ) {
			return true;
		}

		return $this->make_error_message();
	}

	public function get_settings_config() {
		return [
			'label'       => esc_html__( 'Strong Passwords', 'anti-spam' ),
			'description' => esc_html__( 'Force users to use strong passwords as rated by the WordPress password meter.', 'anti-spam' ),
			'render'      => [ $this, 'render_settings' ],
			'sanitize'    => [ $this, 'sanitize_settings' ],
		];
	}

	/**
	 * Render the Settings Page.
	 *
	 * @param \ITSEC_Form $form
	 */
	public function render_settings( $form ) {

		$href = 'http://codex.wordpress.org/Roles_and_Capabilities';
		$link = '<a href="' . $href . '" target="_blank" rel="noopener noreferrer">' . $href . '</a>';
		?>
		<tr>
			<th scope="row">
				<label for="titan-password-requirements-requirement_settings-strength-role">
					<?php esc_html_e( 'Minimum Role', 'anti-spam' ); ?>
				</label>
			</th>
			<td>
				<?php $form->add_canonical_roles( 'role' ); ?>
				<br/>
				<label for="titan-password-requirements-requirement_settings-strength-role"><?php _e( 'Minimum role at which a user must choose a strong password.', 'anti-spam' ); ?></label>
				<?php /* translators: %s: link to WordPress documentation */ ?>
				<p class="description"><?php printf( __( 'For more information on WordPress roles and capabilities please see %s.', 'anti-spam' ), $link ); ?></p>
				<p class="warningtext description"><?php _e( 'Warning: If your site invites public registrations setting the role too low may annoy your members.', 'anti-spam' ); ?></p>
			</td>
		</tr>
		<?php
	}

	/**
	 * Get a list of the sanitizer rules to apply.
	 *
	 * @param array $settings
	 *
	 * @return array
	 */
	public function sanitize_settings( $settings ) {
		return [
			[ 'string', 'role', esc_html__( 'Minimum Role for Strong Passwords', 'anti-spam' ) ],
			[ 'canonical-roles', 'role', esc_html__( 'Minimum Role for Strong Passwords', 'anti-spam' ) ],
		];
	}

	/**
	 * Get the strong password error message according to the given context.
	 *
	 * @return string
	 */
	private function make_error_message() {
		$message = __( '<strong>Error</strong>: Due to site rules, a strong password is required. Please choose a new password that rates as <strong>Strong</strong> on the meter.', 'anti-spam' );

		return wp_kses( $message, [ 'strong' => [] ] );
	}

	/**
	 * Calculate the strength of a password.
	 *
	 * @param \WP_User $user
	 * @param string   $password
	 *
	 * @return int
	 */
	private function get_password_strength( $user, $password ) {

		$penalty_strings = [
			get_site_option( 'admin_email' ),
		];
		$user_properties = [
			'user_login',
			'first_name',
			'last_name',
			'nickname',
			'display_name',
			'user_email',
			'user_url',
			'description',
		];

		foreach ( $user_properties as $user_property ) {
			if ( isset( $user->$user_property ) ) {
				$penalty_strings[] = $user->$user_property;
			}
		}

		$results = self::get_password_strength_results( $password, $penalty_strings );

		return $results['score'];
	}

	/**
	 * Evaluate a password's strength.
	 *
	 * @param string $password
	 * @param array  $penalty_strings Additional strings that if found within the password, will decrease the strength.
	 *
	 * @return array
	 */
	public static function get_password_strength_results( $password, $penalty_strings = [] ) {
		$zxcvbn = new Zxcvbn();
		return $zxcvbn->passwordStrength( $password, $penalty_strings );
	}
}

new Strong_Passwords();
