diff --git a/.tests/php/integration/GravityForms/FormTest.php b/.tests/php/integration/GravityForms/FormTest.php index 354b02ad..f46e606b 100644 --- a/.tests/php/integration/GravityForms/FormTest.php +++ b/.tests/php/integration/GravityForms/FormTest.php @@ -15,6 +15,7 @@ use HCaptcha\GravityForms\Base; use HCaptcha\GravityForms\Form; use HCaptcha\Tests\Integration\HCaptchaWPTestCase; +use Mockery; use ReflectionException; use tad\FunctionMocker\FunctionMocker; @@ -276,48 +277,98 @@ public function dp_test_verify(): array { * @return void */ public function test_verify_when_should_not_be_verified() { - $form_id = 23; - $target_page = "gform_target_page_number_$form_id"; + $form_id = 2; + $nested_form_id = 9; + $multipage_form_id = 3; + $source_page_name = "gform_source_page_number_$multipage_form_id"; + $target_page_name = "gform_target_page_number_$multipage_form_id"; $hcaptcha_field = (object) [ 'type' => 'hcaptcha', ]; - $form = [ + $nested_form_field = Mockery::mock( 'GP_Field_Nested_Form' ); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $nested_form_field->gpnfForm = $form_id; + $form_fields = [ 'id' => $form_id, 'fields' => [ $hcaptcha_field ], ]; - $validation_result = [ + $nested_form_fields = [ + 'id' => $nested_form_id, + 'fields' => [ $nested_form_field ], + ]; + $multipage_form_fields = [ + 'id' => $nested_form_id, + 'pagination' => [ + 'pages' => [ + 0 => [], + 1 => [], + ], + ], + ]; + $validation_result = [ 'is_valid' => true, 'form' => [], 'failed_validation_page' => 0, ]; - $context = 'form-submit'; + $context = 'form-submit'; $subject = new Form(); // The POST 'gform_submit' not set. self::assertSame( $validation_result, $subject->verify( $validation_result, $context ) ); - $_POST['gform_submit'] = $form_id; + // Nested form. + $_POST['gform_submit'] = $form_id; + $_POST['gpnf_parent_form_id'] = $nested_form_id; - // The POST 'gpnf_parent_form_id' is set. - $_POST['gpnf_parent_form_id'] = 5; + FunctionMocker::replace( + 'GFFormsModel::get_form_meta', + static function ( $id ) use ( + $form_id, + $form_fields, + $nested_form_id, + $nested_form_fields, + $multipage_form_id, + $multipage_form_fields + ) { + if ( $id === $form_id ) { + return $form_fields; + } + + if ( $id === $nested_form_id ) { + return $nested_form_fields; + } + + if ( $id === $multipage_form_id ) { + return $multipage_form_fields; + } + + return []; + } + ); self::assertSame( $validation_result, $subject->verify( $validation_result, $context ) ); + // Not a nested form. unset( $_POST['gpnf_parent_form_id'] ); + // Multipage form. + $_POST['gform_submit'] = $multipage_form_id; + // The POST target_page is set and not 0. - $_POST[ $target_page ] = 3; + $_POST[ $source_page_name ] = 1; + $_POST[ $target_page_name ] = 2; self::assertSame( $validation_result, $subject->verify( $validation_result, $context ) ); // The POST target_page is set and 0. - $_POST[ $target_page ] = 0; + $_POST[ $source_page_name ] = 2; + $_POST[ $target_page_name ] = 0; self::assertSame( $validation_result, $subject->verify( $validation_result, $context ) ); // The POST target_page is unset. - unset( $_POST[ $target_page ] ); + unset( $_POST[ $target_page_name ] ); self::assertSame( $validation_result, $subject->verify( $validation_result, $context ) ); } diff --git a/.tests/php/unit/HCaptchaTestCase.php b/.tests/php/unit/HCaptchaTestCase.php index 62f6fce8..3c88fc19 100644 --- a/.tests/php/unit/HCaptchaTestCase.php +++ b/.tests/php/unit/HCaptchaTestCase.php @@ -542,10 +542,19 @@ protected function get_test_general_form_fields(): array { 'type' => 'checkbox', 'section' => General::SECTION_APPEARANCE, 'options' => [ - 'on' => 'Force hCaptcha', + 'on' => 'Force', ], 'helper' => 'Force hCaptcha check before submit.', ], + 'menu_position' => [ + 'label' => 'Tabs Menu Under Settings', + 'type' => 'checkbox', + 'section' => 'appearance', + 'options' => [ + 'on' => 'Tabs', + ], + 'helper' => 'When on, the hCaptcha admin menu is placed under Settings.', + ], 'custom_themes' => [ 'label' => 'Custom Themes', 'type' => 'checkbox', diff --git a/assets/css/events.css b/assets/css/events.css index 05665594..8eec71f4 100644 --- a/assets/css/events.css +++ b/assets/css/events.css @@ -36,7 +36,6 @@ color: #f0f2f5; } - #hcaptcha-events-chart { background-color: #ffffff; position: relative; @@ -49,31 +48,12 @@ width: 220px; } -.hcaptcha-hide { - display: none; -} +@media (max-width: 600px) { + #hcaptcha-options #hcaptcha-events-wrap table tbody tr td { + padding: 3px 8px 3px 35%; + } -.hcaptcha-excerpt { - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.hcaptcha-excerpt:hover .hcaptcha-hide { - position: absolute; - color: #f0f2f5; - background: #5c6f8a; - z-index: 1; - display: block; - max-width: 300px; - width: max-content; - padding: 8px 10px; - top: 0; - left: 0; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1); - border-radius: 6px; - border: 1px solid #c3c4c7; - white-space: normal; - text-align: center; + #hcaptcha-options #hcaptcha-events-wrap table tbody tr td.column-primary { + padding: 3px 10px; + } } diff --git a/assets/css/forms.css b/assets/css/forms.css index 4aecb379..6fd85b6d 100644 --- a/assets/css/forms.css +++ b/assets/css/forms.css @@ -44,31 +44,12 @@ aspect-ratio: 3/1; } -.hcaptcha-hide { - display: none; -} - -.hcaptcha-excerpt { - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.hcaptcha-excerpt:hover .hcaptcha-hide { - position: absolute; - color: #f0f2f5; - background: #5c6f8a; - z-index: 1; - display: block; - max-width: 300px; - width: max-content; - padding: 8px 10px; - top: 0; - left: 0; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1); - border-radius: 6px; - border: 1px solid #c3c4c7; - white-space: normal; - text-align: center; +@media (max-width: 600px) { + #hcaptcha-options #hcaptcha-forms-wrap table tbody tr td { + padding: 3px 8px 3px 35%; + } + + #hcaptcha-options #hcaptcha-forms-wrap table tbody tr td.column-primary { + padding: 3px 10px; + } } diff --git a/assets/css/general.css b/assets/css/general.css index d720c9d0..775dc625 100644 --- a/assets/css/general.css +++ b/assets/css/general.css @@ -128,7 +128,7 @@ h3.closed .hcaptcha-section-header-toggle:after { grid-template-columns: repeat(4, 1fr); grid-template-areas: "theme size language mode" - "force force . ."; + "force force position position"; gap: 10px 20px; padding: 15px 20px; } @@ -153,6 +153,10 @@ h3.closed .hcaptcha-section-header-toggle:after { grid-area: force; } +.hcaptcha-section-appearance + table tbody tr.hcaptcha-general-menu-position { + grid-area: position; +} + .hcaptcha-section-appearance + table tbody tr th { padding: 0 0 10px 0; width: auto; @@ -162,6 +166,10 @@ h3.closed .hcaptcha-section-header-toggle:after { width: max-content; } +.hcaptcha-section-appearance + table tbody tr.hcaptcha-general-menu-position td { + width: max-content; +} + .hcaptcha-section-appearance + table tbody tr td select { width: 100%; } @@ -352,35 +360,73 @@ h3.closed .hcaptcha-section-header-toggle:after { } @media (max-width: 600px) { - .hcaptcha-general table tbody tr td { - width: auto; + .hcaptcha-section-keys + table tbody { + grid-template-columns: repeat(2, 1fr) !important; + grid-template-areas: + "site-key site-key" + "secret-key secret-key" + "sample-hcaptcha sample-hcaptcha" + "check-config reset-notifications"; } - .hcaptcha-section-keys + table tbody { - grid-template-areas: unset; + .hcaptcha-section-appearance + table tbody { + grid-template-columns: repeat(2, 1fr) !important; + grid-template-areas: + "theme size" + "language mode" + "force force" + "position position"; } - .hcaptcha-section-keys + table tbody tr { - grid-area: unset !important; + .hcaptcha-section-appearance + table tbody tr td { + width: unset !important; } - .hcaptcha-section-other + table tbody { - grid-template-areas: unset; + .hcaptcha-section-custom + table tbody { + grid-template-columns: repeat(2, 1fr) !important; + grid-template-areas: + "custom-themes custom-themes" + "custom-prop custom-value" + "config-params config-params"; } - .hcaptcha-section-other + table tbody tr { - grid-area: unset !important; + .hcaptcha-section-custom + table tbody tr td { + width: unset !important; + } + + .hcaptcha-section-enterprise + table tbody { + grid-template-columns: repeat(1, 1fr) !important; + } + + .hcaptcha-section-enterprise + table tbody tr td { + width: unset !important; + } + + .hcaptcha-section-other + table tbody { + grid-template-columns: repeat(1, 1fr) !important; + grid-template-areas: + "logged" + "recaptcha" + "network" + "whitelisted" + "login-limit" + "login-interval" + "delay"; } - .hcaptcha-section-other + table tbody tr.hcaptcha-general-whitelisted-ips td { - width: auto; + .hcaptcha-section-other + table tbody tr td { + width: unset !important; } .hcaptcha-section-statistics + table tbody { - grid-template-areas: unset; + grid-template-columns: repeat(1, 1fr); + grid-template-areas: + "statistics" + "collect-ip" + "collect-ua"; } - .hcaptcha-section-statistics + table tbody tr { - grid-area: unset !important; + .hcaptcha-section-statistics + table tbody tr td { + width: unset !important; } } diff --git a/assets/css/settings-base.css b/assets/css/settings-base.css index 02f55ccb..12e347ba 100644 --- a/assets/css/settings-base.css +++ b/assets/css/settings-base.css @@ -211,6 +211,35 @@ body.settings_page_hcaptcha { animation: blink 3s linear; } +.hcaptcha-hide { + display: none; +} + +.hcaptcha-excerpt { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hcaptcha-excerpt:hover .hcaptcha-hide { + position: absolute; + color: #f0f2f5; + background: #5c6f8a; + z-index: 1; + display: block; + max-width: 300px; + width: max-content; + padding: 8px 10px; + top: 0; + left: 0; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1); + border-radius: 6px; + border: 1px solid #c3c4c7; + white-space: normal; + text-align: center; +} + @media (max-width: 782px) { .hcaptcha-settings-tabs { margin: 10px -12px 0 -10px; diff --git a/assets/js/events.js b/assets/js/events.js index 690a001a..1bd90bf2 100644 --- a/assets/js/events.js +++ b/assets/js/events.js @@ -8,6 +8,7 @@ */ document.addEventListener( 'DOMContentLoaded', function() { const ctx = document.getElementById( 'eventsChart' ); + const aspectRatio = window.innerWidth > 600 ? 3 : 2; new Chart( ctx, { type: 'bar', @@ -28,7 +29,7 @@ document.addEventListener( 'DOMContentLoaded', function() { options: { responsive: true, maintainAspectRatio: true, - aspectRatio: 3, + aspectRatio, scales: { x: { type: 'time', diff --git a/assets/js/forms.js b/assets/js/forms.js index 0cd1c6f5..3a051062 100644 --- a/assets/js/forms.js +++ b/assets/js/forms.js @@ -6,6 +6,7 @@ */ document.addEventListener( 'DOMContentLoaded', function() { const ctx = document.getElementById( 'formsChart' ); + const aspectRatio = window.innerWidth > 600 ? 3 : 2; new Chart( ctx, { type: 'bar', @@ -22,7 +23,7 @@ document.addEventListener( 'DOMContentLoaded', function() { options: { responsive: true, maintainAspectRatio: true, - aspectRatio: 3, + aspectRatio, scales: { x: { type: 'time', diff --git a/changelog.txt b/changelog.txt index 2517f395..a2e073ff 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ += 4.1.2 = +* Added option to have the hCaptcha admin menu under Settings. +* Fixed the General admin page on the mobile. +* Fixed Forms and Events admin pages on the mobile. + = 4.1.1 = * Added updating of the Custom Themes properties on the General page upon manual editing of the Config Params JSON. * Fixed a possible fatal error with third-party plugins using a Jetpack library. diff --git a/hcaptcha.php b/hcaptcha.php index 8e0867a9..24859255 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: 4.1.1 + * Version: 4.1.2 * Requires at least: 5.1 * Requires PHP: 7.0 * Author: hCaptcha @@ -39,7 +39,7 @@ /** * Plugin version. */ -const HCAPTCHA_VERSION = '4.1.1'; +const HCAPTCHA_VERSION = '4.1.2'; /** * Path to the plugin dir. diff --git a/readme.txt b/readme.txt index 9c145d42..740f2888 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: captcha, hcaptcha, antispam, abuse, protect form Requires at least: 5.1 Tested up to: 6.5 Requires PHP: 7.0.0 -Stable tag: 4.1.1 +Stable tag: 4.1.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -565,6 +565,11 @@ Instructions for popular native integrations are below: == Changelog == += 4.1.2 = +* Added option to have the hCaptcha admin menu under Settings. +* Fixed the General admin page on the mobile. +* Fixed Forms and Events admin pages on the mobile. + = 4.1.1 = * Added updating of the Custom Themes properties on the General page upon manual editing of the Config Params JSON. * Fixed a possible fatal error with third-party plugins using a Jetpack library. diff --git a/src/php/GravityForms/Form.php b/src/php/GravityForms/Form.php index 5bbb199c..c86d41d3 100644 --- a/src/php/GravityForms/Form.php +++ b/src/php/GravityForms/Form.php @@ -11,6 +11,7 @@ namespace HCaptcha\GravityForms; use GFFormsModel; +use GP_Field_Nested_Form; use HCaptcha\Helpers\HCaptcha; /** @@ -263,17 +264,47 @@ private function should_verify(): bool { return false; } - if ( isset( $_POST['gpnf_parent_form_id'] ) ) { - // Do not verify nested form. - return false; + $form_id = (int) $_POST['gform_submit']; + + // Nested form. + $parent_form_id = isset( $_POST['gpnf_parent_form_id'] ) ? (int) $_POST['gpnf_parent_form_id'] : 0; + + if ( $parent_form_id ) { + $fields = (array) GFFormsModel::get_form_meta( $parent_form_id )['fields']; + + foreach ( $fields as $field ) { + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( $field instanceof GP_Field_Nested_Form && (int) $field->gpnfForm === $form_id ) { + + // Do not verify nested form. + return false; + } + } } - $form_id = (int) $_POST['gform_submit']; - $target_page = "gform_target_page_number_$form_id"; + // Multipage form. + $target_page_name = "gform_target_page_number_$form_id"; - if ( isset( $_POST[ $target_page ] ) && 0 !== (int) $_POST[ $target_page ] ) { - // Do not verify hCaptcha and return success when switching between form pages. - return false; + if ( isset( $_POST[ $target_page_name ] ) ) { + $source_page_name = "gform_source_page_number_$form_id"; + + $target_page = (int) $_POST[ $target_page_name ]; + $source_page = isset( $_POST[ $source_page_name ] ) ? (int) $_POST[ $source_page_name ] : 0; + + $form_meta = (array) GFFormsModel::get_form_meta( $form_id ); + + if ( + 0 !== (int) $_POST[ $target_page_name ] && + $target_page !== $source_page && + isset( + $form_meta['pagination']['pages'][ $target_page - 1 ], + $form_meta['pagination']['pages'][ $source_page - 1 ] + ) + ) { + + // Do not verify hCaptcha and return success when switching between form pages. + return false; + } } // phpcs:enable WordPress.Security.NonceVerification.Missing diff --git a/src/php/Main.php b/src/php/Main.php index 79d4eb3c..b275be20 100644 --- a/src/php/Main.php +++ b/src/php/Main.php @@ -134,8 +134,10 @@ public function init() { public function init_hooks() { $this->load_textdomain(); - $args = [ - 'mode' => SettingsBase::MODE_PAGES, + $settings = get_option( 'hcaptcha_settings', [] ); + $menu_position = $settings['menu_position'] ?? []; + $args = [ + 'mode' => [ 'on' ] === $menu_position ? SettingsBase::MODE_TABS : SettingsBase::MODE_PAGES, ]; /** diff --git a/src/php/Settings/General.php b/src/php/Settings/General.php index b21be8c1..be70b8a8 100644 --- a/src/php/Settings/General.php +++ b/src/php/Settings/General.php @@ -370,10 +370,19 @@ public function init_form_fields() { 'type' => 'checkbox', 'section' => self::SECTION_APPEARANCE, 'options' => [ - 'on' => __( 'Force hCaptcha', 'hcaptcha-for-forms-and-more' ), + 'on' => __( 'Force', 'hcaptcha-for-forms-and-more' ), ], 'helper' => __( 'Force hCaptcha check before submit.', 'hcaptcha-for-forms-and-more' ), ], + 'menu_position' => [ + 'label' => __( 'Tabs Menu Under Settings', 'hcaptcha-for-forms-and-more' ), + 'type' => 'checkbox', + 'section' => self::SECTION_APPEARANCE, + 'options' => [ + 'on' => __( 'Tabs', 'hcaptcha-for-forms-and-more' ), + ], + 'helper' => __( 'When on, the hCaptcha admin menu is placed under Settings.', 'hcaptcha-for-forms-and-more' ), + ], 'custom_themes' => [ 'label' => __( 'Custom Themes', 'hcaptcha-for-forms-and-more' ), 'type' => 'checkbox',