567 lines
14 KiB
PHP
567 lines
14 KiB
PHP
<?php
|
|
|
|
/**
|
|
*
|
|
* Class PPW_Recaptcha
|
|
*/
|
|
class PPW_Recaptcha {
|
|
const TYPE_PARAM = 'ppwp_type';
|
|
const TYPE_VALUE = 'recaptcha';
|
|
|
|
const RECAPTCHA_V3_TYPE = 'recaptcha_v3';
|
|
const RECAPTCHA_V2_CHECKBOX_TYPE = 'recaptcha_v2_checkbox';
|
|
const RECAPTCHA_V2_INVISIBLE_TYPE = 'recaptcha_v2_invisible';
|
|
const SINGLE_PASSWORD = 'single';
|
|
const PCP_PASSWORD = 'pcp';
|
|
const SITEWIDE_PASSWORD = 'sitewide';
|
|
|
|
private $show_message = false;
|
|
|
|
/**
|
|
* @var PPW_Recaptcha
|
|
*/
|
|
protected static $instance;
|
|
|
|
/**
|
|
* @return PPW_Recaptcha
|
|
*/
|
|
public static function get_instance() {
|
|
if ( null == self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Recaptcha error message.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_error_message() {
|
|
$message = get_theme_mod( 'ppwp_form_error_recaptcha_message_text', PPW_Constants::DEFAULT_ERROR_RECAPTCHA_MESSAGE );
|
|
$message = wp_kses_post( $message );
|
|
|
|
return _x( $message, PPW_Constants::CONTEXT_PASSWORD_FORM, 'password-protect-page' );
|
|
}
|
|
|
|
/**
|
|
* Register hooks.
|
|
* @since 1.0.0
|
|
*/
|
|
public function register() {
|
|
add_filter( 'ppwp_customize_ppf', array( $this, 'maybe_customize_error_message' ), 25 );
|
|
add_filter( 'ppwp_ppf_redirect_url', array( $this, 'maybe_add_blocked_message' ), 20, 2 );
|
|
add_filter( 'ppwp_ppf_referrer_url', array( $this, 'maybe_remove_recaptcha_query' ), 10, 2 );
|
|
add_filter( 'ppwpea_recaptcha_v2_site_key', array( $this, 'get_ppwpea_recaptcha_v2_api_key' ), 10 );
|
|
add_filter( 'ppwpea_recaptcha_v2_secret', array( $this, 'get_ppwpea_recaptcha_v2_api_secret' ), 10 );
|
|
add_action( 'wp_footer', array( $this, 'load_js_in_footer' ), 10 );
|
|
add_action( 'ppw_custom_footer_form_entire_site', array( $this, 'maybe_load_sitewide_recaptcha_js' ), 10 );
|
|
add_action( 'ppw_sitewide_above_submit_button', array( $this, 'maybe_add_recaptcha_input_below_sitewide_form' ), 10 );
|
|
add_action( 'ppw_sitewide_custom_internal_css', array( $this, 'customize_sitewide_css' ), 10 );
|
|
add_filter( 'ppw_sitewide_valid_password', array( $this, 'validate_sitewide_password' ), 10 );
|
|
}
|
|
|
|
/**
|
|
* Remove blocked query if user enter right password.
|
|
*
|
|
* @param string $referrer_url Referrer URL.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function maybe_remove_recaptcha_query( $referrer_url ) {
|
|
if ( ! $this->using_single_recaptcha() ) {
|
|
return $referrer_url;
|
|
}
|
|
|
|
if ( $this->has_recaptcha_parameter( $referrer_url ) ) {
|
|
$referrer_url = add_query_arg( self::TYPE_PARAM, false, $referrer_url );
|
|
}
|
|
|
|
return $referrer_url;
|
|
}
|
|
|
|
public function using_recaptcha() {
|
|
return ppw_core_get_setting_type_bool_by_option_name( PPW_Constants::USING_RECAPTCHA, PPW_Constants::EXTERNAL_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Add blocked message if user turn on option.
|
|
*
|
|
* @param string $redirect_url Redirect URL.
|
|
* @param array $params Parameters.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function maybe_add_blocked_message( $redirect_url, $params ) {
|
|
if ( ! $this->using_single_recaptcha() ) {
|
|
return $redirect_url;
|
|
}
|
|
if ( $params['is_valid'] ) {
|
|
return $redirect_url;
|
|
}
|
|
|
|
if ( ! $this->show_message ) {
|
|
// Remove blocked parameter if URL has it.
|
|
if ( $this->has_recaptcha_parameter( $redirect_url ) ) {
|
|
$redirect_url = add_query_arg( self::TYPE_PARAM, false, $redirect_url );
|
|
}
|
|
|
|
return $redirect_url;
|
|
}
|
|
|
|
$redirect_url = add_query_arg( self::TYPE_PARAM, self::TYPE_VALUE, $redirect_url );
|
|
|
|
return $redirect_url;
|
|
}
|
|
|
|
/**
|
|
* Has recaptcha parameter on URL.
|
|
*
|
|
* @param string $url $url URL.
|
|
* @param string $query_value Query value.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function has_recaptcha_parameter( $url = '', $query_value = self::TYPE_VALUE ) {
|
|
if ( empty( $url ) ) {
|
|
$query_params = ppw_core_get_query_param();
|
|
} else {
|
|
$query_params = ppw_core_get_param_in_url( $url );
|
|
}
|
|
|
|
if ( ! isset( $query_params[ self::TYPE_PARAM ] ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $query_value === $query_params[ self::TYPE_PARAM ];
|
|
}
|
|
|
|
/**
|
|
* Customize error message.
|
|
*
|
|
* @param array $params Parameters.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function maybe_customize_error_message( $params ) {
|
|
if ( ! $this->using_single_recaptcha() ) {
|
|
return $params;
|
|
}
|
|
|
|
if ( $this->has_recaptcha_parameter() ) {
|
|
$message = $this->get_error_message();
|
|
$params['error_msg'] = apply_filters( 'ppw_recaptcha_error_message', $message, $params );
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
/**
|
|
* Validate recaptcha.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_valid_recaptcha() {
|
|
$_post = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- We no need to handle nonce verification here because already handle on parent function.
|
|
if ( ! isset( $_post['g-recaptcha-response'] ) ) {
|
|
$this->show_message = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
$result = $this->verify_recaptcha( $_post['g-recaptcha-response'] );
|
|
if ( ! $result['success'] ) {
|
|
$this->show_message = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get limit score.
|
|
*
|
|
* @return double
|
|
*/
|
|
public function get_limit_score() {
|
|
$score = ppw_core_get_settings_by_option_name( PPW_Constants::RECAPTCHA_SCORE, PPW_Constants::EXTERNAL_OPTIONS );
|
|
if ( is_null( $score ) ) {
|
|
return (double) 0.5;
|
|
}
|
|
|
|
return (double) $score;
|
|
}
|
|
|
|
/**
|
|
* Get setting api key of recaptcha with current type.
|
|
*
|
|
* @param string $type Recaptcha type.
|
|
* @param string $default Default value.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_setting_api_key( $type = '', $default = '' ) {
|
|
if ( empty( $type ) ) {
|
|
$type = $this->get_recaptcha_type();
|
|
}
|
|
|
|
switch ( $type ) {
|
|
case self::RECAPTCHA_V3_TYPE:
|
|
return $this->get_recaptcha_v3_api_key();
|
|
case self::RECAPTCHA_V2_CHECKBOX_TYPE:
|
|
return $this->get_recaptcha_v2_api_key();
|
|
default:
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get setting api secret of recaptcha with current type.
|
|
*
|
|
* @param string $type Recaptcha type.
|
|
* @param string $default Default value.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_setting_api_secret( $type = '', $default = '' ) {
|
|
if ( empty( $type ) ) {
|
|
$type = $this->get_recaptcha_type();
|
|
}
|
|
|
|
switch ( $type ) {
|
|
case self::RECAPTCHA_V3_TYPE:
|
|
return $this->get_recaptcha_v3_api_secret();
|
|
case self::RECAPTCHA_V2_CHECKBOX_TYPE:
|
|
return $this->get_recaptcha_v2_api_secret();
|
|
default:
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha v3 API key.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_v3_api_key() {
|
|
return ppw_core_get_setting_type_string_by_option_name( PPW_Constants::RECAPTCHA_API_KEY, PPW_Constants::EXTERNAL_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha v3 API secret.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_v3_api_secret() {
|
|
return ppw_core_get_setting_type_string_by_option_name( PPW_Constants::RECAPTCHA_API_SECRET, PPW_Constants::EXTERNAL_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha v2 API key.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_v2_api_key() {
|
|
return ppw_core_get_setting_type_string_by_option_name( PPW_Constants::RECAPTCHA_V2_CHECKBOX_API_KEY, PPW_Constants::EXTERNAL_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha v2 API secret.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_v2_api_secret() {
|
|
return ppw_core_get_setting_type_string_by_option_name( PPW_Constants::RECAPTCHA_V2_CHECKBOX_API_SECRET, PPW_Constants::EXTERNAL_OPTIONS );
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha type.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_type() {
|
|
$recaptcha_type = ppw_core_get_setting_type_string_by_option_name( PPW_Constants::RECAPTCHA_TYPE, PPW_Constants::EXTERNAL_OPTIONS );
|
|
|
|
return $recaptcha_type ? $recaptcha_type : self::RECAPTCHA_V3_TYPE;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get password types selected.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function get_password_types() {
|
|
$password_types = ppw_core_get_setting_type_array_by_option_name( PPW_Constants::RECAPTCHA_PASSWORD_TYPES, PPW_Constants::EXTERNAL_OPTIONS );
|
|
|
|
return $password_types ? $password_types : array( 'single' );
|
|
}
|
|
|
|
/**
|
|
* Using single recaptcha
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function using_single_recaptcha() {
|
|
return $this->using_recaptcha() && in_array( self::SINGLE_PASSWORD, $this->get_password_types() );
|
|
}
|
|
|
|
/**
|
|
* Using sitewide recaptcha
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function using_sitewide_recaptcha() {
|
|
return $this->using_recaptcha() && in_array( self::SITEWIDE_PASSWORD, $this->get_password_types() );
|
|
}
|
|
|
|
/**
|
|
* Using sitewide recaptcha
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function using_pcp_recaptcha() {
|
|
return $this->using_recaptcha() && in_array( self::PCP_PASSWORD, $this->get_password_types() );
|
|
}
|
|
|
|
/**
|
|
* Load recaptcha v2 javascript.
|
|
*/
|
|
public function load_recaptcha_v2_js() {
|
|
ob_start();
|
|
?>
|
|
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
|
<?php
|
|
|
|
echo ob_get_clean(); // phpcs:ignore -- we cannot escape ob_start ob_get_clean(), there are no variable to escape in statement above
|
|
}
|
|
|
|
/**
|
|
* Load recaptcha v3 javascript.
|
|
*/
|
|
public function load_recaptcha_v3_js() {
|
|
$recaptcha_key = $this->get_recaptcha_v3_api_key();
|
|
|
|
ob_start();
|
|
?>
|
|
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo esc_attr( $recaptcha_key ); ?>"></script>
|
|
<script>
|
|
grecaptcha.ready(function () {
|
|
grecaptcha.execute('<?php echo esc_attr( $recaptcha_key ); ?>', {action: 'enter_password'}).then(function (token) {
|
|
var recaptchaResponse = document.getElementById('ppwRecaptchaResponse');
|
|
recaptchaResponse.value = token;
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
|
|
echo ob_get_clean(); // phpcs:ignore -- we already escape the $recaptcha_key above
|
|
}
|
|
|
|
/**
|
|
* Verify google recaptcha V3.
|
|
*
|
|
* @param string $recaptcha_response Recaptcha response.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function verify_recaptcha( $recaptcha_response ) {
|
|
$default = array(
|
|
'success' => false,
|
|
'message' => '',
|
|
);
|
|
if ( ! $recaptcha_response ) {
|
|
return $default;
|
|
}
|
|
$secret = $this->get_setting_api_secret();
|
|
if ( ! $secret ) {
|
|
return $default;
|
|
}
|
|
|
|
$response = wp_remote_post(
|
|
'https://www.google.com/recaptcha/api/siteverify',
|
|
array(
|
|
'method' => 'POST',
|
|
'timeout' => 45,
|
|
'redirection' => 5,
|
|
'httpversion' => '1.0',
|
|
'blocking' => true,
|
|
'headers' => array(),
|
|
'body' => array(
|
|
'secret' => $secret,
|
|
'response' => $recaptcha_response,
|
|
),
|
|
'cookies' => array(),
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $default;
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body( $response );
|
|
$body = json_decode( $body );
|
|
|
|
// Whether this request was a valid reCAPTCHA token for your site.
|
|
$success = isset( $body->success ) && $body->success;
|
|
|
|
$external = true;
|
|
if ( $this->get_recaptcha_type() === self::RECAPTCHA_V3_TYPE ) {
|
|
$limit_score = $this->get_limit_score();
|
|
|
|
// The score for this request (0.0 - 1.0) 1.0 is very likely a good interaction, 0.0 is very likely a bot.
|
|
$score = isset( $body->score ) ? (double) $body->score : 0;
|
|
$external = $score > $limit_score;
|
|
}
|
|
$default['success'] = $success && $external;
|
|
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Load PPWPEA api key.
|
|
*
|
|
* @param string $key Recaptcha API key.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_ppwpea_recaptcha_v2_api_key( $key ) {
|
|
return $this->get_recaptcha_v2_api_key();
|
|
}
|
|
|
|
/**
|
|
* Load PPWPEA api secret.
|
|
*
|
|
* @param string $secret Recaptcha API secret.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_ppwpea_recaptcha_v2_api_secret( $secret ) {
|
|
return $this->get_recaptcha_v2_api_secret();
|
|
}
|
|
|
|
public function maybe_load_sitewide_recaptcha_js() {
|
|
if ( ! $this->using_sitewide_recaptcha() ) {
|
|
return;
|
|
}
|
|
|
|
$this->add_recaptcha_to_head();
|
|
}
|
|
|
|
/**
|
|
* Add recaptcha to head.
|
|
*/
|
|
public function add_recaptcha_to_head() {
|
|
$recaptcha_type = $this->get_recaptcha_type();
|
|
switch ( $recaptcha_type ) {
|
|
case self::RECAPTCHA_V3_TYPE:
|
|
$this->load_recaptcha_v3_js();
|
|
break;
|
|
case self::RECAPTCHA_V2_CHECKBOX_TYPE:
|
|
case self::RECAPTCHA_V2_INVISIBLE_TYPE:
|
|
$this->load_recaptcha_v2_js();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add recaptcha input below sitewide form.
|
|
*/
|
|
public function maybe_add_recaptcha_input_below_sitewide_form() {
|
|
$recaptcha_input = $this->get_recaptcha_input();
|
|
if ( ! empty( $recaptcha_input ) ) {
|
|
echo $recaptcha_input;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recaptcha input.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_recaptcha_input() {
|
|
switch ( $this->get_recaptcha_type() ) {
|
|
case PPW_Recaptcha::RECAPTCHA_V2_CHECKBOX_TYPE:
|
|
$site_key = $this->get_recaptcha_v2_api_key();
|
|
|
|
return '<div class="ppw-recaptcha g-recaptcha" data-sitekey="' . $site_key . '"></div>';
|
|
default:
|
|
return '<input type="hidden" name="g-recaptcha-response" id="ppwRecaptchaResponse" />';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Customize sitewide css.
|
|
*/
|
|
public function customize_sitewide_css() {
|
|
if ( ! $this->using_sitewide_recaptcha() ) {
|
|
return;
|
|
}
|
|
|
|
?>
|
|
.g-recaptcha {
|
|
transform:scale(0.9);
|
|
transform-origin:0 0;
|
|
}
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Validate sitewide password form.
|
|
*
|
|
* @param $validated
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function validate_sitewide_password( $validated ) {
|
|
if ( ! $validated ) {
|
|
return $validated;
|
|
}
|
|
|
|
if ( ! $this->using_sitewide_recaptcha() ) {
|
|
return $validated;
|
|
}
|
|
|
|
if ( ! $this->is_valid_recaptcha() ) {
|
|
add_filter( 'ppw_sitewide_error_message', array( $this, 'get_sitewide_error_message' ) );
|
|
|
|
return false;
|
|
}
|
|
|
|
return $validated;
|
|
}
|
|
|
|
/**
|
|
* Get sitewide error message.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_sitewide_error_message() {
|
|
return __( 'Google reCAPTCHA verification failed, please try again later.', 'password-protect-page' );
|
|
}
|
|
|
|
|
|
/**
|
|
* Load JS in footer.
|
|
*/
|
|
public function load_js_in_footer() {
|
|
if ( ! $this->using_single_recaptcha() ) {
|
|
return;
|
|
}
|
|
$allowed = is_singular();
|
|
$allowed = apply_filters( 'ppw_recaptcha_allowed_to_load_script', $allowed );
|
|
if ( ! $allowed ) {
|
|
return;
|
|
}
|
|
$post_id = get_the_ID();
|
|
if ( ! $post_id || ! post_password_required( $post_id ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->add_recaptcha_to_head();
|
|
}
|
|
|
|
}
|