<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Created by PhpStorm.
 * User: gaupoit
 * Date: 5/6/19
 * Time: 21:04
 */
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals
if ( ! class_exists( 'PPW_Password_Services' ) ) {
	class PPW_Password_Services implements PPW_Service_Interfaces {

		/**
		 * @var PPW_Repository_Passwords
		 */
		private $passwords_repository;

		/**
		 * PPW_Password_Services constructor.
		 *
		 * @param PPW_Repository_Passwords $repo The password repository class help to interact with DB.
		 */
		public function __construct( $repo = null ) {
			if ( is_null( $repo ) ) {
				$this->passwords_repository = new PPW_Repository_Passwords();
			} else {
				$this->passwords_repository = $repo;
			}
		}

		/**
		 * Check content is protected
		 *
		 * @param $post_id
		 *
		 * @return array|bool
		 */
		public function is_protected_content( $post_id ) {
			$result = $this->get_passwords( $post_id );
			if ( ! $result['has_global_passwords'] && ! $result['has_role_passwords'] ) {
				return false;
			}

			return $result;
		}

		/**
		 * Check password is valid
		 *
		 * @param $password
		 * @param $post_id
		 * @param $current_roles
		 *
		 * @return bool
		 */
		public function is_valid_password( $password, $post_id, $current_roles ) {
			if ( $this->check_password_type_is_global( $post_id, $password ) ) {
				$this->set_cookie_bypass_cache( $password . $post_id, PPW_Constants::COOKIE_NAME . $post_id );

				return true;
			}

			if ( ! is_user_logged_in() ) {
				return false;
			}

			$role_meta       = get_post_meta( $post_id, PPW_Constants::POST_PROTECTION_ROLES, true );
			$protected_roles = ppw_free_fix_serialize_data( $role_meta );

			if ( empty( $protected_roles ) ) {
				return false;
			}

			return $this->check_password_type_is_roles( $current_roles, $protected_roles, $password, $post_id );
		}

		/**
		 * Set password to cookie
		 *
		 * @param $password
		 * @param $cookie_name
		 */
		public function set_password_to_cookie( $password, $cookie_name ) {
			$password_hashed         = wp_hash_password( $password );
			$expire                  = apply_filters( PPW_Constants::HOOK_COOKIE_EXPIRED, time() + 7 * DAY_IN_SECONDS );
			$password_cookie_expired = ppw_core_get_setting_type_string( PPW_Constants::COOKIE_EXPIRED );
			if ( ! empty( $password_cookie_expired ) ) {
				$time = explode( ' ', $password_cookie_expired )[0];
				$unit = ppw_core_get_unit_time( $password_cookie_expired );
				if ( 0 !== $unit ) {
					$expire = apply_filters( PPW_Constants::HOOK_COOKIE_EXPIRED, time() + (int) $time * $unit );
				}
			}

			$referer = wp_get_referer();
			// phpcs:disable
			if ( $referer ) {
				$secure = ( 'https' === parse_url( $referer, PHP_URL_SCHEME ) );
			} else {
				$secure = false;
			}
			
			$expire = apply_filters( 'ppw_cookie_expire', $expire );
			$expire = apply_filters( 'ppwp_cookie_expiry', $expire );
			return setcookie( $cookie_name . COOKIEHASH, $password_hashed, $expire, COOKIEPATH, COOKIE_DOMAIN, $secure );
		}

		public function generate_cookie_data( $cookie_name, $password ) {
			$password_hashed         = wp_hash_password( $password );
			$expire                  = apply_filters( PPW_Constants::HOOK_COOKIE_EXPIRED, time() + 7 * DAY_IN_SECONDS );
			$password_cookie_expired = ppw_core_get_setting_type_string( PPW_Constants::COOKIE_EXPIRED );
			if ( ! empty( $password_cookie_expired ) ) {
				$time = explode( ' ', $password_cookie_expired )[0];
				$unit = ppw_core_get_unit_time( $password_cookie_expired );
				if ( 0 !== $unit ) {
					$expire = apply_filters( PPW_Constants::HOOK_COOKIE_EXPIRED, time() + (int) $time * $unit );
				}
			}

			$referer = wp_get_referer();
			// phpcs:disable
			if ( $referer ) {
				$secure = ( 'https' === parse_url( $referer, PHP_URL_SCHEME ) );
			} else {
				$secure = false;
			}
			
			$expire = apply_filters( 'ppw_cookie_expire', $expire );
			$expire = apply_filters( 'ppwp_cookie_expiry', $expire );

			return array(
				'name'   => $cookie_name . COOKIEHASH,
				'value'  => $password_hashed,
				'expire' => $expire,
			);
		}

		/**
		 * Set password to cookie with case cookie name the same WP to bypass cache
		 *
		 * @param string $cookie_value The value of the cookie.
		 * @param string $cookie_name  The name of the cookie.
		 */
		public function set_cookie_bypass_cache( $cookie_value, $cookie_name ) {
			// Bypass Caching plugin with WordPress cookie.
			$this->set_password_to_cookie( $cookie_value, PPW_Constants::WP_POST_PASS );
			$this->set_password_to_cookie( $cookie_value, $cookie_name );
		}

		/**
		 * Check whether the current cookie is valid.
		 *
		 * @param $post_id
		 * @param $passwords
		 * @param $cookie_name
		 *
		 * @return bool
		 */
		public function is_valid_cookie( $post_id, $passwords, $cookie_name ) {
			$_cookie = wp_unslash( $_COOKIE );
			if ( ! isset( $_cookie[ $cookie_name . $post_id . COOKIEHASH ] ) ) {
				return false;
			}

			$cookie  = sanitize_text_field( $_cookie[ $cookie_name . $post_id . COOKIEHASH ] );
			$hash    = wp_unslash( $cookie );
			
			$roles = ppw_core_get_current_role();
			foreach ( $passwords as $password ) {
				if ( wp_check_password( $password . $post_id, $hash ) ) {
					return true;
				}

				foreach ( $roles as $role ) {
					if ( wp_check_password( $password . $role . $post_id, $hash ) ) {
						return true;
					}
				}
			}

			return false;
		}

		/**
		 * Redirect after enter password
		 *
		 * @param bool $is_valid Is entered password valid.
		 */
		public function handle_redirect_after_enter_password( $is_valid ) {
			// Refactor since 1.4.2.
			// 1. Clean code
			// 2. Easier to write UT.
			$redirect_url = $this->get_redirect_url( $is_valid );
			wp_safe_redirect( $redirect_url );
			exit();
		}

		/**
		 * Get redirect URL after user entered password.
		 *
		 * @param bool $is_valid Is password valid.
		 *
		 * @return string
		 */
		public function get_redirect_url( $is_valid ) {
			$referrer_url      = $this->get_referer_url();
			$params_in_referer = ppw_core_get_param_in_url( $referrer_url );

			if ( $is_valid ) {
				$referrer_url = apply_filters(
					'ppwp_ppf_referrer_url',
					$referrer_url,
					array(
						'is_valid'   => $is_valid,
						'parameters' => $params_in_referer,
					)
				);

				$url_redirect = preg_replace( '/[&?]' . PPW_Constants::WRONG_PASSWORD_PARAM . '=true/', '', $referrer_url );
				$params       = apply_filters(
					PPW_Constants::HOOK_PARAM_PASSWORD_SUCCESS,
					array(
						'name'  => PPW_Constants::PASSWORD_PARAM_NAME,
						'value' => PPW_Constants::PASSWORD_PARAM_VALUE,
					)
				);

				if ( array_key_exists( $params['name'], $params_in_referer ) && '1' === $params_in_referer[ $params['name'] ] ) {
					return $url_redirect;
				}

				$params_in_redirect = ppw_core_get_param_in_url( $url_redirect );
				$new_param          = empty( $params_in_redirect ) ? '?' . $params['name'] . '=' . $params['value'] : '&' . $params['name'] . '=' . $params['value'];

				$new_param = apply_filters(
					'ppwp_ppf_redirect_url_param_before_return_content',
					$new_param
				);

				return apply_filters( 'ppwp_ppf_redirect_url_before_return_content', $url_redirect . $new_param );
			}

			if ( array_key_exists( PPW_Constants::WRONG_PASSWORD_PARAM, $params_in_referer ) && 'true' === $params_in_referer[ PPW_Constants::WRONG_PASSWORD_PARAM ] ) {
				return apply_filters(
					'ppwp_ppf_redirect_url',
					$referrer_url,
					array(
						'is_valid'   => $is_valid,
						'parameters' => $params_in_referer,
					)
				);
			}

			$new_param = empty( $params_in_referer ) ? '?' . PPW_Constants::WRONG_PASSWORD_PARAM . '=true' : '&' . PPW_Constants::WRONG_PASSWORD_PARAM . '=true';

			return apply_filters(
				'ppwp_ppf_redirect_url',
				$referrer_url . $new_param,
				array(
					'is_valid'   => $is_valid,
					'parameters' => $params_in_referer,
				)
			);
		}

		/**
		 * Get referer URL from HTTP Referrer or callback URL in post form action URL.
		 *
		 * @return mixed False if cannot find the referer URL.
		 */
		public function get_referer_url() {
			$referrer_url = wp_get_referer();
			$using_cb     = false === $referrer_url;
			$using_cb     = apply_filters( 'ppw_use_callback_url', $using_cb );
			if ( $using_cb ) {
				// We need to get the callback URL in the password form action URL.
				// in case Referrer-Policy is set no-referrer.
				$cb_param = PPW_Constants::CALL_BACK_URL_PARAM;
				if ( isset( $_GET[ $cb_param ] ) ) {  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We no need to handle nonce verfication for this one because already verify in parent function.
					$referrer_url = rawurldecode( esc_url_raw( wp_unslash( $_GET[ $cb_param ] ) ) );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We no need to handle nonce verfication for this one because already verify in parent function.
				}
			}

			// If doesn't have callback URL and no-referer then return to home page.
			if ( false === $referrer_url ) {
				global $wp;
				$referrer_url = home_url( $wp->request );
			}

			return $referrer_url;
		}

		/**
		 * Handle and check condition before create new password
		 *
		 * @param int|string $id                   The post ID.
		 * @param string     $role_selected        The role user select on client.
		 * @param array      $new_global_passwords List global passwords user enter on client.
		 * @param string     $new_role_password    Role password user enter on client.
		 *
		 * @return array|mixed
		 */
		public function create_new_password( $id, $role_selected, $new_global_passwords, $new_role_password ) {
			$post_meta                = get_post_meta( $id, PPW_Constants::POST_PROTECTION_ROLES, true );
			$current_roles_password   = ppw_free_fix_serialize_data( $post_meta );
			$current_global_passwords = get_post_meta( $id, PPW_Constants::GLOBAL_PASSWORDS, true );
			if ( 'global' === $role_selected ) {
				return $this->create_password_type_global( $id, $new_global_passwords, $current_global_passwords, $current_roles_password, $role_selected );
			}

			return $this->create_password_type_role( $id, $role_selected, $new_role_password, $current_global_passwords, $current_roles_password );
		}

		/**
		 * Check condition before create new password type global
		 *
		 * @param int|string $id                       The post ID.
		 * @param array      $new_global_passwords     List global passwords user enter on client.
		 * @param array      $current_global_passwords List all current global passwords.
		 * @param array      $current_roles_password   List all current role passwords.
		 * @param string     $role_selected            The role user select on client.
		 *
		 * @return mixed
		 */
		public function create_password_type_global( $id, $new_global_passwords, $current_global_passwords, $current_roles_password, $role_selected ) {
			// Validate global password(check bad request).
			if ( $this->global_passwords_is_bad_request( $new_global_passwords ) ) {
				wp_send_json(
					array(
						'is_error' => true,
						'message'  => PPW_Constants::BAD_REQUEST_MESSAGE,
					),
					400
				);
				wp_die();
			}

			// Validate global password(empty and duplicate).
			ppw_free_validate_password_type_global( $new_global_passwords, $current_global_passwords, $current_roles_password );
			update_post_meta( $id, PPW_Constants::GLOBAL_PASSWORDS, $new_global_passwords );

			// Clear cache for Cache plugin.
			ppw_core_clear_cache_by_id( $id );

			/*
			// Handle cache for page/post have password type is global with Super Cache plugin.
			$free_cache = new PPW_Cache_Services();
			$free_cache->handle_cache_for_password_type_global_with_super_cache( $new_global_passwords, $id, $current_roles_password );
			*/
			$current_roles_password[ $role_selected ] = implode( "\n", $new_global_passwords );

			return $current_roles_password;
		}

		/**
		 * Check bad request with data type is global passwords
		 *
		 * @param array $passwords Global passwords.
		 *
		 * @return bool
		 */
		private function global_passwords_is_bad_request( $passwords ) {
			foreach ( $passwords as $password ) {
				if ( strpos( $password, ' ' ) !== false ) {
					return true;
				}
			}

			// Check element unique in array.
			return count( $passwords ) !== count( array_unique( $passwords ) );
		}

		/**
		 * Check condition before create new password type role
		 *
		 * @param int|string $id                       The post ID.
		 * @param string     $role_selected            The role user select on client.
		 * @param string     $new_role_password        Role password user enter on client.
		 * @param array      $current_global_passwords List all current global passwords.
		 * @param array      $current_roles_password   List all current role passwords.
		 *
		 * @return mixed
		 */
		public function create_password_type_role( $id, $role_selected, $new_role_password, $current_global_passwords, $current_roles_password ) {
			// Validate role password(check bad request).
			if ( $this->role_password_is_bad_request( $new_role_password ) ) {
				wp_send_json(
					array(
						'is_error' => true,
						'message'  => PPW_Constants::BAD_REQUEST_MESSAGE,
					),
					400
				);
				wp_die();
			}

			// Validate role password(empty and duplicate).
			ppw_free_validate_password_type_role( $role_selected, $new_role_password, $current_global_passwords, $current_roles_password );
			$current_roles_password[ $role_selected ] = $new_role_password;
			delete_post_meta( $id, PPW_Constants::POST_PROTECTION_ROLES );
			add_post_meta( $id, PPW_Constants::POST_PROTECTION_ROLES, $current_roles_password );

			// Clear cache for Cache plugin.
			ppw_core_clear_cache_by_id( $id );

			/*
			// Handle cache for page/post have password type is role with Super Cache plugin.
			$free_cache = new PPW_Cache_Services();
			$free_cache->handle_cache_for_password_type_role_with_super_cache( $new_role_password, $id, $current_roles_password, $current_global_passwords );
			*/
			if ( ! empty( $current_global_passwords ) ) {
				$current_roles_password['global'] = implode( "\n", $current_global_passwords );
			}

			return $current_roles_password;
		}

		/**
		 * Check bad request with data type is role password
		 *
		 * @param string $password Role password.
		 *
		 * @return bool
		 */
		private function role_password_is_bad_request( $password ) {
			return strpos( $password, ' ' ) !== false;
		}

		/**
		 * Password is empty with not 0.
		 *
		 * @param string $pwd Password.
		 *
		 * @return bool
		 */
		public function has_no_empty_password( $pwd ) {
			return ! empty( $pwd ) || '0' === $pwd;
		}

		/**
		 * Get all passwords
		 *
		 * @param int|string $post_id The post ID.
		 *
		 * @return array
		 */
		public function get_passwords( $post_id ) {
			// 1. Get all passwords.
			$global_passwords     = get_post_meta( $post_id, PPW_Constants::GLOBAL_PASSWORDS, true );
			$global_passwords     = ! empty( $global_passwords ) ? $global_passwords : array();
			$has_global_passwords = ! empty( $global_passwords ) && is_array( $global_passwords );
			$raw_data             = get_post_meta( $post_id, PPW_Constants::POST_PROTECTION_ROLES, true );
			$protected_roles      = ppw_free_fix_serialize_data( $raw_data );

			$filtered_protected_roles  = array_filter(
				$protected_roles,
				function ( $pass ) {
					return $this->has_no_empty_password( $pass );
				}
			);
			$has_role_passwords        = ! empty( $filtered_protected_roles );
			$has_current_role_password = false;
			if ( $has_role_passwords ) {
				$roles = ppw_core_get_current_role();
				foreach ( $roles as $role ) {
					if ( array_key_exists( $role, $filtered_protected_roles ) ) {
						$has_current_role_password = true;
						array_push( $global_passwords, $protected_roles[ $role ] );
					}
				}
			}

			return array(
				'passwords'                 => $global_passwords,
				'has_role_passwords'        => $has_role_passwords,
				'has_current_role_password' => $has_current_role_password,
				'has_global_passwords'      => $has_global_passwords,
			);
		}

		/**
		 * Check password type is global
		 *
		 * @param $post_id
		 * @param $password
		 *
		 * @return bool
		 */
		public function check_password_type_is_global( $post_id, $password ) {
			$global_passwords = get_post_meta( $post_id, PPW_Constants::GLOBAL_PASSWORDS, true );
			if ( empty( $global_passwords ) || ! is_array( $global_passwords ) ) {
				return false;
			}

			foreach ( $global_passwords as $pass ) {
				if ( $password === $pass ) {
					return true;
				}
			}

			return false;
		}

		/**
		 * Check password type is roles
		 *
		 * @param $current_roles
		 * @param $protectedRoles
		 * @param $password
		 * @param $post_id
		 *
		 * @return bool
		 */
		public function check_password_type_is_roles( $current_roles, $protectedRoles, $password, $post_id ) {
			foreach ( $current_roles as $role ) {
				if ( ! array_key_exists( $role, $protectedRoles ) || ! $this->has_no_empty_password( $protectedRoles[ $role ] ) || $protectedRoles[ $role ] !== $password ) {
					continue;
				}

				$this->set_cookie_bypass_cache( $password . $role . $post_id, PPW_Constants::COOKIE_NAME . $post_id );

				return true;
			}

			return false;
		}

		/**
		 * Migrate default password and update post password of Wordpress for free version
		 * TODO: need to revamp the logic.
		 */
		function migrate_default_password() {
			$posts = ppw_core_get_posts_password_protected_by_wp();
			foreach ( $posts as $post ) {
				$post_id = $post->ID;
				$global_password = get_post_meta( $post_id, PPW_Constants::GLOBAL_PASSWORDS, true );
				$global_password = ! empty( $global_password ) ? $global_password : array();
				$raw_data        = get_post_meta( $post->ID, PPW_Constants::POST_PROTECTION_ROLES, true );

				// 1. Update password for role
				$protected_roles = ppw_free_fix_serialize_data( $raw_data );
				foreach ( $protected_roles as $key => $value ) {
					if ( str_replace( " ", "", $post->post_password ) === $value ) {
						$protected_roles[ $key ] = '';
						update_post_meta( $post_id, PPW_Constants::POST_PROTECTION_ROLES, $protected_roles );
					}
				}

				// 2. Update password for global
				if ( ! in_array( str_replace( " ", "", $post->post_password ), $global_password ) ) {
					array_push( $global_password, str_replace( " ", "", $post->post_password ) );
				}

				update_post_meta( $post_id, PPW_Constants::GLOBAL_PASSWORDS, $global_password );
				update_post_meta( $post_id, 'ppwp_post_password_bk', $post->post_password );

				// 3. Update default password for Wordpress
				wp_update_post( array(
					'ID'            => $post_id,
					'post_password' => '',
				) );
			}
		}

		public function get_pw_meta( $post_id = false ) {
			global $wpdb;
			$table_name = $wpdb->prefix . 'postmeta';
			$global_key = PPW_Constants::GLOBAL_PASSWORDS;
			$role_key   = PPW_Constants::POST_PROTECTION_ROLES;

			$query = $wpdb->prepare( "SELECT * FROM {$table_name} where ( meta_key IN ( %s, %s ) )", $global_key, $role_key ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- We don't want to set table name as placeholder

			if ( $post_id ) {
				$query = $wpdb->prepare( $query . ' AND post_id = %d', $post_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- we already prepare $query above
			}

			return $wpdb->get_results( $query );  // phpcs:ignore -- db call ok, we already prepare $query above.
		}


		public function get_data_to_migrate() {
			$ids    = $this->get_protected_post_ids();
			$result = array();
			foreach ( $ids as $post_id ) {
				$passwords = $this->get_pw_meta( $post_id );
				$result[]  = array(
					'post_id'   => $post_id,
					'passwords' => $this->massage_pw_from_post_meta( $passwords ),
				);
			}
			$old = get_option( 'ppw_data_checksum', false );
			if ( false === $old ) {
				update_option( 'ppw_data_checksum', $result );

				return $result;
			}
			$diff = $this->check_sum_migrate_data( $result, $old );
			update_option( 'ppw_data_checksum', $result );

			return $diff;
		}

		public function check_sum_migrate_data( $current, $old ) {
			if ( count( $current ) > count( $old ) ) {
				$large = $current;
				$small = $old;
			} else {
				$large = $old;
				$small = $current;
			}

			$post_ids = array_column( $small, 'post_id' );
			$result   = array();
			foreach ( $large as $cur ) {
				$post_id     = $cur['post_id'];
				$found_index = array_search( $post_id, $post_ids );
				if ( false === $found_index ) {
					$result[] = $cur;
					continue;
				}

				$found = $small[ $found_index ];

				if ( ! isset ( $found['passwords'] ) ) {
					continue;
				}

				if ( $this->compare_passwords( $cur, $found ) ) {
					$result[] = $found;
				}
			}

			return $result;
		}

		/**
		 * Massage password from post meta
		 *
		 * @param array $meta post meta from DB.
		 *
		 * @return array
		 */
		public function massage_pw_from_post_meta( $meta ) {
			$result = array(
				'global' => array(),
				'role'   => array(),
			);
			foreach ( $meta as $val ) {
				if ( PPW_Constants::GLOBAL_PASSWORDS === $val->meta_key ) {
					$meta_value       = ppw_free_fix_serialize_data( $val->meta_value );
					$result['global'] = array_merge( $result['global'], $meta_value );
				} elseif ( PPW_Constants::POST_PROTECTION_ROLES === $val->meta_key ) {
					$meta_value     = ppw_free_fix_serialize_data( $val->meta_value );
					$result['role'] = $this->massage_pw_for_roles_from_post_meta( $meta_value );
				}
			}

			return $result;
		}

		/**
		 * Massage by define a password - role map
		 *
		 * Input: [ "admin" => "1", "editor" => "2", author => "1"]
		 * Output: [ "1" => array('admin', 'author'), "2" => array('editor') ]
		 *
		 * @param $meta_value
		 *
		 * @return array
		 */
		public function massage_pw_for_roles_from_post_meta( $meta_value ) {
			$result = array();
			if ( ! is_array( $meta_value ) ) {
				return $result;
			}

			foreach ( $meta_value as $role => $pw ) {

				if ( '' === $pw ) {
					continue;
				}

				if ( ! array_key_exists( $pw, $result ) ) {
					$result[ $pw ] = array( $role );
				}

				if ( ! in_array( $role, $result[ $pw ] ) ) {
					array_push( $result[ $pw ], $role );
				}
			}

			return $result;
		}


		public function get_protected_post_ids() {
			$role_key = PPW_Constants::POST_PROTECTION_ROLES;
			$results  = array_filter( $this->get_pw_meta(), function ( $value ) use ( $role_key ) {
				$meta_value = ppw_free_fix_serialize_data( $value->meta_value );
				$is_valid_meta_value = is_array( $meta_value );
				if ( $is_valid_meta_value && $role_key === $value->meta_key ) {
					foreach ( $meta_value as $meta ) {
						return $meta !== '';
					}
				}

				return $is_valid_meta_value && count( $meta_value ) > 0;
			} );

			return array_unique(
				array_map( function ( $val ) {
					return $val->post_id;
				}, $results )
			);
		}

		/**
		 * Generate custom row action.
		 *
		 * @param array    $actions An array for row action.
		 * @param stdClass $post    The post object.
		 *
		 * @return array
		 */
		public function generate_custom_row_action( $actions, $post ) {
			$post_id           = $post->ID;
			$is_protected      = $this->is_protected_content( $post_id );
			$btn_label         = $is_protected ? __( 'Unprotect', 'password-protect-page' ) : __( 'Protect', 'password-protect-page' );
			$title             = $is_protected ? __( 'Unprotect this page', 'password-protect-page' ) : __( 'Protect this page', 'password-protect-page' );
			$protection_status = $is_protected ? PPW_Constants::PROTECTION_STATUS['unprotect'] : PPW_Constants::PROTECTION_STATUS['protect'];

			$actions['ppw_protect'] = '<a style="cursor: pointer" data-ppw-status="' . $protection_status . '" onclick="ppwpRowAction.handleOnClickRowAction(' . $post_id . ')" id="ppw-protect-post_' . $post_id . '" class="ppw-protect-action" title="' . $title . '">' . $btn_label . '</a>';

			return $actions;
		}

		/**
		 * Handle protect page/post.
		 *
		 * @param int $post_id The post ID.
		 */
		public function protect_page_post( $post_id ) {
			$password = array(
				uniqid( '', false )
			);

			$this->create_new_password( $post_id, 'global', $password, null );
		}

		/**
		 * Handle unprotect page/post.
		 *
		 * @param int $post_id The post ID.
		 */
		public function unprotect_page_post( $post_id ) {
			delete_post_meta( $post_id, PPW_Constants::POST_PROTECTION_ROLES );
			delete_post_meta( $post_id, PPW_Constants::GLOBAL_PASSWORDS );
		}

		/**
		 * Update post status request from row action
		 *
		 * @param array $request Request from row action.
		 */
		public function update_post_status( $request ) {
			if ( ! isset( $request['postId'] ) || ! isset( $request['status'] ) ) {
				send_json_data_error( __( 'Our server cannot understand the data request!', 'password-protect-page' ) );
			}

			$post_id       = $request['postId'];
			$client_status = (int) $request['status'];

			if ( ! in_array( $client_status, array_values( PPW_Constants::PROTECTION_STATUS ), true ) ) {
				send_json_data_error( __( 'Our server cannot understand the data request!', 'password-protect-page' ) );
			}

			$server_status  = $client_status;
			$message        = __( 'Oops! Something went wrong. Please reload the page and try again.', 'password-protect-page' );
			$status_request = 400;
			if ( PPW_Constants::PROTECTION_STATUS['protect'] === $client_status ) {
				if ( ! $this->is_protected_content( $post_id ) ) {
					$this->protect_page_post( $post_id );
					$server_status  = PPW_Constants::PROTECTION_STATUS['unprotect'];
					$message        = __( 'Great! You\'ve successfully protected this page.', 'password-protect-page' );
					$status_request = 200;
				}
			} else {
				if ( $this->is_protected_content( $post_id ) ) {
					$this->unprotect_page_post( $post_id );
					$server_status  = PPW_Constants::PROTECTION_STATUS['protect'];
					$message        = __( 'Great! You\'ve successfully unprotected this page.', 'password-protect-page' );
					$status_request = 200;
				}
			}

			wp_send_json(
				array(
					'is_error'      => 200 === $status_request ? false : true,
					'server_status' => $server_status,
					'message'       => $message,
				),
				$status_request
			);
			wp_die();
		}

		/**
		 * @param $pwds
		 *
		 * @return array
		 */
		private function massage_role_pwd( $pwds ) {
			return array_map( function ( $v ) {
				natsort( $v );

				return implode( ',', $v );
			}, $pwds );
		}

		/**
		 * @param $cur
		 * @param $found
		 *
		 * @return bool
		 */
		private function compare_passwords( $cur, $found ) {
			$global_diff = $this->advance_array_diff( $cur['passwords']['global'], $found['passwords']['global'] );

			if ( ! empty( $global_diff ) ) {
				return true;
			}

			$current_roles = $this->massage_role_pwd( $cur['passwords']['role'] );
			$new_roles     = $this->massage_role_pwd( $found['passwords']['role'] );

			$role_diff = $this->advance_array_diff( $current_roles, $new_roles );

			return ! empty ( $role_diff );
		}

		/**
		 * @param $first
		 * @param $second
		 *
		 * @return array
		 */
		private function advance_array_diff( $first, $second ) {
			return array_merge( array_diff(
				$first,
				$second
			), array_diff(
				$second,
				$first
			) );
		}

		/**
		 * Valid permission of post ID.
		 *
		 * @param bool $required Required Password.
		 * @param int  $post_id  Post ID.
		 *
		 * @return bool True|False. True: Password is required so it will render form.
		 */
		public function is_valid_permission( $required, $post_id ) {
			// 1. Check page/post is protected.
			$result = $this->is_protected_content( $post_id );
			if ( false === $result ) {
				return false;
			}

			// 2. Check master password is valid.
			$is_valid_master_password = $this->check_master_password_is_valid( $post_id );
			if ( apply_filters( 'ppw_is_valid_cookie', $is_valid_master_password, $post_id ) ) {
				return false;
			}

			// 3. Check password in cookie.
			$passwords = $result['passwords'];

			$is_valid = $this->is_valid_cookie( $post_id, $passwords, PPW_Constants::COOKIE_NAME );

			return false === apply_filters( 'ppw_is_valid_cookie', $is_valid, $post_id );
		}

		/**
		 * Check password is exist when user enter password in form.
		 *
		 * @param int    $post_id  Post ID.
		 * @param string $password Password which user enter.
		 */
		public function handle_after_enter_password_in_password_form( $post_id, $password ) {
			$using_recaptcha = PPW_Recaptcha::get_instance()->using_single_recaptcha();
			if ( $using_recaptcha && ! PPW_Recaptcha::get_instance()->is_valid_recaptcha() ) {
				do_action( 'ppw_redirect_after_enter_password', false );
			}

			$is_valid = $this->is_valid_password_from_request( $post_id, $password );

			do_action( 'ppw_redirect_after_enter_password', $is_valid );
		}

		public function is_valid_password_from_request( $post_id, $password ) {
			// Get current role of current user.
			$current_roles   = ppw_core_get_current_role();
			$is_pro_activate = apply_filters( PPW_Constants::HOOK_IS_PRO_ACTIVATE, false );
			if ( $is_pro_activate ) {
				$is_valid = apply_filters( PPW_Constants::HOOK_CHECK_PASSWORD_IS_VALID, false, $password, $post_id, $current_roles );

				/**
				 * Check post is protected by pro version to handle master password.
				 */
				if ( $this->is_protected_content_by_pro( $post_id ) ) {
					$is_valid = $this->handle_master_passwords( $password, $is_valid, $current_roles, $post_id );
				}
			} else {
				$is_valid = $this->is_valid_free_password( $post_id, $password, $current_roles );
			}

			return apply_filters( 'ppw_is_valid_password', $is_valid, $post_id, $password, $current_roles );
		}

		/**
		 * Is valid free Password.
		 *
		 * @param integer $post_id       Post ID.
		 * @param string  $password      Password.
		 * @param array   $current_roles Current user roles.
		 *
		 * @return bool True is valid password, false is no.
		 */
		public function is_valid_free_password( $post_id, $password, $current_roles ) {
			$is_valid = $this->is_valid_password( $password, $post_id, $current_roles );
			if ( $this->is_protected_content( $post_id ) ) {
				$is_valid = $this->handle_master_passwords( $password, $is_valid, $current_roles, $post_id );
			}

			return $is_valid;
		}

		/**
		 * Check is protected content by pro version.
		 *
		 * @param integer $post_id Post ID.
		 *
		 * @return bool Is content protected by pro.
		 */
		public function is_protected_content_by_pro( $post_id ) {
			if ( ! function_exists( 'ppw_pro_get_post_id_follow_protect_child_page' ) || ! method_exists( 'PPW_Pro_Password_Services', 'is_protected_content' ) ) {
				return false;
			}
			$password_pro_service = new PPW_Pro_Password_Services();

			/**
			 * Get parent post id if post have parent-child page.
			 */
			$new_post_id = ppw_pro_get_post_id_follow_protect_child_page( $post_id );

			/**
			 * Check post or page is protected.
			 * TODO: Improve with global variable and refactor PPWP pro to check post is protected with Global variables.
			 */
			return apply_filters( PPW_Constants::HOOK_CHECK_CONTENT_IS_PROTECTED_BY_PRO, $password_pro_service->is_protected_content( $new_post_id ), $post_id );
		}


		/**
		 * Check master password is exist and apply for this post.
		 *
		 * @param string $password      Password.
		 * @param bool   $is_valid      Valid password before.
		 * @param array  $current_roles Current roles.
		 * @param int    $post_id       Post ID.
		 *
		 * @return bool Allow open content or not.
		 */
		public function handle_master_passwords( $password, $is_valid, $current_roles, $post_id ) {
			$password_info = $this->passwords_repository->get_master_password_info_by_password( $password );

			if ( is_null( $password_info ) ) {
				return $is_valid;
			}

			// Check post type is exist in password.
			if ( ! $this->check_post_type_for_master_password( $post_id, $password_info ) ) {
				return $is_valid;
			}

			$result = $this->check_valid_master_password( $password_info, $current_roles, $password );
			if ( $result['is_valid'] ) {
				/**
				 * Save cookie to client.
				 * If $result['role'] is not empty then password will be role password
				 * Else global password.
				 */
				$this->set_cookie_bypass_cache( $password . $result['role'] . $password_info->id, PPW_Constants::MASTER_COOKIE_NAME . $password_info->id );

				// Count when user enter right password.
				$this->passwords_repository->update_password(
					$password_info->id,
					array(
						'hits_count' => (int) $password_info->hits_count + 1,
					)
				);

				ppw_tracking_master_password( $post_id, $password, 'single' );

				return true;
			}

			return $is_valid;
		}

		/**
		 * Check valid master password when user enter.
		 *
		 * @param array  $password_info Password information get from database.
		 * @param array  $current_roles Current user roles.
		 * @param string $password      Password.
		 *
		 * @return array
		 */
		public function check_valid_master_password( $password_info, $current_roles, $password ) {
			$password_types = $password_info->campaign_app_type;
			$default_values = array(
				'is_valid' => false,
				'role'     => '',
			);
			// Check with global master password.
			if ( PPW_Constants::PPW_MASTER_GLOBAL === $password_types ) {
				$default_values['is_valid'] = true;

				return $default_values;
			}
			// Check with role master password.
			$role = $this->get_role_of_master_password( $current_roles, $password_types );
			if ( false !== $role ) {
				$default_values['is_valid'] = true;
				$default_values['role']     = $role;

				return $default_values;
			}

			return $default_values;
		}

		/**
		 * Check valid master password when user enter.
		 *
		 * @param array  $password_info Password information get from database.
		 * @param array  $current_roles Current user roles.
		 * @param string $prefix_role   Prefix role.
		 *
		 * @return array
		 */
		public function validate_password( $password_info, $current_roles, $prefix_role, $global_role ) {
			$password_types = $password_info->campaign_app_type;
			$default_values = array(
				'is_valid' => false,
				'role'     => '',
			);
			// Check with global master password.
			if ( $global_role === $password_types ) {
				$default_values['is_valid'] = true;

				return $default_values;
			}
			// Check with role master password.
			$role = $this->get_current_password_role( $current_roles, $password_types, $prefix_role );
			if ( false !== $role ) {
				$default_values['is_valid'] = true;
				$default_values['role']     = $role;

				return $default_values;
			}

			return $default_values;
		}

		/**
		 * Check post type is valid for master password.
		 *
		 * @param string $post_id       Post ID.
		 * @param object $password_info Password information from database.
		 *
		 * @return bool Check post type is exist in database.
		 */
		public function check_post_type_for_master_password( $post_id, $password_info ) {
			$post_type = get_post_type( $post_id );

			// Valid post type data.
			if ( false === $post_type || empty( $password_info->post_types ) ) {
				return false;
			}

			$post_types_protection = apply_filters( PPW_Constants::HOOK_MASTER_PASSWORDS_VALID_POST_TYPES, array( 'post' ) );

			// Check if post type exist in settings.
			if ( ! in_array( $post_type, $post_types_protection, true ) ) {
				return false;
			}

			$post_types_selected = explode( ';', $password_info->post_types );

			return in_array( $post_type, $post_types_selected, true );
		}

		/**
		 * Check password for type is roles.
		 *
		 * @param array  $current_roles  List current roles.
		 * @param string $password_types password type in DB.
		 *
		 * @return bool|mixed
		 */
		public function get_role_of_master_password( $current_roles, $password_types ) {
			$type_array = explode( ';', $password_types );
			foreach ( $type_array as $password_type ) {
				$role = str_replace( PPW_Constants::PPW_MASTER_ROLE, '', $password_type );
				if ( in_array( $role, $current_roles, true ) ) {
					return $role;
				}
			}

			return false;
		}

		/**
		 * Check password for type is roles.
		 *
		 * @param array  $current_roles  List current roles.
		 * @param string $password_types password type in DB.
		 *
		 * @return bool|mixed
		 */
		public function get_current_password_role( $current_roles, $password_types, $prefix_role ) {
			$type_array = explode( ';', $password_types );
			foreach ( $type_array as $password_type ) {
				$role = str_replace( $prefix_role, '', $password_type );
				if ( in_array( $role, $current_roles, true ) ) {
					return $role;
				}
			}

			return false;
		}

		/**
		 * Check protection in settings
		 *
		 * @param int $post_id The post ID.
		 *
		 * @return bool
		 */
		public function check_protection( $post_id ) {
			$post_type = get_post_type( $post_id );
			if ( 'post' === $post_type || 'page' === $post_type ) {
				return true;
			}

			return false;
		}

		/**
		 * Check if master passwords in cookie is valid.
		 *
		 * @param int $post_id Post ID.
		 *
		 * @return bool True if password is valid.
		 */
		public function check_master_password_is_valid( $post_id ) {
			if ( ! $this->check_master_cookies_are_exist() ) {
				return false;
			}
			$master_passwords = $this->passwords_repository->get_activate_master_passwords_info();
			// Get all passwords which exist current post type.
			$master_passwords = $this->massage_master_passwords_with_post_type( $master_passwords, $post_id );
			$master_passwords = apply_filters('ppw_cookie_master_passwords', $master_passwords, $post_id );

			if ( count( $master_passwords ) > 0 ) {
				// Valid master cookies.
				foreach ( $master_passwords as $master_password ) {
					if ( $this->is_valid_cookie( $master_password->id, array( $master_password->password ), PPW_Constants::MASTER_COOKIE_NAME ) ) {
						return true;
					}
				}
			}

			return false;
		}

		/**
		 * Get master passwords which post type is valid.
		 *
		 * @param array $master_passwords List Master Passwords from database.
		 * @param int   $post_id          Post ID.
		 *
		 * @return array List master passwords after valid post type.
		 */
		public function massage_master_passwords_with_post_type( $master_passwords, $post_id ) {
			$post_type = get_post_type( $post_id );

			if ( false === $post_type ) {
				return array();
			}

			$post_types_protection = apply_filters( PPW_Constants::HOOK_MASTER_PASSWORDS_VALID_POST_TYPES, array( 'post' ) );
			if ( ! in_array( $post_type, $post_types_protection, true ) ) {
				return array();
			}

			return array_filter(
				$master_passwords,
				function ( $master_password ) use ( $post_type ) {
					if ( empty( $master_password->post_types ) ) {
						return false;
					}
					$post_types = explode( ';', $master_password->post_types );

					return in_array( $post_type, $post_types, true );
				}
			);
		}

		/**
		 * Check master cookie is exist.
		 *
		 * @return bool True if master cookie is exist.
		 */
		public function check_master_cookies_are_exist() {
			if ( ! isset( $_COOKIE ) ) {
				return false;
			}

			// Check with cookie name which contains master password name.
			foreach ( $_COOKIE as $key => $value ) {
				if ( false !== strpos( $key, PPW_Constants::MASTER_COOKIE_NAME ) ) {
					return true;
				}
			}

			return false;
		}

		/**
		 * Get protection post type after convert it to select options.
		 *
		 * @return array Protection post types select option.
		 */
		public function get_protection_post_types_select() {
			$post_types_protection = apply_filters( PPW_Constants::HOOK_MASTER_PASSWORDS_VALID_POST_TYPES, array( 'post' ) );

			return array_reduce(
				ppw_core_get_all_post_types(),
				function ( $carry, $post_type ) use ( $post_types_protection ) {
					if ( isset( $post_type->name ) && in_array( $post_type->name, $post_types_protection, true ) ) {
						$carry[] = array(
							'key'   => $post_type->name,
							'value' => $post_type->label,
						);
					}

					return $carry;
				},
				array()
			);
		}

		/**
		 * Check logic and hide pages/posts protected in home, categories, search results, tags, authors, archive, feed.
		 *
		 * @param string   $where    The WHERE clause of the query.
		 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
		 *
		 * @return string
		 */
		public function handle_hide_post_protected( $where, $wp_query ) {
			$post_types    = apply_filters( PPW_Constants::HOOK_CUSTOM_POST_TYPE_HIDE_PROTECTED_POST, PPW_Constants::DEFAULT_POST_TYPE );
			$protected_ids = $this->custom_protected_ids();
			if ( empty( $protected_ids ) ) {
				return $where;
			}

			foreach ( $post_types as $post_type ) {
				$is_hide = ppw_core_get_setting_type_bool( PPW_Constants::HIDE_PROTECTED . $post_type );
				if ( ! $is_hide ) {
					continue;
				}
				$position_selected = ppw_core_get_setting_type_array( PPW_Constants::HIDE_SELECTED . $post_type );
				$where             = ppw_core_handle_logic_add_query( $position_selected, $protected_ids, $where, $post_type );
			}

			return $where;
		}

		/**
		 * Check logic and hide posts protected in recent post
		 *
		 * @param array $posts_args An array of arguments used to retrieve the recent posts.
		 *
		 * @return array
		 */
		public function handle_hide_post_protected_recent_post( $posts_args ) {
			$post_types    = apply_filters( PPW_Constants::HOOK_CUSTOM_POST_TYPE_RECENT_POST, array( 'post' ) );
			$protected_ids = $this->custom_protected_ids();
			if ( empty( $protected_ids ) ) {
				return $posts_args;
			}
			// phpcs:disable
			$old_post_not_in = isset( $posts_args['post__not_in'] ) ? $posts_args['post__not_in'] : array();
			foreach ( $post_types as $post_type ) {
				$is_hide = ppw_core_get_setting_type_bool( PPW_Constants::HIDE_PROTECTED . $post_type );
				if ( ! $is_hide ) {
					continue;
				}
				$position_selected = ppw_core_get_setting_type_array( PPW_Constants::HIDE_SELECTED . $post_type );
				if ( ! in_array( PPW_Constants::RECENT_POST, $position_selected, true ) ) {
					continue;
				}
				foreach ( $protected_ids as $id ) {
					if ( get_post_type( $id ) !== $post_type ) {
						continue;
					}
					$old_post_not_in[] = $id;
				}
			}
			$posts_args['post__not_in'] = $old_post_not_in;

			return $posts_args;
			
		}

		/**
		 * Check logic and hide posts protected in next and previous post
		 *
		 * @param string $where The WHERE clause of the query.
		 *
		 * @return string
		 */
		public function handle_hide_post_protected_next_and_previous( $where ) {
			$post_types    = apply_filters( PPW_Constants::HOOK_CUSTOM_POST_TYPE_NEXT_AND_PREVIOUS, array( 'post' ) );
			$protected_ids = $this->custom_protected_ids();
			if ( empty( $protected_ids ) ) {
				return $where;
			}

			foreach ( $post_types as $post_type ) {
				$is_hide = ppw_core_get_setting_type_bool( PPW_Constants::HIDE_PROTECTED . $post_type );
				if ( ! $is_hide ) {
					continue;
				}
				$position_selected = ppw_core_get_setting_type_array( PPW_Constants::HIDE_SELECTED . $post_type );
				if ( ! in_array( PPW_Constants::NEXT_PREVIOUS, $position_selected, true ) ) {
					continue;
				}
				foreach ( $protected_ids as $id ) {
					if ( get_post_type( $id ) !== $post_type ) {
						continue;
					}
					$where .= " AND p.ID != {$id}";
				}
			}

			return $where;
		}

		/**
		 * Check condition and exclude protected page in list page get by function get_pages
		 *
		 * @param array $pages List of pages to retrieve.
		 *
		 * @return array
		 */
		public function handle_hide_page_protected( $pages ) {
			$type      = 'page';
			$page_hide = ppw_core_get_setting_type_bool( PPW_Constants::HIDE_PROTECTED . $type );
			if ( ! $page_hide ) {
				return $pages;
			}
			$protected_ids = $this->custom_protected_ids();
			if ( empty( $protected_ids ) ) {
				return $pages;
			}
			$position_selected = ppw_core_get_setting_type_array( PPW_Constants::HIDE_SELECTED . $type );
			if ( ! in_array( PPW_Constants::EVERYWHERE_PAGE, $position_selected, true ) ) {
				return $pages;
			}
			foreach ( $protected_ids as $id ) {
				if ( 'page' !== get_post_type( $id ) ) {
					continue;
				}
				$pages = array_filter(
					$pages,
					function ( $page ) use ( $id ) {
						return $page->ID !== (int) $id;
					}
				);
			}

			return $pages;
		}

		/**
		 * Check condition and exclude page/post protected in Yoast SEO XML Sitemaps
		 *
		 * @param array $ids List page_id/post_id exclude in Yoast SEO XML Sitemaps.
		 *
		 * @return array
		 */
		public function handle_hide_page_protected_yoast_seo_sitemaps( $ids ) {
			$post_types    = apply_filters( PPW_Constants::HOOK_CUSTOM_POST_TYPE_HIDE_PROTECTED_POST, PPW_Constants::DEFAULT_POST_TYPE );
			$protected_ids = $this->custom_protected_ids();
			if ( empty( $protected_ids ) ) {
				return $ids;
			}

			foreach ( $post_types as $post_type ) {
				$is_hide = ppw_core_get_setting_type_bool( PPW_Constants::HIDE_PROTECTED . $post_type );
				if ( ! $is_hide ) {
					continue;
				}
				$position_selected = ppw_core_get_setting_type_array( PPW_Constants::HIDE_SELECTED . $post_type );
				// Push the post ID into list exclude from site map.
				$ids = ppw_core_list_posts_exclude_in_site_maps( $position_selected, $protected_ids, $ids, $post_type );
			}

			return $ids;
		}

		/**
		 * Get protected IDs.
		 * Declare hook for Pro custom protected IDs to handle hide protected posts.
		 *
		 * @return array
		 */
		public function custom_protected_ids() {
			$protected_ids = wp_cache_get( 'ppwp_protected_ids' );
			if ( false === $protected_ids ) {
				$protected_ids = apply_filters( PPW_Constants::HOOK_CUSTOM_POST_ID_HIDE_PROTECTED_POST, array() );
				if ( empty( $protected_ids ) ) {
					$protected_ids = $this->get_protected_post_ids();
				}
				wp_cache_set( 'ppwp_protected_ids', $protected_ids );
			}

			return $protected_ids;
		}

		/**
		 * Restore WP Post password.
		 */
		public function restore_wp_post_password() {
			$post_passwords = $this->passwords_repository->get_wp_post_passwords();
			if ( empty( $post_passwords ) ) {
				return;
			}

			foreach ( $post_passwords as $post_password ) {
				$post_id = wp_update_post(
					array(
						'ID'            => $post_password->post_id,
						'post_password' => $post_password->meta_value,
					)
				);
				if ( $post_id ) {
					delete_post_meta( $post_id, $post_password->meta_key, $post_password->meta_value );
				}
			}
		}

	}
}
