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() { '

General

-

- To use hCaptcha, please register here to get your site and secret keys.

Keys

', ], @@ -244,7 +262,7 @@ public function test_admin_enqueue_scripts() { $site_key = 'some key'; $settings = Mockery::mock( Settings::class )->makePartial(); - $settings->shouldReceive( 'get_site_key' )->andReturn( $site_key ); + $settings->shouldReceive( 'get' )->with( 'site_key' )->andReturn( $site_key ); $main = Mockery::mock( Main::class )->makePartial(); $main->shouldReceive( 'settings' )->andReturn( $settings ); @@ -297,7 +315,7 @@ static function ( $name ) use ( $plugin_url, $plugin_version ) { General::OBJECT, [ 'ajaxUrl' => $ajax_url, - 'action' => General::CHECK_CONFIG_ACTION, + 'checkConfigAction' => General::CHECK_CONFIG_ACTION, 'nonce' => $nonce, 'modeLive' => General::MODE_LIVE, 'modeTestPublisher' => General::MODE_TEST_PUBLISHER, diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png deleted file mode 100644 index 385ec141..00000000 Binary files a/.wordpress-org/icon-128x128.png and /dev/null differ diff --git a/.wordpress-org/icon.svg b/.wordpress-org/icon.svg new file mode 100644 index 00000000..e07eb713 --- /dev/null +++ b/.wordpress-org/icon.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/css/general.css b/assets/css/general.css index cf960c31..eaac9add 100644 --- a/assets/css/general.css +++ b/assets/css/general.css @@ -1,3 +1,9 @@ +#hcaptcha-message, +#setting-error-settings_updated { + max-width: 760px; + box-sizing: border-box; +} + #hcaptcha-options table tbody { background: #fff; } @@ -20,11 +26,38 @@ .hcaptcha-section-keys + table tbody { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(4, 1fr); + grid-template-areas: + "site-key site-key secret-key secret-key" + "sample-hcaptcha sample-hcaptcha check-config reset-notifications"; gap: 10px 20px; padding: 15px 20px; } +.hcaptcha-section-keys + table tbody th { + width: auto; +} + +.hcaptcha-section-keys + table tbody tr.hcaptcha-general-site-key { + grid-area: site-key; +} + +.hcaptcha-section-keys + table tbody tr.hcaptcha-general-secret-key { + grid-area: secret-key; +} + +.hcaptcha-section-keys + table tbody tr.hcaptcha-general-sample-hcaptcha { + grid-area: sample-hcaptcha; +} + +.hcaptcha-section-keys + table tbody tr.hcaptcha-general-check-config { + grid-area: check-config; +} + +.hcaptcha-section-keys + table tbody tr.hcaptcha-reset-notifications { + grid-area: reset-notifications; +} + .hcaptcha-section-keys + table tbody tr th { padding: 0 0 10px 0; } @@ -163,6 +196,14 @@ width: auto; } + .hcaptcha-section-keys + table tbody { + grid-template-areas: unset; + } + + .hcaptcha-section-keys + table tbody tr { + grid-area: unset !important; + } + .hcaptcha-section-other + table tbody { grid-template-areas: unset; } diff --git a/assets/css/notifications.css b/assets/css/notifications.css new file mode 100644 index 00000000..b65832fc --- /dev/null +++ b/assets/css/notifications.css @@ -0,0 +1,95 @@ +#hcaptcha-notifications { + margin: 5px 0 15px; + background: #fff; + padding: 15px 20px; + max-width: 760px; + box-sizing: border-box; +} + +#hcaptcha-notifications-header { + display: flex; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #c3c4c7; + line-height: 1.3; + font-size: 14px; + font-weight: 600; + justify-content: flex-start; + align-items: center; + column-gap: 3px; +} + +#hcaptcha-notifications-header::before { + content: ''; + background-image: url('../images/notification-bell.svg'); + background-size: cover; + position: relative; + width: 30px; + height: 30px; +} + +#hcaptcha-notifications-footer { + display: flex; + justify-content: space-between; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #c3c4c7; +} + +#hcaptcha-notifications .hcaptcha-notification { + margin: 0; +} + +.hcaptcha-notification .hcaptcha-notification-title { + margin: 7px 0; + padding: 2px; + line-height: 1.3; + font-weight: 600; +} + +.hcaptcha-notification { + display: none; +} + +#hcaptcha-notifications-header + .hcaptcha-notification { + display: block; +} + +#hcaptcha-navigation { + display: flex; + justify-content: flex-end; + column-gap: 3px; +} + +#hcaptcha-navigation a { + width: 30px; + height: 30px; + border: 1px solid #2271b1; + border-radius: 3px; + background: #f6f7f7; + font-size: 16px; + line-height: 25px; + text-align: center; + cursor: pointer; + box-sizing: border-box; +} + +#hcaptcha-navigation a.disabled { + color: #a7aaad; + border-color: #dcdcde; + background: #f6f7f7; + cursor: default; +} + +#hcaptcha-navigation a.prev::after, +#hcaptcha-navigation a.next::after { + font-size: 12px; +} + +#hcaptcha-navigation a.prev::after { + content: '<' +} + +#hcaptcha-navigation a.next::after { + content: '>'; +} diff --git a/assets/images/notification-bell.svg b/assets/images/notification-bell.svg new file mode 100644 index 00000000..8838c2b4 --- /dev/null +++ b/assets/images/notification-bell.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/js/general.js b/assets/js/general.js index 6dce1d71..d464006f 100644 --- a/assets/js/general.js +++ b/assets/js/general.js @@ -2,7 +2,7 @@ /** * @param HCaptchaGeneralObject.ajaxUrl - * @param HCaptchaGeneralObject.action + * @param HCaptchaGeneralObject.checkConfigAction * @param HCaptchaGeneralObject.nonce * @param HCaptchaGeneralObject.modeLive * @param HCaptchaGeneralObject.modeTestPublisher @@ -42,9 +42,20 @@ const general = function( $ ) { function showMessage( message, msgClass ) { $message.removeClass(); - $message.addClass( msgClass + ' notice settings-error is-dismissible' ); + $message.addClass( msgClass + ' notice is-dismissible' ); $message.html( `

${ 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; + } + + ?> +
+
+ +
+ $notification ) { + if ( array_key_exists( $id, $dismissed ) ) { + continue; + } + + $title = $notification['title'] ?: ''; + $message = $notification['message'] ?? ''; + $button_url = $notification['button']['url'] ?? ''; + $button_text = $notification['button']['text'] ?? ''; + $button = ''; + + if ( $button_url && $button_text ) { + ob_start(); + ?> + + +
+
+ +
+

+ +
+ + +
+ min_prefix.js", + [ 'jquery' ], + constant( 'HCAPTCHA_VERSION' ), + true + ); + + wp_localize_script( + self::HANDLE, + self::OBJECT, + [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'dismissNotificationAction' => self::DISMISS_NOTIFICATION_ACTION, + 'dismissNotificationNonce' => wp_create_nonce( self::DISMISS_NOTIFICATION_ACTION ), + 'resetNotificationAction' => self::RESET_NOTIFICATIONS_ACTION, + 'resetNotificationNonce' => wp_create_nonce( self::RESET_NOTIFICATIONS_ACTION ), + ] + ); + + wp_enqueue_style( + self::HANDLE, + constant( 'HCAPTCHA_URL' ) . "/assets/css/notifications$this->min_prefix.css", + [], + constant( 'HCAPTCHA_VERSION' ) + ); + } + + /** + * Ajax action to dismiss notification. + * + * @return void + */ + public function dismiss_notification() { + // Run a security check. + if ( ! check_ajax_referer( self::DISMISS_NOTIFICATION_ACTION, 'nonce', false ) ) { + wp_send_json_error( esc_html__( 'Your session has expired. Please reload the page.', 'hcaptcha-for-forms-and-more' ) ); + } + + // Check for permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'hcaptcha-for-forms-and-more' ) ); + } + + $id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : ''; + + if ( ! $this->update_dismissed( $id ) ) { + wp_send_json_error(); + } + + wp_send_json_success(); + } + + /** + * Update dismissed notifications. + * + * @param string $id Notification id. + * + * @return bool + */ + private function update_dismissed( string $id ): bool { + if ( ! $id ) { + return false; + } + + $user = wp_get_current_user(); + + if ( ! $user ) { + return false; + } + + $dismissed = get_user_meta( $user->ID, self::HCAPTCHA_DISMISSED_META_KEY, true ); + $dismissed = $dismissed ?: []; + + if ( in_array( $id, $dismissed, true ) ) { + return false; + } + + $dismissed[] = $id; + + $result = update_user_meta( $user->ID, self::HCAPTCHA_DISMISSED_META_KEY, $dismissed ); + + if ( ! $result ) { + return false; + } + + return true; + } + + /** + * Ajax action to reset notifications. + * + * @return void + */ + public function reset_notifications() { + // Run a security check. + if ( ! check_ajax_referer( self::RESET_NOTIFICATIONS_ACTION, 'nonce', false ) ) { + wp_send_json_error( esc_html__( 'Your session has expired. Please reload the page.', 'hcaptcha-for-forms-and-more' ) ); + } + + // Check for permissions. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'hcaptcha-for-forms-and-more' ) ); + } + + if ( ! $this->remove_dismissed() ) { + wp_send_json_error(); + } + + ob_start(); + $this->show(); + + wp_send_json_success( wp_kses_post( ob_get_clean() ) ); + } + + /** + * Remove dismissed status for all notifications. + * + * @return bool + */ + private function remove_dismissed() { + $user = wp_get_current_user(); + + if ( ! $user ) { + return false; + } + + return delete_user_meta( $user->ID, self::HCAPTCHA_DISMISSED_META_KEY ); + } +} diff --git a/src/php/Main.php b/src/php/Main.php index e0883d2c..c27d503d 100644 --- a/src/php/Main.php +++ b/src/php/Main.php @@ -13,6 +13,7 @@ namespace HCaptcha; use Automattic\WooCommerce\Utilities\FeaturesUtil; +use HCaptcha\Admin\Notifications; use HCaptcha\AutoVerify\AutoVerify; use HCaptcha\CF7\CF7; use HCaptcha\DelayedScript\DelayedScript; @@ -22,7 +23,6 @@ use HCaptcha\Jetpack\JetpackForm; use HCaptcha\Migrations\Migrations; use HCaptcha\NF\NF; -use HCaptcha\Otter; use HCaptcha\Quform\Quform; use HCaptcha\Sendinblue\Sendinblue; use HCaptcha\Settings\General; @@ -89,6 +89,13 @@ class Main { */ private $settings; + /** + * Notifications class instance. + * + * @var Notifications + */ + private $notifications; + /** * Instance of AutoVerify. * @@ -138,6 +145,9 @@ public function init() { * @return void */ public function init_hooks() { + $this->notifications = new Notifications(); + $this->notifications->init(); + $this->settings = new Settings( [ 'hCaptcha' => [ @@ -184,6 +194,15 @@ public function get( string $class ) { return $this->loaded_classes[ $class ] ?? null; } + /** + * Get Notifications instance. + * + * @return Notifications + */ + public function notifications(): Notifications { + return $this->notifications; + } + /** * Get Settings instance. * diff --git a/src/php/Settings/General.php b/src/php/Settings/General.php index 3393c6c3..0186075d 100644 --- a/src/php/Settings/General.php +++ b/src/php/Settings/General.php @@ -145,6 +145,12 @@ public function init_form_fields() { 'text' => __( 'Check', 'hcaptcha-for-forms-and-more' ), 'section' => self::SECTION_KEYS, ], + 'reset_notifications' => [ + 'label' => __( 'Reset Notifications', 'hcaptcha-for-forms-and-more' ), + 'type' => 'button', + 'text' => __( 'Reset', 'hcaptcha-for-forms-and-more' ), + 'section' => self::SECTION_KEYS, + ], 'theme' => [ 'label' => __( 'Theme', 'hcaptcha-for-forms-and-more' ), 'type' => 'select', @@ -410,7 +416,8 @@ public function setup_fields() { return; } - $mode = $this->get( 'mode' ); + // In Settings, a filter applied for mode. + $mode = hcaptcha()->settings()->get_mode(); if ( self::MODE_LIVE !== $mode ) { $this->form_fields['site_key']['disabled'] = true; @@ -433,17 +440,8 @@ public function section_callback( array $arguments ) { page_title() ); ?>
-

- 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' ) );