diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1637074..a4e79b83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,7 @@ jobs: - name: Install plugins working-directory: ${{ env.wp-directory }} - run: wp plugin install bbpress buddypress contact-form-7 ultimate-member wpforms-lite wpforo + run: wp plugin install bbpress buddypress ultimate-member wpforms-lite wpforo - name: Install plugins requiring 7.1 if: ${{ matrix.php-version >= '7.1' }} @@ -123,6 +123,11 @@ jobs: working-directory: ${{ env.wp-directory }} run: wp plugin install woocommerce + - name: Install plugins requiring 7.4 + if: ${{ matrix.php-version >= '7.4' }} + working-directory: ${{ env.wp-directory }} + run: wp plugin install contact-form-7 + - name: Run WP tests working-directory: ${{ env.wp-plugin-directory }} run: composer integration -- --env github-actions diff --git a/.tests/php/integration/CF7/CF7Test.php b/.tests/php/integration/CF7/CF7Test.php index ce0335fe..012d029f 100644 --- a/.tests/php/integration/CF7/CF7Test.php +++ b/.tests/php/integration/CF7/CF7Test.php @@ -23,6 +23,10 @@ /** * Test CF7 class. * + * CF7 requires PHP 7.4. + * + * @requires PHP >= 7.4 + * * @group cf7 */ class CF7Test extends HCaptchaPluginWPTestCase { diff --git a/.tests/php/integration/HCaptchaPluginWPTestCase.php b/.tests/php/integration/HCaptchaPluginWPTestCase.php index 873a9d57..141a8a29 100644 --- a/.tests/php/integration/HCaptchaPluginWPTestCase.php +++ b/.tests/php/integration/HCaptchaPluginWPTestCase.php @@ -52,8 +52,9 @@ public static function tearDownAfterClass(): void { // phpcs:ignore PHPCompatib */ public function setUp(): void { // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.voidFound $plugins_requiring_php = [ - '7.2' => [ 'woocommerce/woocommerce.php' ], '7.1' => [ 'ninja-forms/ninja-forms.php' ], + '7.3' => [ 'woocommerce/woocommerce.php' ], + '7.4' => [ 'contact-form-7/wp-contact-form-7.php' ], ]; foreach ( $plugins_requiring_php as $php_version => $plugins_requiring_php_version ) { diff --git a/.tests/php/unit/HCaptchaTestCase.php b/.tests/php/unit/HCaptchaTestCase.php index 9e0a4973..b1e17763 100644 --- a/.tests/php/unit/HCaptchaTestCase.php +++ b/.tests/php/unit/HCaptchaTestCase.php @@ -383,6 +383,12 @@ protected function get_test_general_form_fields() { 'text' => 'Check', 'section' => General::SECTION_KEYS, ], + 'reset_notifications' => [ + 'label' => 'Reset Notifications', + 'type' => 'button', + 'text' => 'Reset', + 'section' => General::SECTION_KEYS, + ], 'theme' => [ 'label' => 'Theme', 'type' => 'select', diff --git a/.tests/php/unit/Settings/GeneralTest.php b/.tests/php/unit/Settings/GeneralTest.php index 177cf033..fda7daca 100644 --- a/.tests/php/unit/Settings/GeneralTest.php +++ b/.tests/php/unit/Settings/GeneralTest.php @@ -12,6 +12,7 @@ namespace HCaptcha\Tests\Unit\Settings; +use HCaptcha\Admin\Notifications; use HCaptcha\Main; use HCaptcha\Settings\Abstracts\SettingsBase; use HCaptcha\Settings\General; @@ -124,15 +125,22 @@ public function test_init_form_fields() { * @dataProvider dp_test_setup_fields */ public function test_setup_fields( $mode ) { + $settings = Mockery::mock( Settings::class )->makePartial(); + $settings->shouldReceive( 'get_mode' )->andReturn( $mode ); + + $main = Mockery::mock( Main::class )->makePartial(); + $main->shouldReceive( 'settings' )->andReturn( $settings ); + $subject = Mockery::mock( General::class )->makePartial(); $subject->shouldAllowMockingProtectedMethods(); $subject->shouldReceive( 'is_options_screen' )->andReturn( true ); - $subject->shouldReceive( 'get' )->andReturn( $mode ); $this->set_protected_property( $subject, 'form_fields', $this->get_test_form_fields() ); WP_Mock::passthruFunction( 'register_setting' ); WP_Mock::passthruFunction( 'add_settings_field' ); + WP_Mock::userFunction( 'hcaptcha' )->with()->once()->andReturn( $main ); + $subject->setup_fields(); $form_fields = $this->get_protected_property( $subject, 'form_fields' ); @@ -187,6 +195,18 @@ public function test_setup_fields_not_on_options_screen() { public function test_section_callback( $section_id, $expected ) { $subject = Mockery::mock( General::class )->makePartial()->shouldAllowMockingProtectedMethods(); + $notifications = Mockery::mock( Notifications::class )->makePartial(); + + if ( General::SECTION_KEYS === $section_id ) { + $notifications->shouldReceive( 'show' )->once(); + $main = Mockery::mock( Main::class )->makePartial(); + $main->shouldReceive( 'notifications' )->andReturn( $notifications ); + + WP_Mock::userFunction( 'hcaptcha' )->with()->once()->andReturn( $main ); + } else { + WP_Mock::userFunction( 'hcaptcha' )->never(); + } + WP_Mock::passthruFunction( 'wp_kses_post' ); ob_start(); @@ -206,8 +226,6 @@ public function dp_test_section_callback() { '
- To use hCaptcha, please register here to get your site and secret keys.
${ message }
` ); + $( document ).trigger( 'wp-updates-notice-added' ); + + const $wpwrap = $( '#wpwrap' ); + const top = $wpwrap.position().top; + + $( 'html, body' ).animate( + { + scrollTop: $message.offset().top - top, + }, + 1000 + ); } function showSuccessMessage( response ) { @@ -115,8 +126,9 @@ const general = function( $ ) { clearMessage(); const data = { - action: HCaptchaGeneralObject.action, + action: HCaptchaGeneralObject.checkConfigAction, nonce: HCaptchaGeneralObject.nonce, + 'ajax-mode': $( 'select[name="hcaptcha_settings[mode]"]' ).val(), 'h-captcha-response': $( 'textarea[name="h-captcha-response"]' ).val(), }; diff --git a/assets/js/notifications.js b/assets/js/notifications.js new file mode 100644 index 00000000..7d6f54a3 --- /dev/null +++ b/assets/js/notifications.js @@ -0,0 +1,152 @@ +/* global jQuery, HCaptchaNotificationsObject */ + +/** + * @param HCaptchaNotificationsObject.ajaxUrl + * @param HCaptchaNotificationsObject.dismissNotificationAction + * @param HCaptchaNotificationsObject.dismissNotificationNonce + * @param HCaptchaNotificationsObject.resetNotificationAction + * @param HCaptchaNotificationsObject.resetNotificationNonce + */ + +/** + * Notifications logic. + * + * @param {Object} $ jQuery instance. + */ +const notifications = ( $ ) => { + const optionsSelector = 'form#hcaptcha-options'; + const sectionKeysSelector = 'h3.hcaptcha-section-keys'; + const notificationsSelector = 'div#hcaptcha-notifications'; + const notificationSelector = 'div.hcaptcha-notification'; + const dismissSelector = notificationsSelector + ' button.notice-dismiss'; + const navPrevSelector = '#hcaptcha-navigation .prev'; + const navNextSelector = '#hcaptcha-navigation .next'; + const navSelectors = navPrevSelector + ', ' + navNextSelector; + const buttonsSelector = '.hcaptcha-notification-buttons'; + const resetBtnSelector = 'button#reset_notifications'; + const footerSelector = '#hcaptcha-notifications-footer'; + let $notifications; + + const getVisibleNotificationIndex = function() { + $notifications = $( notificationSelector ); + + if ( ! $notifications.length ) { + return false; + } + + let index = 0; + + $notifications.each( function( i ) { + if ( $( this ).is( ':visible' ) ) { + index = i; + return false; + } + } ); + + return index; + }; + + const setNavStatus = function() { + const index = getVisibleNotificationIndex(); + + if ( index >= 0 ) { + $( navSelectors ).removeClass( 'disabled' ); + } else { + $( navSelectors ).addClass( 'disabled' ); + return; + } + + if ( index === 0 ) { + $( navPrevSelector ).addClass( 'disabled' ); + } + + if ( index === $notifications.length - 1 ) { + $( navNextSelector ).addClass( 'disabled' ); + } + }; + + const setButtons = function() { + const index = getVisibleNotificationIndex(); + + $( footerSelector ).find( buttonsSelector ).remove(); + + if ( index < 0 ) { + return; + } + + $( $notifications[ index ] ).find( buttonsSelector ).clone().removeClass( 'hidden' ).prependTo( footerSelector ); + }; + + $( optionsSelector ).on( 'click', dismissSelector, function( event ) { + const $notification = $( event.target ).closest( notificationSelector ); + + const data = { + action: HCaptchaNotificationsObject.dismissNotificationAction, + nonce: HCaptchaNotificationsObject.dismissNotificationNonce, + id: $notification.data( 'id' ), + }; + + $notification.remove(); + $( notificationSelector ).show(); + + setNavStatus(); + setButtons(); + + if ( $( notificationSelector ).length === 0 ) { + $( notificationsSelector ).remove(); + } + + // noinspection JSVoidFunctionReturnValueUsed,JSCheckFunctionSignatures + $.post( { + url: HCaptchaNotificationsObject.ajaxUrl, + data, + } ); + + return false; + } ); + + $( optionsSelector ).on( 'click', navSelectors, function( event ) { + let direction = 1; + + if ( $( event.target ).hasClass( 'prev' ) ) { + direction = -1; + } + + const index = getVisibleNotificationIndex(); + + const newIndex = index + direction; + + if ( index >= 0 && newIndex !== index && newIndex >= 0 && newIndex < $notifications.length ) { + $( $notifications[ index ] ).hide(); + $( $notifications[ newIndex ] ).show(); + setNavStatus(); + setButtons(); + } + } ); + + $( resetBtnSelector ).on( 'click', function() { + const data = { + action: HCaptchaNotificationsObject.resetNotificationAction, + nonce: HCaptchaNotificationsObject.resetNotificationNonce, + }; + + // noinspection JSVoidFunctionReturnValueUsed,JSCheckFunctionSignatures + $.post( { + url: HCaptchaNotificationsObject.ajaxUrl, + data, + } ).success( function( response ) { + if ( ! response.success ) { + return; + } + + $( notificationsSelector ).remove(); + $( response.data ).insertBefore( sectionKeysSelector ); + + setButtons(); + } ); + } ); + + setButtons(); +}; + +jQuery( document ).ready( notifications ); diff --git a/hcaptcha.php b/hcaptcha.php index 41712ff8..5d47abfa 100644 --- a/hcaptcha.php +++ b/hcaptcha.php @@ -10,7 +10,7 @@ * Plugin Name: hCaptcha for WordPress * Plugin URI: https://www.hcaptcha.com/ * Description: hCaptcha keeps out bots and spam while putting privacy first. It is a drop-in replacement for reCAPTCHA. - * Version: 3.0.1 + * Version: 3.1.0 * Requires at least: 5.0 * Requires PHP: 7.0 * Author: hCaptcha @@ -39,7 +39,7 @@ /** * Plugin version. */ -const HCAPTCHA_VERSION = '3.0.1'; +const HCAPTCHA_VERSION = '3.1.0'; /** * Path to the plugin dir. @@ -81,7 +81,7 @@ * * @return Main */ -function hcaptcha() { +function hcaptcha(): Main { static $hcaptcha; if ( ! $hcaptcha ) { diff --git a/readme.txt b/readme.txt index b9d8d54b..d7722f97 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: captcha, hcaptcha, recaptcha, spam, abuse Requires at least: 5.0 Tested up to: 6.3 Requires PHP: 7.0.0 -Stable tag: 3.0.1 +Stable tag: 3.1.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -482,6 +482,10 @@ Instructions for popular native integrations are below: == Changelog == += 3.1.0 = +* Added notification system. +* Fixed mode selection for sample hCaptcha on the General settings page. + = 3.0.1 = * Fixed error on Contact Form 7 validation. * Fixed checkboxes disabled status after activation of a plugin on the Integrations page. diff --git a/src/php/Admin/Notifications.php b/src/php/Admin/Notifications.php new file mode 100644 index 00000000..7f892ba7 --- /dev/null +++ b/src/php/Admin/Notifications.php @@ -0,0 +1,343 @@ +min_prefix = defined( 'SCRIPT_DEBUG' ) && constant( 'SCRIPT_DEBUG' ) ? '' : '.min'; + + $this->init_notifications(); + $this->init_hooks(); + } + + /** + * Init class hooks. + * + * @return void + */ + private function init_hooks() { + add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); + add_action( 'wp_ajax_' . self::DISMISS_NOTIFICATION_ACTION, [ $this, 'dismiss_notification' ] ); + add_action( 'wp_ajax_' . self::RESET_NOTIFICATIONS_ACTION, [ $this, 'reset_notifications' ] ); + } + + /** + * Init notifications. + * + * @return void + */ + private function init_notifications() { + $hcaptcha_url = 'https://www.hcaptcha.com/?r=wp&utm_source=wordpress&utm_medium=wpplugin&utm_campaign=sk'; + $register_url = 'https://www.hcaptcha.com/signup-interstitial/?r=wp&utm_source=wordpress&utm_medium=wpplugin&utm_campaign=sk'; + $pro_url = 'https://www.hcaptcha.com/pro?r=wp&utm_source=wordpress&utm_medium=wpplugin&utm_campaign=not'; + $dashboard_url = 'https://dashboard.hcaptcha.com/?r=wp&utm_source=wordpress&utm_medium=wpplugin&utm_campaign=not'; + + $this->notifications = [ + 'register' => [ + 'title' => __( 'Get your hCaptcha site keys', 'hcaptcha-for-forms-and-more' ), + 'message' => sprintf( + /* translators: 1: hCaptcha link, 2: register link. */ + __( 'To use %1$s, please register %2$s to get your site and secret keys.', 'hcaptcha-for-forms-and-more' ), + sprintf( + '%2$s', + $hcaptcha_url, + __( 'hCaptcha', 'hcaptcha-for-forms-and-more' ) + ), + sprintf( + '%2$s', + $register_url, + __( 'here', 'hcaptcha-for-forms-and-more' ) + ) + ), + 'button' => [ + 'url' => $register_url, + 'text' => __( 'Get site keys', 'hcaptcha-for-forms-and-more' ), + ], + ], + 'pro-free-trial' => [ + 'title' => __( 'Try Pro for free', 'hcaptcha-for-forms-and-more' ), + 'message' => sprintf( + /* translators: 1: hCaptcha Pro link, 2: dashboard link. */ + __( 'Want low friction and custom themes? %1$s is for you. %2$s, no credit card required.', 'hcaptcha-for-forms-and-more' ), + sprintf( + '%2$s', + $pro_url, + __( 'hCaptcha Pro', 'hcaptcha-for-forms-and-more' ) + ), + sprintf( + '%2$s', + $dashboard_url, + __( 'Start a free trial in your dashboard', 'hcaptcha-for-forms-and-more' ) + ) + ), + 'button' => [ + 'url' => $pro_url, + 'text' => __( 'Try Pro', 'hcaptcha-for-forms-and-more' ), + ], + ], + ]; + } + + /** + * Show notifications. + * + * @return void + */ + public function show() { + $user = wp_get_current_user(); + + if ( null === $user ) { + return; + } + + $dismissed = (array) get_user_meta( $user->ID, self::HCAPTCHA_DISMISSED_META_KEY, true ); + $notifications = array_diff_key( $this->notifications, array_flip( $dismissed ) ); + + if ( ! $notifications ) { + return; + } + + ?> +- hCaptcha, please register here to get your site and secret keys.', - 'hcaptcha-for-forms-and-more' - ) - ); - ?> -
notifications()->show(); $this->print_section_header( $arguments['id'], __( 'Keys', 'hcaptcha-for-forms-and-more' ) ); break; case self::SECTION_APPEARANCE: @@ -491,13 +489,13 @@ public function admin_enqueue_scripts() { self::OBJECT, [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'action' => self::CHECK_CONFIG_ACTION, + 'checkConfigAction' => self::CHECK_CONFIG_ACTION, 'nonce' => wp_create_nonce( self::CHECK_CONFIG_ACTION ), 'modeLive' => self::MODE_LIVE, 'modeTestPublisher' => self::MODE_TEST_PUBLISHER, 'modeTestEnterpriseSafeEndUser' => self::MODE_TEST_ENTERPRISE_SAFE_END_USER, 'modeTestEnterpriseBotDetected' => self::MODE_TEST_ENTERPRISE_BOT_DETECTED, - 'siteKey' => hcaptcha()->settings()->get_site_key(), + 'siteKey' => hcaptcha()->settings()->get( 'site_key' ), 'modeTestPublisherSiteKey' => self::MODE_TEST_PUBLISHER_SITE_KEY, 'modeTestEnterpriseSafeEndUserSiteKey' => self::MODE_TEST_ENTERPRISE_SAFE_END_USER_SITE_KEY, 'modeTestEnterpriseBotDetectedSiteKey' => self::MODE_TEST_ENTERPRISE_BOT_DETECTED_SITE_KEY, @@ -550,6 +548,7 @@ public function print_hcaptcha_field() { * Ajax action to check config. * * @return void + * @noinspection PhpUnusedParameterInspection */ public function check_config() { // Run a security check. @@ -562,17 +561,24 @@ public function check_config() { wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'hcaptcha-for-forms-and-more' ) ); } - $settings = hcaptcha()->settings(); + $ajax_mode = isset( $_POST['ajax-mode'] ) ? sanitize_text_field( wp_unslash( $_POST['ajax-mode'] ) ) : ''; - $params = [ + add_filter( + 'hcap_mode', + static function ( $mode ) use ( $ajax_mode ) { + return $ajax_mode; + } + ); + + $settings = hcaptcha()->settings(); + $params = [ 'host' => (string) wp_parse_url( site_url(), PHP_URL_HOST ), 'sitekey' => $settings->get_site_key(), 'sc' => 1, 'swa' => 1, 'spst' => 0, ]; - - $url = add_query_arg( $params, 'https://hcaptcha.com/checksiteconfig' ); + $url = add_query_arg( $params, 'https://hcaptcha.com/checksiteconfig' ); $raw_response = wp_remote_post( $url ); diff --git a/src/php/Settings/Settings.php b/src/php/Settings/Settings.php index a4c880dc..1c624a63 100644 --- a/src/php/Settings/Settings.php +++ b/src/php/Settings/Settings.php @@ -162,11 +162,10 @@ public function is_on( string $key ): bool { * @return array */ private function get_keys(): array { - $mode = $this->get( 'mode' ); // String concat is used for the PHP 5.6 compatibility. // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found - switch ( $mode ) { + switch ( $this->get_mode() ) { case General::MODE_LIVE: $site_key = $this->get( 'site_key' ); $secret_key = $this->get( 'secret_key' ); @@ -196,6 +195,20 @@ private function get_keys(): array { ]; } + /** + * Get mode. + * + * @return string + */ + public function get_mode(): string { + /** + * Filters the current operating mode to get relevant key pair. + * + * @param string $mode Current operating mode. + */ + return (string) apply_filters( 'hcap_mode', $this->get( 'mode' ) ); + } + /** * Get site key. * diff --git a/src/php/Settings/SystemInfo.php b/src/php/Settings/SystemInfo.php index 13601fb4..9fec8016 100644 --- a/src/php/Settings/SystemInfo.php +++ b/src/php/Settings/SystemInfo.php @@ -138,7 +138,7 @@ private function hcaptcha_info(): string { $data .= $this->data( 'Theme', $settings->get( 'theme' ) ); $data .= $this->data( 'Size', $settings->get( 'size' ) ); $data .= $this->data( 'Language', $settings->get( 'language' ) ); - $data .= $this->data( 'Mode', $settings->get( 'mode' ) ); + $data .= $this->data( 'Mode', $settings->get_mode() ); $data .= $this->data( 'Custom Themes', $this->is_on( 'custom_themes' ) ); $data .= $this->data( 'Config Params', $this->is_empty( $settings->get( 'config_params' ) ) ); $data .= $this->data( 'Turn Off When Logged In', $this->is_on( 'off_when_logged_in' ) );