From 634d142921f5e8ee2cbf146b12fb1e38398d4e6c Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 23 Oct 2024 13:26:30 +0300 Subject: [PATCH 1/3] procaptcha integration --- .../src/js/admin/form-editor/field-forms.js | 10 ++ .../src/js/admin/form-editor/field-manager.js | 9 ++ autoload.php | 2 + integrations/bootstrap.php | 2 + .../prosopo-procaptcha/admin-after.php | 138 ++++++++++++++++++ .../prosopo-procaptcha/admin-before.php | 11 ++ integrations/prosopo-procaptcha/bootstrap.php | 3 + .../class-procaptcha-integration.php | 95 ++++++++++++ .../prosopo-procaptcha/class-procaptcha.php | 83 +++++++++++ 9 files changed, 353 insertions(+) create mode 100644 integrations/prosopo-procaptcha/admin-after.php create mode 100644 integrations/prosopo-procaptcha/admin-before.php create mode 100644 integrations/prosopo-procaptcha/bootstrap.php create mode 100644 integrations/prosopo-procaptcha/class-procaptcha-integration.php create mode 100644 integrations/prosopo-procaptcha/class-procaptcha.php diff --git a/assets/src/js/admin/form-editor/field-forms.js b/assets/src/js/admin/form-editor/field-forms.js index 258cdc35..f5b224d6 100755 --- a/assets/src/js/admin/form-editor/field-forms.js +++ b/assets/src/js/admin/form-editor/field-forms.js @@ -93,4 +93,14 @@ forms.number = function (config) { ] } +forms.procaptcha = function (config) { + config.placeholder = '' + config.label = '' + config.wrap = false + config.required = false + config.type = 'hidden' + + return [] +} + module.exports = forms diff --git a/assets/src/js/admin/form-editor/field-manager.js b/assets/src/js/admin/form-editor/field-manager.js index 4905fdf1..2a0640b2 100755 --- a/assets/src/js/admin/form-editor/field-manager.js +++ b/assets/src/js/admin/form-editor/field-manager.js @@ -165,6 +165,15 @@ function registerCustomFields (lists) { title: i18n.submitButton }, true) + register(i18n.formFields, { + name: 'procaptcha', + type: 'procaptcha', + label: 'Procaptcha', + title: 'Procaptcha', + showLabel: false, + required: true + }, true) + // register lists choice field choices = {} for (const key in lists) { diff --git a/autoload.php b/autoload.php index f6866063..a50b2d22 100644 --- a/autoload.php +++ b/autoload.php @@ -66,6 +66,8 @@ 'MC4WP_Ninja_Forms_Integration' => __DIR__ . '/integrations/ninja-forms/class-ninja-forms.php', 'MC4WP_Ninja_Forms_V2_Integration' => __DIR__ . '/integrations/ninja-forms-2/class-ninja-forms.php', 'MC4WP_Plugin' => __DIR__ . '/includes/class-plugin.php', + 'MC4WP_Procaptcha_Integration' => __DIR__ . '/integrations/prosopo-procaptcha/class-procaptcha-integration.php', + 'MC4WP_Procaptcha' => __DIR__ . '/integrations/prosopo-procaptcha/class-procaptcha.php', 'MC4WP_Queue' => __DIR__ . '/includes/class-queue.php', 'MC4WP_Queue_Job' => __DIR__ . '/includes/class-queue-job.php', 'MC4WP_Registration_Form_Integration' => __DIR__ . '/integrations/wp-registration-form/class-registration-form.php', diff --git a/integrations/bootstrap.php b/integrations/bootstrap.php index 5fc5bacf..ee8a97e5 100755 --- a/integrations/bootstrap.php +++ b/integrations/bootstrap.php @@ -46,7 +46,9 @@ function mc4wp_admin_after_integration_settings(MC4WP_Integration $integration, mc4wp_register_integration('memberpress', 'MC4WP_MemberPress_Integration'); mc4wp_register_integration('affiliatewp', 'MC4WP_AffiliateWP_Integration'); mc4wp_register_integration('give', 'MC4WP_Give_Integration'); + require __DIR__ . '/woocommerce/woocommerce.php'; +require __DIR__ . '/prosopo-procaptcha/bootstrap.php'; mc4wp_register_integration('custom', 'MC4WP_Custom_Integration', true); require __DIR__ . '/ninja-forms/bootstrap.php'; diff --git a/integrations/prosopo-procaptcha/admin-after.php b/integrations/prosopo-procaptcha/admin-after.php new file mode 100644 index 00000000..617efdbb --- /dev/null +++ b/integrations/prosopo-procaptcha/admin-after.php @@ -0,0 +1,138 @@ + esc_html__('Light', 'mailchimp-for-wp'), + 'dark' => esc_html__('Dark', 'mailchimp-for-wp'), +); +$type_options = array( + 'frictionless' => esc_html__('Frictionless', 'mailchimp-for-wp'), + 'pow' => esc_html__('Pow', 'mailchimp-for-wp'), + 'image' => esc_html__('Image', 'mailchimp-for-wp'), +); + +?> + + +

+ +

+render_captcha_element(); +$procaptcha_api->render_captcha_js(array( + 'siteKey' => $site_key, + 'theme' => $theme, + 'type' => $type, +)); +?> + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ ', + '' + ); + ?> +

+
+ +
+ +
+ +
+ + + + diff --git a/integrations/prosopo-procaptcha/admin-before.php b/integrations/prosopo-procaptcha/admin-before.php new file mode 100644 index 00000000..9fb58eb3 --- /dev/null +++ b/integrations/prosopo-procaptcha/admin-before.php @@ -0,0 +1,11 @@ +', + '' +); diff --git a/integrations/prosopo-procaptcha/bootstrap.php b/integrations/prosopo-procaptcha/bootstrap.php new file mode 100644 index 00000000..e7ebda78 --- /dev/null +++ b/integrations/prosopo-procaptcha/bootstrap.php @@ -0,0 +1,3 @@ +options['enabled'] ?? ''; + + return '1' === $enabled_setting; + } + + /** + * @param string $html + * @return string + */ + public function inject_captcha_element($html) + { + $stub = ''; + + if (false === strpos($html, $stub)) { + return $html; + } + + $captcha_element = ''; + + // fixme + if (true === $this->is_enabled()) { + $captcha_element = 'test'; + } + + return str_replace($stub, $captcha_element, $html); + } + + /** + * @return bool + */ + public function is_installed() + { + return true; + } + + /** + * @return array + */ + public function get_ui_elements() + { + return array( + 'procaptcha_site_key', + 'procaptcha_secret_key', + ); + } + + /** + * @return array + */ + protected function get_default_options() + { + return array( + 'enabled' => 0, + 'css' => 0, + 'site_key' => '', + 'secret_key' => '', + 'theme' => 'light', + 'type' => 'frictionless', + ); + } +} diff --git a/integrations/prosopo-procaptcha/class-procaptcha.php b/integrations/prosopo-procaptcha/class-procaptcha.php new file mode 100644 index 00000000..caf279d5 --- /dev/null +++ b/integrations/prosopo-procaptcha/class-procaptcha.php @@ -0,0 +1,83 @@ +is_in_use = false; + } + + /** + * @return MC4WP_Procaptcha + */ + public static function get_instance() + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * @return void + */ + public function render_captcha_element() + { + $this->is_in_use = true; + + echo '
'; + } + + /** + * @param array $attributes + * @return void + */ + public function render_captcha_js($attributes) + { + echo ''; + ?> + + Date: Wed, 23 Oct 2024 16:00:09 +0300 Subject: [PATCH 2/3] procaptcha integration --- includes/admin/class-admin.php | 2 + includes/views/general-settings.php | 20 ++ .../prosopo-procaptcha/admin-after.php | 32 +- integrations/prosopo-procaptcha/bootstrap.php | 2 + .../class-procaptcha-integration.php | 38 +- .../prosopo-procaptcha/class-procaptcha.php | 333 +++++++++++++++++- 6 files changed, 364 insertions(+), 63 deletions(-) diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php index 632fd22a..9cab2178 100755 --- a/includes/admin/class-admin.php +++ b/includes/admin/class-admin.php @@ -433,6 +433,8 @@ public function show_generals_setting_page() } $obfuscated_api_key = mc4wp_obfuscate_string($api_key); + $is_procaptcha_configured = MC4WP_Procaptcha::get_instance()->is_enabled(); + require MC4WP_PLUGIN_DIR . '/includes/views/general-settings.php'; } diff --git a/includes/views/general-settings.php b/includes/views/general-settings.php index 7d086ad6..c7689fa9 100755 --- a/includes/views/general-settings.php +++ b/includes/views/general-settings.php @@ -65,6 +65,26 @@ ?> + + + + + + + + +

+ + +

+ + diff --git a/integrations/prosopo-procaptcha/admin-after.php b/integrations/prosopo-procaptcha/admin-after.php index 617efdbb..70fee6c1 100644 --- a/integrations/prosopo-procaptcha/admin-after.php +++ b/integrations/prosopo-procaptcha/admin-after.php @@ -5,11 +5,12 @@ $opts : array(); -$site_key = $opts['site_key'] ?? ''; -$secret_key = $opts['secret_key'] ?? ''; -$enabled = $opts['enabled'] ?? 0; -$theme = $opts['theme'] ?? ''; -$type = $opts['type'] ?? ''; +$site_key = $opts['site_key'] ?? ''; +$secret_key = $opts['secret_key'] ?? ''; +$enabled = $opts['enabled'] ?? '0'; +$display_for_authorized = $opts['display_for_authorized'] ?? '0'; +$theme = $opts['theme'] ?? ''; +$type = $opts['type'] ?? ''; $theme_options = array( 'light' => esc_html__('Light', 'mailchimp-for-wp'), @@ -32,12 +33,7 @@ render_captcha_element(); -$procaptcha_api->render_captcha_js(array( - 'siteKey' => $site_key, - 'theme' => $theme, - 'type' => $type, -)); +echo $procaptcha_api->print_captcha_element(true, true); ?> @@ -105,6 +101,20 @@ + + + +   + +

+ + diff --git a/integrations/prosopo-procaptcha/bootstrap.php b/integrations/prosopo-procaptcha/bootstrap.php index e7ebda78..d1c3c49f 100644 --- a/integrations/prosopo-procaptcha/bootstrap.php +++ b/integrations/prosopo-procaptcha/bootstrap.php @@ -1,3 +1,5 @@ set_hooks(); diff --git a/integrations/prosopo-procaptcha/class-procaptcha-integration.php b/integrations/prosopo-procaptcha/class-procaptcha-integration.php index ff836eca..b2a0304e 100644 --- a/integrations/prosopo-procaptcha/class-procaptcha-integration.php +++ b/integrations/prosopo-procaptcha/class-procaptcha-integration.php @@ -24,39 +24,6 @@ class MC4WP_Procaptcha_Integration extends MC4WP_Integration */ protected function add_hooks() { - add_action('mc4wp_form_content', array($this, 'inject_captcha_element')); - } - - /** - * @return bool - */ - protected function is_enabled() - { - $enabled_setting = $this->options['enabled'] ?? ''; - - return '1' === $enabled_setting; - } - - /** - * @param string $html - * @return string - */ - public function inject_captcha_element($html) - { - $stub = ''; - - if (false === strpos($html, $stub)) { - return $html; - } - - $captcha_element = ''; - - // fixme - if (true === $this->is_enabled()) { - $captcha_element = 'test'; - } - - return str_replace($stub, $captcha_element, $html); } /** @@ -84,12 +51,13 @@ public function get_ui_elements() protected function get_default_options() { return array( - 'enabled' => 0, - 'css' => 0, + 'enabled' => '0', + 'css' => '0', 'site_key' => '', 'secret_key' => '', 'theme' => 'light', 'type' => 'frictionless', + 'display_for_authorized' => '0', ); } } diff --git a/integrations/prosopo-procaptcha/class-procaptcha.php b/integrations/prosopo-procaptcha/class-procaptcha.php index caf279d5..6d478392 100644 --- a/integrations/prosopo-procaptcha/class-procaptcha.php +++ b/integrations/prosopo-procaptcha/class-procaptcha.php @@ -9,6 +9,10 @@ */ class MC4WP_Procaptcha { + const SCRIPT_URL = 'https://js.prosopo.io/js/procaptcha.bundle.js'; + const FORM_FIELD_NAME = 'procaptcha-response'; + const API_URL = 'https://api.prosopo.io/siteverify'; + /** * @var MC4WP_Procaptcha */ @@ -18,46 +22,116 @@ class MC4WP_Procaptcha * @var bool */ private $is_in_use; + /** + * @var bool + */ + private $is_enabled; + /** + * @var bool + */ + private $is_displayed_for_authorized; + /** + * @var string + */ + private $site_key; + /** + * @var string + */ + private $secret_key; + /** + * @var string + */ + private $theme; + /** + * @var string + */ + private $type; private function __construct() { - $this->is_in_use = false; + $this->is_in_use = false; + $this->is_enabled = false; + $this->is_displayed_for_authorized = false; + $this->site_key = ''; + $this->secret_key = ''; + $this->theme = ''; + $this->type = ''; + + $this->read_settings(); } /** - * @return MC4WP_Procaptcha + * @return void */ - public static function get_instance() + protected function read_settings() { - if (null === self::$instance) { - self::$instance = new self(); + $integrations = get_option('mc4wp_integrations', array()); + if ( + false === is_array($integrations) || + false === key_exists('prosopo-procaptcha', $integrations) || + false === is_array($integrations['prosopo-procaptcha']) + ) { + return; } - return self::$instance; + $settings = $integrations['prosopo-procaptcha']; + + $this->is_enabled = true === key_exists('enabled', $settings) && + '1' === $settings['enabled']; + $this->is_displayed_for_authorized = true === key_exists('display_for_authorized', $settings) && + '1' === $settings['display_for_authorized']; + $this->site_key = true === key_exists('site_key', $settings) && + true === is_string($settings['site_key']) ? + $settings['site_key'] : + ''; + $this->secret_key = true === key_exists('secret_key', $settings) && + true === is_string($settings['secret_key']) ? + $settings['secret_key'] : + ''; + $this->theme = true === key_exists('theme', $settings) && + true === is_string($settings['theme']) ? + $settings['theme'] : + ''; + $this->type = true === key_exists('type', $settings) && + true === is_string($settings['type']) ? + $settings['type'] : + ''; } /** - * @return void + * @return MC4WP_Procaptcha */ - public function render_captcha_element() + public static function get_instance() { - $this->is_in_use = true; + if (null === self::$instance) { + self::$instance = new self(); + } - echo '
'; + return self::$instance; } /** - * @param array $attributes * @return void */ - public function render_captcha_js($attributes) + protected function print_captcha_js() { - echo ''; + $attributes = array( + 'siteKey' => $this->site_key, + 'theme' => $this->theme, + 'captchaType' => $this->type, + ); ?> 'POST', + // limit waiting time to 20 seconds. + 'timeout' => 20, + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'body' => (string) wp_json_encode( + array( + 'secret' => $this->secret_key, + 'token' => $token, + ) + ), + ) + ); + + if (true === is_wp_error($response)) { + // something went wrong, maybe connection issue, but we still shouldn't allow the request. + return false; + } + + $body = wp_remote_retrieve_body($response); + $body = json_decode($body, true); + + $is_verified = $body['verified'] ?? false; + + return true === $is_verified; + } + + public function maybe_add_async_attribute(string $tag, string $handle, string $src): string + { + if ( + 'prosopo-procaptcha' !== $handle || + // make sure we don't make it twice if other Procaptcha integrations are present. + false !== strpos('type="module"', $tag) + ) { + return $tag; + } + + // for old WP versions. + $tag = str_replace(' type="text/javascript"', '', $tag); + + return str_replace('src', 'type="module" src', $tag); + } + + /** + * @return bool + */ + public function is_enabled() + { + return $this->is_enabled; + } + + /** + * @param bool $is_without_validation_element + * @param bool $is_forced_render E.g. if it's a preview. + * + * @return string + */ + public function print_captcha_element($is_without_validation_element = false, $is_forced_render=false) + { + if ( + false === $this->is_displayed_for_authorized && + true === is_user_logged_in() && + false === $is_forced_render + ) { + return ''; + } + + $this->is_in_use = true; + + $html = ''; + $html .= '
'; + + // The element is optional, e.g. should be missing on the settings page. + if (false === $is_without_validation_element) { + $html .= ''; + } + + $html .= '
'; + + return $html; + } + + /** + * @return void + */ + public function maybe_enqueue_captcha_js() + { + if (false === $this->is_in_use) { + return; + } + + // do not use wp_enqueue_module() because it doesn't work on the login screens. + wp_enqueue_script( + 'prosopo-procaptcha', + self::SCRIPT_URL, + array(), + null, + array( + 'in_footer' => true, + 'strategy' => 'defer', + ) + ); + + $this->print_captcha_js(); + } + + /** + * @param array $messages + * @return array + */ + public function register_error_message(array $messages) + { + $messages['procaptcha_required'] = 'Please verify that you are human.'; + + return $messages; + } + + /** + * @param string[] $error_keys + * @param MC4WP_Form $form + * + * @return string[] + */ + public function validate_form($error_keys, $form) + { + if ( + false === strpos($form->content, $this->get_field_stub()) || + (false === $this->is_displayed_for_authorized && true === is_user_logged_in()) || + true === $this->is_human_made_request() + ) { + return $error_keys; + } + + $error_keys[] = 'procaptcha_required'; + + return $error_keys; + } + + /** + * @param string $html + * @return string + */ + public function inject_captcha_element($html) + { + $stub = $this->get_field_stub(); + + if (false === strpos($html, $stub)) { + return $html; + } + + $captcha_element = $this->print_captcha_element(); + + return str_replace($stub, $captcha_element, $html); + } + + /** + * @return string + */ + protected function get_field_stub() + { + return ''; + } + + public function set_hooks(): void + { + if (false === $this->is_enabled) { + return; + } + + add_filter('mc4wp_form_messages', array($this, 'register_error_message')); + add_action('mc4wp_form_content', array($this, 'inject_captcha_element')); + add_filter('mc4wp_form_errors', array($this, 'validate_form'), 10, 2); + + add_filter('script_loader_tag', array($this, 'maybe_add_async_attribute'), 10, 3); + + $hook = true === is_admin() ? + 'admin_print_footer_scripts' : + 'wp_print_footer_scripts'; + + // priority must be less than 10, to make sure the wp_enqueue_script still has effect. + add_action($hook, array($this, 'maybe_enqueue_captcha_js'), 9); + } } From a283f526094a274f40ec67e8c0b5c27a40ac4752 Mon Sep 17 00:00:00 2001 From: Maxim Akimov Date: Wed, 23 Oct 2024 16:29:31 +0300 Subject: [PATCH 3/3] procaptcha integration --- includes/views/general-settings.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/includes/views/general-settings.php b/includes/views/general-settings.php index c7689fa9..fd304935 100755 --- a/includes/views/general-settings.php +++ b/includes/views/general-settings.php @@ -78,10 +78,24 @@ +

- - + tag, %2$s is the closing tag, %3$s is the opening tag, %4$s is the closing tag. + esc_html__( + 'Click %1$s here %2$s to configure %3$s Procaptcha%4$s, privacy-friendly and GDPR-compliant anti-bot protection.', + 'mailchimp-for-wp' + ), + '', + '', + '', + '' + ); + ?>