diff --git a/.distignore b/.distignore index 7566f3a7..668ecdde 100644 --- a/.distignore +++ b/.distignore @@ -9,14 +9,13 @@ /.tests /.wordpress-org /.yarn -/bin /node_modules /src/js # Files /.DS_Store /.babelrc -/.codeception.yml +/codeception.yml /.distignore /.editorconfig /.eslintrc.json diff --git a/.tests/php/integration/AAAMainTest.php b/.tests/php/integration/AAAMainTest.php index 5ddd283e..4a28c397 100644 --- a/.tests/php/integration/AAAMainTest.php +++ b/.tests/php/integration/AAAMainTest.php @@ -26,6 +26,7 @@ use HCaptcha\Jetpack\JetpackForm; use HCaptcha\Main; use HCaptcha\ElementorPro\HCaptchaHandler; +use HCaptcha\Migrations\Migrations; use HCaptcha\NF\NF; use HCaptcha\Quform\Quform; use HCaptcha\Sendinblue\Sendinblue; @@ -109,15 +110,41 @@ public function test_init(): void { $hcaptcha = hcaptcha(); // The plugin was loaded by codeception. - self::assertSame( - PHP_INT_MAX, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + self::assertSame( Main::LOAD_PRIORITY, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); - remove_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ], -PHP_INT_MAX ); + remove_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ], Main::LOAD_PRIORITY ); self::assertFalse( has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); $hcaptcha->init(); - self::assertSame( - PHP_INT_MAX, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + self::assertSame( Main::LOAD_PRIORITY, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + } + + /** + * Test init() on cron request. + * + * @return void + * @throws ReflectionException ReflectionException. + */ + public function test_init_on_cron(): void { + $hcaptcha = hcaptcha(); + + // The plugin was loaded by codeception. + self::assertSame( Main::LOAD_PRIORITY, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + + remove_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ], Main::LOAD_PRIORITY ); + + self::assertFalse( has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + + add_filter( 'wp_doing_cron', '__return_true' ); + + $hcaptcha->init(); + + $migrations = $this->get_protected_property( $hcaptcha, 'migrations' ); + + self::assertFalse( has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + self::assertSame( Migrations::LOAD_PRIORITY, has_action( 'plugins_loaded', [ $migrations, 'migrate' ] ) ); } /** @@ -166,9 +193,9 @@ static function () use ( $whitelisted ) { ) ); - self::assertSame( - PHP_INT_MAX, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); + self::assertSame( Main::LOAD_PRIORITY, has_action( 'plugins_loaded', [ $hcaptcha, 'init_hooks' ] ) ); - self::assertSame( - PHP_INT_MAX + 1, has_action( 'plugins_loaded', [ $hcaptcha, 'load_modules' ] ) ); + self::assertSame( Main::LOAD_PRIORITY + 1, has_action( 'plugins_loaded', [ $hcaptcha, 'load_modules' ] ) ); self::assertSame( 10, has_filter( 'wp_resource_hints', [ $hcaptcha, 'prefetch_hcaptcha_dns' ] ) ); self::assertSame( 10, has_filter( 'wp_headers', [ $hcaptcha, 'csp_headers' ] ) ); @@ -188,11 +215,11 @@ static function () use ( $whitelisted ) { $subject->init_hooks(); self::assertSame( - - PHP_INT_MAX + 1, + Main::LOAD_PRIORITY + 1, has_action( 'plugins_loaded', [ $subject, 'load_modules' ] ) ); self::assertSame( - - PHP_INT_MAX, + -PHP_INT_MAX, has_filter( 'hcap_whitelist_ip', [ $subject, 'whitelist_ip' ] @@ -285,11 +312,11 @@ static function () { $subject->init_hooks(); self::assertSame( - - PHP_INT_MAX + 1, + Main::LOAD_PRIORITY + 1, has_action( 'plugins_loaded', [ $subject, 'load_modules' ] ) ); self::assertSame( - - PHP_INT_MAX, + -PHP_INT_MAX, has_filter( 'hcap_whitelist_ip', [ $subject, 'whitelist_ip' ] @@ -341,8 +368,12 @@ public function dp_test_init_and_init_hooks_on_elementor_pro_edit_page(): array ], 'request2' => [ 'on', - [ 'REQUEST_URI' => '/elementor?elementor-preview=23' ], - [ 'elementor-preview' => 23 ], + [], + [ + 'preview_id' => 23, + 'preview_nonce' => 'some', + 'preview' => true, + ], [], true, ], @@ -613,7 +644,8 @@ public function dp_test_print_inline_styles(): array { /** * Test login_head(). * - * @noinspection CssUnusedSymbol*/ + * @noinspection CssUnusedSymbol + */ public function test_login_head(): void { FunctionMocker::replace( 'defined', @@ -930,7 +962,7 @@ function delayedLoad() { 'custom_themes' => $custom_themes ? [ $custom_themes ] : [], // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode 'config_params' => json_encode( $config_params ), - 'delay' => - 100, + 'delay' => -100, 'license' => 'pro', ] ); @@ -1302,11 +1334,38 @@ public function dp_test_load_modules(): array { 'back-in-stock-notifier-for-woocommerce/cwginstocknotifier.php', \HCaptcha\BackInStockNotifier\Form::class, ], + 'bbPress Login Form' => [ + [ 'bbp_status', 'login' ], + 'bbpress/bbpress.php', + [ + \HCaptcha\BBPress\Login::class, + \HCaptcha\BBPress\LostPassword::class, + \HCaptcha\BBPress\Register::class, + ], + ], + 'bbPress Lost Password Form' => [ + [ 'bbp_status', 'lost_pass' ], + 'bbpress/bbpress.php', + [ + \HCaptcha\BBPress\Login::class, + \HCaptcha\BBPress\LostPassword::class, + \HCaptcha\BBPress\Register::class, + ], + ], 'bbPress New Topic' => [ [ 'bbp_status', 'new_topic' ], 'bbpress/bbpress.php', NewTopic::class, ], + 'bbPress Register Form' => [ + [ 'bbp_status', 'register' ], + 'bbpress/bbpress.php', + [ + \HCaptcha\BBPress\Login::class, + \HCaptcha\BBPress\LostPassword::class, + \HCaptcha\BBPress\Register::class, + ], + ], 'bbPress Reply' => [ [ 'bbp_status', 'reply' ], 'bbpress/bbpress.php', @@ -1696,7 +1755,7 @@ static function ( $override, $domain, $mofile ) use ( &$override_filter_params ) return $override; }, - - PHP_INT_MAX, + Main::LOAD_PRIORITY, 3 ); diff --git a/.tests/php/integration/BBPress/LoginTest.php b/.tests/php/integration/BBPress/LoginTest.php new file mode 100644 index 00000000..4167662e --- /dev/null +++ b/.tests/php/integration/BBPress/LoginTest.php @@ -0,0 +1,104 @@ + + $placeholder + + + + + + + + + +HTML; + + $subject = new Form(); + + $content = 'some content'; + + // Ignore non-form blocks. + self::assertSame( $content, $subject->add_hcaptcha( $content, $block, $instance ) ); + + $block = [ + 'blockName' => 'coblocks/form', + ]; + + $args = [ + 'action' => 'hcaptcha_coblocks', + 'name' => 'hcaptcha_coblocks_nonce', + 'id' => [ + 'source' => [ 'coblocks/class-coblocks.php' ], + 'form_id' => $form_id, + ], + ]; + $hcaptcha = $this->get_hcap_form( $args ); + + $content = str_replace( $placeholder, '', $template ); + $expected = str_replace( $placeholder, $hcaptcha . "\n", $template ); + + self::assertSame( $expected, $subject->add_hcaptcha( $content, $block, $instance ) ); + } + + /** + * Test render_block_data(). + * + * @return void + */ + public function test_render_block_data(): void { + $parsed_block = [ + 'blockName' => 'some', + 'attrs' => [ 'some' ], + ]; + $source_block = [ + 'blockName' => 'coblocks/form', + 'attrs' => [ 'some' ], + ]; + + $subject = new Form(); + + self::assertFalse( has_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ) ); + + // Ignore other blocks. + $subject->render_block_data( $parsed_block, $source_block ); + + self::assertFalse( has_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ) ); + + // Ignore for coblocks/form if no POST data. + $parsed_block['blockName'] = 'coblocks/form'; + + $subject->render_block_data( $parsed_block, $source_block ); + + self::assertFalse( has_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ) ); + + // Add action for coblocks/form. + $_POST['action'] = 'coblocks-form-submit'; + + $subject->render_block_data( $parsed_block, $source_block ); + + self::assertSame( 10, has_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ) ); + + // Do not add action for coblocks/form if already added. + remove_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ); + + $subject->render_block_data( $parsed_block, $source_block ); + + self::assertFalse( has_action( 'coblocks_before_form_submit', [ $subject, 'before_form_submit' ] ) ); + } + + /** + * Test before_form_submit(). + * + * @return void + */ + public function test_before_form_submit(): void { + $post = [ 'some' ]; + $atts = [ 'some' ]; + + $subject = new Form(); + + $subject->before_form_submit( $post, $atts ); + + self::assertSame( 10, has_filter( 'pre_option_coblocks_google_recaptcha_site_key', '__return_true' ) ); + self::assertSame( 10, has_filter( 'pre_option_coblocks_google_recaptcha_secret_key', '__return_true' ) ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + self::assertSame( 'hcaptcha_token', $_POST['g-recaptcha-token'] ); + + self::assertSame( 10, has_filter( 'pre_http_request', [ $subject, 'verify' ] ) ); + } + + /** + * Test verify(). + * + * @return void + */ + public function test_verify(): void { + $verify_url = CoBlocks_Form::GCAPTCHA_VERIFY_URL; + $response = [ 'some response' ]; + $parsed_args = [ + 'body' => [ + 'response' => 'some response', + ], + ]; + $url = 'some url'; + $expected = [ + 'body' => '{"success":true}', + 'response' => + [ + 'code' => 200, + 'message' => 'OK', + ], + ]; + + $this->prepare_hcaptcha_verify_post( 'hcaptcha_coblocks_nonce', 'hcaptcha_coblocks' ); + + $subject = new Form(); + + add_filter( 'pre_http_request', [ $subject, 'verify' ] ); + + // Wrong url. + self::assertSame( $response, $subject->verify( $response, $parsed_args, $url ) ); + self::assertSame( 10, has_filter( 'pre_http_request', [ $subject, 'verify' ] ) ); + + // Wrong token. + $url = $verify_url; + + self::assertSame( $response, $subject->verify( $response, $parsed_args, $url ) ); + self::assertSame( 10, has_filter( 'pre_http_request', [ $subject, 'verify' ] ) ); + + // Process verification. + $parsed_args['body']['response'] = 'hcaptcha_token'; + + self::assertSame( $expected, $subject->verify( $response, $parsed_args, $url ) ); + self::assertFalse( has_filter( 'pre_http_request', [ $subject, 'verify' ] ) ); + } + + /** + * Test verify() when not verified. + * + * @return void + */ + public function test_verify_not_verified(): void { + $verify_url = CoBlocks_Form::GCAPTCHA_VERIFY_URL; + $response = [ 'some response' ]; + $parsed_args = [ + 'body' => [ + 'response' => 'hcaptcha_token', + ], + ]; + $url = $verify_url; + $expected = [ + 'body' => '{"success":false}', + 'response' => + [ + 'code' => 200, + 'message' => 'OK', + ], + ]; + + $this->prepare_hcaptcha_verify_post( 'hcaptcha_coblocks_nonce', 'hcaptcha_coblocks', false ); + + $subject = new Form(); + + add_filter( 'pre_http_request', [ $subject, 'verify' ] ); + + // Process verification. + $parsed_args['body']['response'] = 'hcaptcha_token'; + + self::assertSame( $expected, $subject->verify( $response, $parsed_args, $url ) ); + self::assertFalse( has_filter( 'pre_http_request', [ $subject, 'verify' ] ) ); + } + + /** + * Test print_inline_styles(). + * + * @return void + * @noinspection CssUnusedSymbol + */ + public function test_print_inline_styles(): void { + FunctionMocker::replace( + 'defined', + static function ( $constant_name ) { + return 'SCRIPT_DEBUG' === $constant_name; + } + ); + + FunctionMocker::replace( + 'constant', + static function ( $name ) { + return 'SCRIPT_DEBUG' === $name; + } + ); + + $expected = <<print_inline_styles(); + + self::assertSame( $expected, ob_get_clean() ); + } +} diff --git a/.tests/php/integration/Divi/EmailOptinTest.php b/.tests/php/integration/Divi/EmailOptinTest.php index 21019910..7a1646e8 100644 --- a/.tests/php/integration/Divi/EmailOptinTest.php +++ b/.tests/php/integration/Divi/EmailOptinTest.php @@ -20,6 +20,7 @@ * Class EmailOptinTest * * @group divi + * @group divi-email-optin */ class EmailOptinTest extends HCaptchaWPTestCase { @@ -137,4 +138,35 @@ public function test_enqueue_scripts_when_form_was_not_shown(): void { self::assertFalse( wp_script_is( EmailOptin::HANDLE ) ); } + + /** + * Test add_type_module(). + * + * @return void + * @noinspection JSUnresolvedLibraryURL + */ + public function test_add_type_module(): void { + $subject = new EmailOptin(); + + // Wrong handle. + + // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript + $tag = ''; + $handle = 'some'; + $src = 'https://example.com/script.js'; + + self::assertSame( $tag, $subject->add_type_module( $tag, $handle, $src ) ); + + // Proper handle. + $handle = EmailOptin::HANDLE; + $expected = ''; + + self::assertSame( $expected, $subject->add_type_module( $tag, $handle, $src ) ); + + // Script has a type. + $tag = ''; + // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript + + self::assertSame( $expected, $subject->add_type_module( $tag, $handle, $src ) ); + } } diff --git a/.tests/php/integration/Helpers/PagesTest.php b/.tests/php/integration/Helpers/PagesTest.php new file mode 100644 index 00000000..61f9ecd7 --- /dev/null +++ b/.tests/php/integration/Helpers/PagesTest.php @@ -0,0 +1,70 @@ +prepare_hcaptcha_get_verify_message( 'hcaptcha_jetpack_nonce', 'hcaptcha_jetpack', false ); @@ -73,6 +87,13 @@ public function test_jetpack_verify_not_verified(): void { $subject = new JetpackForm(); self::assertEquals( $error, $subject->verify() ); + self::assertNull( $this->get_protected_property( $subject, 'error_form_hash' ) ); + self::assertSame( 10, has_action( 'hcap_hcaptcha_content', [ $subject, 'error_message' ] ) ); + + $_POST['contact-form-hash'] = $hash; + + self::assertEquals( $error, $subject->verify() ); + self::assertSame( $hash, $this->get_protected_property( $subject, 'error_form_hash' ) ); self::assertSame( 10, has_action( 'hcap_hcaptcha_content', [ $subject, 'error_message' ] ) ); } @@ -85,6 +106,12 @@ public function test_jetpack_verify_not_verified(): void { public function test_error_message(): void { $hcaptcha_content = 'some content'; $error_message = 'some error message'; + $error_form_hash = 'some hash'; + $args = [ + 'id' => [ + 'form_id' => $error_form_hash, + ], + ]; $subject = new JetpackForm(); @@ -92,6 +119,10 @@ public function test_error_message(): void { $this->set_protected_property( $subject, 'error_message', $error_message ); + self::assertSame( $hcaptcha_content, $subject->error_message( $hcaptcha_content ) ); + + $this->set_protected_property( $subject, 'error_form_hash', $error_form_hash ); + $expected = $hcaptcha_content . '
Warning. @@ -100,7 +131,7 @@ public function test_error_message(): void { ' . $error_message . '
'; - self::assertSame( $expected, $subject->error_message( $hcaptcha_content ) ); + self::assertSame( $expected, $subject->error_message( $hcaptcha_content, $args ) ); } /** diff --git a/.tests/php/integration/Jetpack/JetpackFormTest.php b/.tests/php/integration/Jetpack/JetpackFormTest.php index a57d8135..18fb99e1 100644 --- a/.tests/php/integration/Jetpack/JetpackFormTest.php +++ b/.tests/php/integration/Jetpack/JetpackFormTest.php @@ -40,39 +40,49 @@ public function test_add_captcha( string $content, string $expected ): void { public function dp_test_add_captcha(): array { $_SERVER['REQUEST_URI'] = 'http://test.test/'; - $args = [ + $hash = 'some hash'; + $hash_input = ""; + $classic_args = [ 'action' => 'hcaptcha_jetpack', 'name' => 'hcaptcha_jetpack_nonce', - 'force' => true, 'id' => [ 'source' => [ 'jetpack/jetpack.php' ], 'form_id' => 'contact', ], ]; - $hcaptcha = $this->get_hcap_form( $args ); + $classic_hcaptcha = $this->get_hcap_form( $classic_args ); + $args = [ + 'action' => 'hcaptcha_jetpack', + 'name' => 'hcaptcha_jetpack_nonce', + 'id' => [ + 'source' => [ 'jetpack/jetpack.php' ], + 'form_id' => 'contact_' . $hash, + ], + ]; + $hcaptcha = $this->get_hcap_form( $args ); return [ 'Empty contact form' => [ '', '' ], 'Classic contact form' => [ - '[contact-form] Some contact form [/contact-form]', - '[contact-form] Some contact form
' . $hcaptcha . '
[/contact-form]', + '
Contact Us' . $hash_input . '
', + '
' . $hcaptcha . '' . $hash_input . '
', ], 'Classic contact form with hcaptcha' => [ '[contact-form] Some contact form [hcaptcha][/contact-form]', '[contact-form] Some contact form [hcaptcha][/contact-form]', ], 'Block contact form' => [ - '
Contact Us
', - '
' . $hcaptcha . '
Contact Us', + '
Contact Us' . $hash_input . '
', + '
' . $hcaptcha . '
Contact Us' . $hash_input . '', ], 'Block contact form with hcaptcha' => [ '
Contact Us
', '
Contact Us
', ], 'Block contact form and search form' => [ - '
Contact Us
' . + '
Contact Us' . $hash_input . '
' . '
', - '
' . $hcaptcha . '
Contact Us' . + '
' . $hcaptcha . '
Contact Us' . $hash_input . '' . '
', ], ]; diff --git a/.tests/php/integration/MailPoet/FormTest.php b/.tests/php/integration/MailPoet/FormTest.php new file mode 100644 index 00000000..22a90335 --- /dev/null +++ b/.tests/php/integration/MailPoet/FormTest.php @@ -0,0 +1,169 @@ + + + $placeholder + +
+ +
some hCaptcha
+
+
+ + $placeholder +
+HTML; + + $subject = new Form(); + + $content = 'some content'; + self::assertSame( $content, $subject->the_content_filter( $content ) ); + + $args = [ + 'action' => 'hcaptcha_mailpoet', + 'name' => 'hcaptcha_mailpoet_nonce', + 'id' => [ + 'source' => [ 'mailpoet/mailpoet.php' ], + 'form_id' => $form_id, + ], + ]; + $hcaptcha = $this->get_hcap_form( $args ); + + $content = str_replace( $placeholder, '', $template ); + $expected = str_replace( $placeholder, $hcaptcha, $template ); + + self::assertSame( $expected, $subject->the_content_filter( $content ) ); + } + + /** + * Test verify(). + */ + public function test_verify(): void { + $api = Mockery::mock( API::class ); + + $subject = new Form(); + + $subject->verify( $api ); + + $_POST['action'] = 'mailpoet'; + $_POST['endpoint'] = 'subscribers'; + $_POST['method'] = 'subscribe'; + + $this->prepare_hcaptcha_verify_post( 'hcaptcha_mailpoet_nonce', 'hcaptcha_mailpoet' ); + + $subject->verify( $api ); + } + + /** + * Test verify() when not verified. + */ + public function test_verify_not_verified(): void { + $code = 'fail'; + $error_message = 'The hCaptcha is invalid.'; + + Mockery::namedMock( Response::class, ResponseStub::class ); + $error_response = Mockery::mock( ErrorResponse::class ); + $api = Mockery::mock( API::class ); + + $error_response->shouldReceive( 'send' )->once(); + $api->shouldReceive( 'createErrorResponse' ) + ->with( $code, $error_message, ResponseStub::STATUS_UNAUTHORIZED ) + ->andReturn( $error_response ); + + $subject = new Form(); + + $subject->verify( $api ); + + $_POST['action'] = 'mailpoet'; + $_POST['endpoint'] = 'subscribers'; + $_POST['method'] = 'subscribe'; + + $this->prepare_hcaptcha_verify_post( 'hcaptcha_mailpoet_nonce', 'hcaptcha_mailpoet', false ); + + $subject->verify( $api ); + } + + /** + * Test enqueue_scripts(). + */ + public function test_enqueue_scripts(): void { + $subject = new Form(); + + self::assertFalse( wp_script_is( 'hcaptcha-mailpoet' ) ); + + $subject->enqueue_scripts(); + + self::assertFalse( wp_script_is( 'hcaptcha-mailpoet' ) ); + + hcaptcha()->form_shown = true; + + $subject->enqueue_scripts(); + + self::assertTrue( wp_script_is( 'hcaptcha-mailpoet' ) ); + } +} diff --git a/.tests/php/integration/Mailchimp/FormTest.php b/.tests/php/integration/Mailchimp/FormTest.php index 9c4f423e..0cd827d3 100644 --- a/.tests/php/integration/Mailchimp/FormTest.php +++ b/.tests/php/integration/Mailchimp/FormTest.php @@ -18,6 +18,8 @@ /** * Test Form class. + * + * @group mailchimp */ class FormTest extends HCaptchaWPTestCase { diff --git a/.tests/php/integration/MainPluginFileTest.php b/.tests/php/integration/MainPluginFileTest.php index 16109bb0..5658e16b 100644 --- a/.tests/php/integration/MainPluginFileTest.php +++ b/.tests/php/integration/MainPluginFileTest.php @@ -68,6 +68,10 @@ public function test_main_file_content(): void { * Test that readme.txt contains proper stable tag. */ public function test_stable_tag_in_readme_txt(): void { + if ( preg_match( '/-.+$/', HCAPTCHA_VERSION ) ) { + $this->markTestSkipped( 'Not a final version, skipping stable tag in readme.txt test.' ); + } + $expected = [ 'stable_tag' => HCAPTCHA_VERSION, ]; @@ -86,6 +90,10 @@ public function test_stable_tag_in_readme_txt(): void { * Test that readme.txt contains changelog records for the current version. */ public function test_changelog(): void { + if ( preg_match( '/-.+$/', HCAPTCHA_VERSION ) ) { + $this->markTestSkipped( 'Not a final version, skipping changelog test.' ); + } + $readme_file = HCAPTCHA_PATH . '/readme.txt'; $changelog_file = HCAPTCHA_PATH . '/changelog.txt'; diff --git a/.tests/php/integration/Migrations/MigrationsTest.php b/.tests/php/integration/Migrations/MigrationsTest.php index 070d9271..0451af9f 100644 --- a/.tests/php/integration/Migrations/MigrationsTest.php +++ b/.tests/php/integration/Migrations/MigrationsTest.php @@ -9,6 +9,7 @@ use HCaptcha\Admin\Events\Events; use HCaptcha\Migrations\Migrations; +use HCaptcha\Settings\PluginSettingsBase; use HCaptcha\Tests\Integration\HCaptchaWPTestCase; use Mockery; use ReflectionException; @@ -29,6 +30,7 @@ public function tearDown(): void { parent::tearDown(); } + /** * Test init() and init_hooks(). * @@ -62,7 +64,7 @@ public function dp_test_init_and_init_hooks(): array { return [ [ false, false, false ], [ true, false, false ], - [ false, true, - PHP_INT_MAX ], + [ false, true, -PHP_INT_MAX ], [ true, true, false ], ]; } @@ -122,6 +124,8 @@ public function test_migrate(): void { $subject->migrate(); + self::assertSame( 10, has_action( 'init', [ $subject, 'send_plugin_stats' ] ) ); + self::assertTrue( $this->compare_migrated( $expected_option, get_option( $subject::MIGRATED_VERSIONS_OPTION_NAME, [] ) ) ); self::assertSame( $expected_settings, get_option( 'hcaptcha_settings', [] ) ); self::assertFalse( get_option( 'hcaptcha_size' ) ); @@ -158,6 +162,21 @@ private function compare_migrated( array $expected_option, array $option ): bool return true; } + /** + * Test send_plugin_stats(). + * + * @return void + */ + public function test_send_plugin_stats(): void { + $subject = new Migrations(); + + self::assertSame( 0, did_action( 'hcap_send_plugin_stats' ) ); + + $subject->send_plugin_stats(); + + self::assertSame( 1, did_action( 'hcap_send_plugin_stats' ) ); + } + /** * Test migrate_360() when WPForms status not set. * @@ -224,4 +243,88 @@ static function ( $queries ) use ( &$actual_query ) { self::assertSame( array_filter( explode( ';', $expected_query ) ), $actual_query ); } + + /** + * Test save_license_level(). + * + * @param string $license_level License level. + * + * @return void + * @dataProvider dp_test_save_license_level + */ + public function test_save_license_level( string $license_level ): void { + $subject = new Migrations(); + + $option = get_option( PluginSettingsBase::OPTION_NAME, [] ); + + self::assertSame( [], $option ); + + switch ( $license_level ) { + case 'free': + $result = [ + 'features' => [], + 'pass' => true, + ]; + $expected = [ + 'license' => $license_level, + ]; + + break; + case 'pro': + $result = [ + 'features' => [ + 'custom_theme' => [ 'some theme' ], + ], + 'pass' => true, + ]; + $expected = [ + 'license' => $license_level, + ]; + + break; + case 'error': + $result['pass'] = false; + $result['error'] = 'some error'; + $expected = []; + + break; + default: + $result = []; + $expected = []; + + break; + } + + add_filter( + 'pre_http_request', + static function ( $value, $parsed_args, $url ) use ( $result ) { + if ( false !== strpos( $url, 'hcaptcha.com' ) ) { + return [ + 'body' => wp_json_encode( $result ), + ]; + } + + return $value; + }, + 10, + 3 + ); + + $subject->save_license_level(); + + self::assertSame( $expected, get_option( PluginSettingsBase::OPTION_NAME, [] ) ); + } + + /** + * Data provider for test_save_license_level(). + * + * @return array + */ + public function dp_test_save_license_level(): array { + return [ + [ 'free' ], + [ 'pro' ], + [ 'error' ], + ]; + } } diff --git a/.tests/php/integration/NF/FieldTest.php b/.tests/php/integration/NF/FieldTest.php index 20decfc5..ef934d05 100644 --- a/.tests/php/integration/NF/FieldTest.php +++ b/.tests/php/integration/NF/FieldTest.php @@ -15,7 +15,8 @@ * * Ninja Forms requires PHP 7.2. * - * @requires PHP <= 8.2 + * @requires PHP < 8.3 + * @group nf */ class FieldTest extends HCaptchaPluginWPTestCase { @@ -35,6 +36,7 @@ public function test_constructor(): void { $subject = new Field(); self::assertSame( 'hCaptcha', $subject->get_nicename() ); + self::assertSame( 10, has_filter( 'nf_sub_hidden_field_types', [ $subject, 'hide_field_type' ] ) ); } /** @@ -69,4 +71,18 @@ public function test_validate_not_validated(): void { self::assertSame( 'The hCaptcha is invalid.', $subject->validate( $field, null ) ); } + + /** + * Test hide_field_type(). + * + * @return void + */ + public function test_hide_field_type(): void { + $hidden_field_types = [ 'some type' ]; + $expected = [ 'some type', 'hcaptcha-for-ninja-forms' ]; + + $subject = new Field(); + + self::assertSame( $expected, $subject->hide_field_type( $hidden_field_types ) ); + } } diff --git a/.tests/php/integration/NF/NFTest.php b/.tests/php/integration/NF/NFTest.php index 3f3a7eb3..0605444c 100644 --- a/.tests/php/integration/NF/NFTest.php +++ b/.tests/php/integration/NF/NFTest.php @@ -7,9 +7,11 @@ namespace HCaptcha\Tests\Integration\NF; +use HCaptcha\NF\Base; use HCaptcha\NF\Field; use HCaptcha\NF\NF; use HCaptcha\Tests\Integration\HCaptchaPluginWPTestCase; +use ReflectionException; use tad\FunctionMocker\FunctionMocker; /** @@ -17,7 +19,8 @@ * * Ninja Forms requires PHP 7.2. * - * @requires PHP <= 8.2 + * @requires PHP < 8.3 + * @group nf */ class NFTest extends HCaptchaPluginWPTestCase { @@ -29,26 +32,218 @@ class NFTest extends HCaptchaPluginWPTestCase { protected static $plugin = 'ninja-forms/ninja-forms.php'; /** - * Test init_hooks(). + * Tear down the test. + * + * @return void + */ + public function tearDown(): void { + unset( $_GET['form_id'] ); + + parent::tearDown(); + } + + /** + * Test init() and init_hooks(). + * + * @throws ReflectionException ReflectionException. */ - public function test_init_hooks(): void { + public function test_init_and_init_hooks(): void { $subject = new NF(); + self::assertSame( + str_replace( '\\', '/', HCAPTCHA_PATH . '/src/php/NF/templates/' ), + str_replace( '\\', '/', $this->get_protected_property( $subject, 'templates_dir' ) ) + ); + + self::assertSame( + 11, + has_filter( 'toplevel_page_ninja-forms', [ $subject, 'admin_template' ] ) + ); + self::assertSame( + 10, + has_filter( 'nf_admin_enqueue_scripts', [ $subject, 'nf_admin_enqueue_scripts' ] ) + ); self::assertSame( 10, has_filter( 'ninja_forms_register_fields', [ $subject, 'register_fields' ] ) ); + self::assertSame( + 10, + has_filter( 'ninja_forms_loaded', [ $subject, 'place_hcaptcha_before_recaptcha_field' ] ) + ); self::assertSame( 10, has_filter( 'ninja_forms_field_template_file_paths', [ $subject, 'template_file_paths' ] ) ); + self::assertSame( + 10, + has_filter( 'nf_get_form_id', [ $subject, 'set_form_id' ] ) + ); self::assertSame( 10, has_filter( 'ninja_forms_localize_field_hcaptcha-for-ninja-forms', [ $subject, 'localize_field' ] ) ); + self::assertSame( + 10, + has_filter( 'ninja_forms_localize_field_hcaptcha-for-ninja-forms_preview', [ $subject, 'localize_field' ] ) + ); self::assertSame( 9, has_action( 'wp_print_footer_scripts', [ $subject, 'nf_captcha_script' ] ) ); } + /** + * Test admin_template(). + * + * @return void + */ + public function test_admin_template(): void { + $subject = new NF(); + + ob_start(); + $subject->admin_template(); + self::assertSame( '', ob_get_clean() ); + + $_GET['form_id'] = 'some'; + $templates_dir = $this->get_protected_property( $subject, 'templates_dir' ); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $template = file_get_contents( $templates_dir . 'fields-hcaptcha.html' ); + $expected = str_replace( + 'tmpl-nf-field-' . Base::TYPE, + 'tmpl-nf-field-' . Base::NAME, + $template + ); + + ob_start(); + $subject->admin_template(); + self::assertSame( $expected, ob_get_clean() ); + } + + /** + * Test nf_admin_enqueue_scripts(). + * + * @return void + * @throws ReflectionException ReflectionException. + */ + public function test_nf_admin_enqueue_scripts(): void { + global $wp_scripts; + + $subject = new NF(); + + $subject->nf_admin_enqueue_scripts(); + + self::assertFalse( wp_script_is( 'hcaptcha-nf' ) ); + + $field_id = 27; + $hcaptcha_id = 'hcaptcha-nf-625d3b9b318fc0.86180601'; + $form_data = << [ + 'source' => 'ninja-forms/ninja-forms.php', + 'form_id' => $form_id, + ], + ]; + + $hcaptcha = $this->get_hcap_form( $args ); + $hcaptcha = str_replace( + 'set_protected_property( $subject, 'form_id', $form_id ); + + FunctionMocker::replace( 'uniqid', $hcaptcha_id ); + + $subject->nf_admin_enqueue_scripts(); + + $data = $wp_scripts->registered['nf-builder']->extra['data']; + + preg_match( '/var nfDashInlineVars = (.+);/', $data, $m ); + + self::assertSame( $expected, json_decode( $m[1], true ) ); + + self::assertTrue( wp_script_is( 'kagg-dialog' ) ); + self::assertTrue( wp_style_is( 'kagg-dialog' ) ); + self::assertTrue( wp_script_is( 'admin-nf' ) ); + + $data = $wp_scripts->registered['admin-nf']->extra['data']; + + preg_match( '/var HCaptchaAdminNFObject = ({.+});/', $data, $m ); + + $admin_nf_obj = json_decode( $m[1], true ); + self::assertSame( + [ + 'onlyOne' => 'Only one hCaptcha field allowed.', + 'OKBtnText' => 'OK', + ], + $admin_nf_obj + ); + } + /** * Test register_fields. */ @@ -60,6 +255,31 @@ public function test_register_fields(): void { self::assertInstanceOf( Field::class, $fields['hcaptcha-for-ninja-forms'] ); } + /** + * Test place_hcaptcha_before_recaptcha_field(). + * + * @return void + */ + public function test_place_hcaptcha_before_recaptcha_field(): void { + $hcaptcha_key = Base::NAME; + + $fields = Ninja_Forms()->fields; + $hcap_index = array_search( $hcaptcha_key, array_keys( $fields ), true ); + + self::assertFalse( $hcap_index ); + + $subject = new NF(); + + Ninja_Forms()->fields = $subject->register_fields( Ninja_Forms()->fields ); + $subject->place_hcaptcha_before_recaptcha_field(); + + $fields = Ninja_Forms()->fields; + $hcap_index = array_search( $hcaptcha_key, array_keys( $fields ), true ); + $recap_index = array_search( 'recaptcha', array_keys( $fields ), true ); + + self::assertSame( $recap_index, $hcap_index + 1 ); + } + /** * Test template_file_paths(). */ @@ -78,6 +298,24 @@ static function ( &$item ) { self::assertSame( $expected, $paths ); } + /** + * Test set_form_id(). + * + * @return void + * @throws ReflectionException ReflectionException. + */ + public function test_set_form_id(): void { + $form_id = 23; + + $subject = new NF(); + + self::assertSame( 0, $this->get_protected_property( $subject, 'form_id' ) ); + + $subject->set_form_id( $form_id ); + + self::assertSame( $form_id, $this->get_protected_property( $subject, 'form_id' ) ); + } + /** * Test localize_field(). */ diff --git a/.tests/php/integration/Spectra/FormTest.php b/.tests/php/integration/Spectra/FormTest.php new file mode 100644 index 00000000..2736dd9e --- /dev/null +++ b/.tests/php/integration/Spectra/FormTest.php @@ -0,0 +1,418 @@ + 'some block', + ]; + $instance = Mockery::mock( WP_Block::class ); + $template = << +
+
+
First Name
+
+
+
Last Name
+
+
+
Email
+
+
+
Message
+
+
+
+ $placeholder
+ +
+
+
The form has been submitted successfully! +
+
There has been some error while submitting the form. Please verify all form fields again. +
+
+HTML; + + $subject = new Form(); + + $content = 'some content'; + + // Ignore non-form blocks. + self::assertSame( $content, $subject->render_block( $content, $block, $instance ) ); + + $content = str_replace( $placeholder, 'uagb-forms-recaptcha', $template ); + $block = [ + 'blockName' => 'uagb/forms', + 'attrs' => [ + 'block_id' => $form_id, + ], + ]; + + // Do not replace reCaptcha. + self::assertSame( $content, $subject->render_block( $content, $block, $instance ) ); + + $args = [ + 'action' => 'hcaptcha_spectra_form', + 'name' => 'hcaptcha_spectra_form_nonce', + 'id' => [ + 'source' => [ 'ultimate-addons-for-gutenberg/ultimate-addons-for-gutenberg.php' ], + 'form_id' => $form_id, + ], + ]; + $hcaptcha = $this->get_hcap_form( $args ); + + $content = str_replace( $placeholder, '', $template ); + $expected = str_replace( $placeholder, $hcaptcha, $template ); + + self::assertSame( $expected, $subject->render_block( $content, $block, $instance ) ); + } + + /** + * Test process_ajax(). + * + * @return void + */ + public function test_process_ajax(): void { + $nonce_field_name = 'hcaptcha_spectra_form_nonce'; + $nonce_action_name = 'hcaptcha_spectra_form'; + $hcaptcha_response = 'some response'; + $form_data = wp_json_encode( + [ + 'h-captcha-response' => $hcaptcha_response, + $nonce_field_name => wp_create_nonce( $nonce_action_name ), + ] + ); + $_POST['form_data'] = $form_data; + + $this->prepare_hcaptcha_request_verify( $hcaptcha_response ); + + add_filter( 'wp_doing_ajax', '__return_true' ); + + $subject = Mockery::mock( Form::class )->makePartial(); + + $subject->shouldAllowMockingProtectedMethods(); + $subject->shouldReceive( 'has_recaptcha' )->andReturn( false ); + + $subject->process_ajax(); + } + + /** + * Test process_ajax() when not verified. + * + * @return void + */ + public function test_process_ajax_when_not_verified(): void { + $nonce_field_name = 'hcaptcha_spectra_form_nonce'; + $nonce_action_name = 'hcaptcha_spectra_form'; + $hcaptcha_response = 'some response'; + $form_data = wp_json_encode( + [ + 'h-captcha-response' => $hcaptcha_response, + $nonce_field_name => wp_create_nonce( $nonce_action_name ), + ] + ); + $response = [ + 'success' => false, + 'data' => 400, + ]; + $expected = [ + '', + '', + [ 'response' => null ], + ]; + + $this->prepare_hcaptcha_request_verify( $hcaptcha_response, false ); + + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( + 'wp_die_ajax_handler', + static function ( $name ) use ( &$die_arr ) { + return static function ( $message, $title, $args ) use ( &$die_arr ) { + $die_arr = [ $message, $title, $args ]; + }; + } + ); + + $subject = Mockery::mock( Form::class )->makePartial(); + + $subject->shouldAllowMockingProtectedMethods(); + $subject->shouldReceive( 'has_recaptcha' )->andReturn( false ); + + // Without form_data. + ob_start(); + + $subject->process_ajax(); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + self::assertSame( json_encode( $response ), ob_get_clean() ); + + self::assertSame( $expected, $die_arr ); + + // With form_data. + $_POST['form_data'] = $form_data; + + ob_start(); + + $subject->process_ajax(); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + self::assertSame( json_encode( $response ), ob_get_clean() ); + + self::assertSame( $expected, $die_arr ); + } + + /** + * Test process_ajax() when has recaptcha. + * + * @return void + */ + public function test_process_ajax_when_has_recaptcha(): void { + $subject = Mockery::mock( Form::class )->makePartial(); + + $subject->shouldAllowMockingProtectedMethods(); + $subject->shouldReceive( 'has_recaptcha' )->andReturn( true ); + + $subject->process_ajax(); + } + + /** + * Test print_inline_styles(). + * + * @return void + * @noinspection CssUnusedSymbol + */ + public function test_print_inline_styles(): void { + FunctionMocker::replace( + 'defined', + static function ( $constant_name ) { + return 'SCRIPT_DEBUG' === $constant_name; + } + ); + + FunctionMocker::replace( + 'constant', + static function ( $name ) { + return 'SCRIPT_DEBUG' === $name; + } + ); + + $expected = <<print_inline_styles(); + + self::assertSame( $expected, ob_get_clean() ); + + // No output if already shown. + ob_start(); + + $subject->print_inline_styles(); + + self::assertSame( '', ob_get_clean() ); + } + + /** + * Test print_hcaptcha_scripts(). + * + * @return void + * @noinspection PhpConditionAlreadyCheckedInspection + * @throws ReflectionException ReflectionException. + */ + public function test_print_hcaptcha_scripts(): void { + $subject = new Form(); + + self::assertTrue( $subject->print_hcaptcha_scripts( false ) ); + + $this->set_protected_property( $subject, 'has_recaptcha_field', true ); + + self::assertFalse( $subject->print_hcaptcha_scripts( false ) ); + } + + /** + * Test enqueue_scripts(). + */ + public function test_enqueue_scripts(): void { + $subject = new Form(); + + self::assertFalse( wp_script_is( 'hcaptcha-spectra' ) ); + + $subject->enqueue_scripts(); + + self::assertTrue( wp_script_is( 'hcaptcha-spectra' ) ); + } + + /** + * Test has_recaptcha(). + * + * @return void + */ + public function test_has_recaptcha() { + $block_id = 'f89cebda'; + $recaptcha_placeholder = '=== recaptcha placeholder ==='; + $template = << +
+
+ +
+
First Name
+
+ + + +
+
Last Name
+
+ + + +
+
Email
+
+ + + +
+
Message
+
+ +
+
+
+ +
+
+
The form has been submitted successfully! +
+
There has been some error while submitting the form. Please verify all form fields again. +
+
+ +HTML; + + $subject = Mockery::mock( Form::class )->makePartial(); + $subject->shouldAllowMockingProtectedMethods(); + + self::assertFalse( $subject->has_recaptcha() ); + + $post_content = str_replace( $recaptcha_placeholder, '', $template ); + $post_id = wp_insert_post( [ 'post_content' => $post_content ] ); + $_POST['post_id'] = $post_id; + $_POST['block_id'] = $block_id; + + self::assertFalse( $subject->has_recaptcha() ); + + $post_content = str_replace( $recaptcha_placeholder, '"reCaptchaEnable":"true",', $template ); + $post_id = wp_insert_post( [ 'post_content' => $post_content ] ); + $_POST['post_id'] = $post_id; + + self::assertTrue( $subject->has_recaptcha() ); + } +} diff --git a/.tests/php/integration/Stubs/CoBlocks_Form.php b/.tests/php/integration/Stubs/CoBlocks_Form.php new file mode 100644 index 00000000..397d8454 --- /dev/null +++ b/.tests/php/integration/Stubs/CoBlocks_Form.php @@ -0,0 +1,20 @@ +recaptcha = [ + 'siteKey' => 'some site key', + 'showForGuests' => 1, + 'showForUsers' => 1, + ]; + $this->wp_discuz = Mockery::mock( 'WpdiscuzCore' ); + $this->wp_discuz->options = $options; + + FunctionMocker::replace( + 'function_exists', + static function ( $function_name ) { + return 'wpDiscuz' === $function_name; + } + ); + FunctionMocker::replace( 'wpDiscuz', $this->wp_discuz ); + } + /** * Tear down test. * @@ -36,14 +72,29 @@ public function tearDown(): void { public function test_init_hooks(): void { $subject = new Comment(); - self::assertTrue( has_filter( 'wpdiscuz_recaptcha_site_key' ) ); self::assertSame( 11, has_action( 'wp_enqueue_scripts', [ $subject, 'enqueue_scripts' ] ) ); self::assertSame( 10, has_filter( 'wpdiscuz_form_render', [ $subject, 'add_hcaptcha' ] ) ); self::assertSame( 9, has_filter( 'preprocess_comment', [ $subject, 'verify' ] ) ); self::assertSame( 20, has_action( 'wp_head', [ $subject, 'print_inline_styles' ] ) ); + } + + /** + * Test init_hooks() without wpDiscuz. + * + * @return void + */ + public function test_init_hooks_without_wpdiscuz(): void { + FunctionMocker::replace( + 'function_exists', + static function ( $function_name ) { + return 'wpDiscuz' !== $function_name; + } + ); + + $subject = new Comment(); - self::assertSame( '', apply_filters( 'wpdiscuz_recaptcha_site_key', 'some site key' ) ); + self::assertFalse( has_action( 'wp_enqueue_scripts', [ $subject, 'enqueue_scripts' ] ) ); } /** @@ -112,19 +163,15 @@ public function test_verify(): void { $_POST['action'] = 'wpdAddComment'; - $wp_discuz = Mockery::mock( 'WpdiscuzCore' ); - - FunctionMocker::replace( 'wpDiscuz', $wp_discuz ); - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'preprocess_comment', [ $wp_discuz, 'validateRecaptcha' ] ); + add_filter( 'preprocess_comment', [ $this->wp_discuz, 'validateRecaptcha' ] ); $this->prepare_hcaptcha_request_verify( $hcaptcha_response ); $subject = new Comment(); self::assertSame( $comment_data, $subject->verify( $comment_data ) ); - self::assertFalse( has_filter( 'preprocess_comment', [ $wp_discuz, 'validateRecaptcha' ] ) ); + self::assertFalse( has_filter( 'preprocess_comment', [ $this->wp_discuz, 'validateRecaptcha' ] ) ); } /** @@ -167,12 +214,8 @@ public function test_verify_not_verified(): void { $_POST['action'] = 'wpdAddComment'; - $wp_discuz = Mockery::mock( 'WpdiscuzCore' ); - - FunctionMocker::replace( 'wpDiscuz', $wp_discuz ); - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'preprocess_comment', [ $wp_discuz, 'validateRecaptcha' ] ); + add_filter( 'preprocess_comment', [ $this->wp_discuz, 'validateRecaptcha' ] ); $this->prepare_hcaptcha_request_verify( $hcaptcha_response, false ); @@ -194,7 +237,7 @@ static function ( $name ) use ( &$die_arr ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing self::assertFalse( isset( $_POST['h-captcha-response'], $_POST['g-recaptcha-response'] ) ); self::assertSame( $expected, $die_arr ); - self::assertFalse( has_filter( 'preprocess_comment', [ $wp_discuz, 'validateRecaptcha' ] ) ); + self::assertFalse( has_filter( 'preprocess_comment', [ $this->wp_discuz, 'validateRecaptcha' ] ) ); } /** diff --git a/.tests/php/integration/WPDiscuz/SubscribeTest.php b/.tests/php/integration/WPDiscuz/SubscribeTest.php index f4c02705..b7e656a9 100644 --- a/.tests/php/integration/WPDiscuz/SubscribeTest.php +++ b/.tests/php/integration/WPDiscuz/SubscribeTest.php @@ -5,10 +5,15 @@ * @package HCaptcha\Tests */ +// phpcs:ignore Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpUndefinedClassInspection */ + namespace HCaptcha\Tests\Integration\WPDiscuz; use HCaptcha\Tests\Integration\HCaptchaWPTestCase; use HCaptcha\WPDiscuz\Subscribe; +use Mockery; +use tad\FunctionMocker\FunctionMocker; /** * Test Subscribe class. @@ -17,6 +22,40 @@ */ class SubscribeTest extends HCaptchaWPTestCase { + /** + * The wpDiscuz core class mock. + * + * @var Mockery\MockInterface|WpdiscuzCore + * @noinspection PhpPrivateFieldCanBeLocalVariableInspection + */ + private $wp_discuz; + + /** + * Setup test. + * + * @return void + */ + public function setUp(): void { + parent::setUp(); + + $options = Mockery::mock( 'WpdiscuzOptions' ); + $options->recaptcha = [ + 'siteKey' => 'some site key', + 'showForGuests' => 1, + 'showForUsers' => 1, + ]; + $this->wp_discuz = Mockery::mock( 'WpdiscuzCore' ); + $this->wp_discuz->options = $options; + + FunctionMocker::replace( + 'function_exists', + static function ( $function_name ) { + return 'wpDiscuz' === $function_name; + } + ); + FunctionMocker::replace( 'wpDiscuz', $this->wp_discuz ); + } + /** * Tear down test. * @@ -24,6 +63,8 @@ class SubscribeTest extends HCaptchaWPTestCase { */ public function tearDown(): void { unset( $_POST['h-captcha-response'], $_POST['g-recaptcha-response'] ); + + parent::tearDown(); } /** @@ -34,15 +75,12 @@ public function tearDown(): void { public function test_init_hooks(): void { $subject = new Subscribe(); - self::assertTrue( has_filter( 'wpdiscuz_recaptcha_site_key' ) ); self::assertSame( 11, has_action( 'wp_enqueue_scripts', [ $subject, 'enqueue_scripts' ] ) ); self::assertSame( 10, has_action( 'wpdiscuz_after_subscription_form', [ $subject, 'add_hcaptcha' ] ) ); self::assertSame( 9, has_action( 'wp_ajax_wpdAddSubscription', [ $subject, 'verify' ] ) ); self::assertSame( 9, has_action( 'wp_ajax_nopriv_wpdAddSubscription', [ $subject, 'verify' ] ) ); self::assertSame( 20, has_action( 'wp_head', [ $subject, 'print_inline_styles' ] ) ); - - self::assertSame( '', apply_filters( 'wpdiscuz_recaptcha_site_key', 'some site key' ) ); } /** diff --git a/.tests/php/integration/Wordfence/GeneralTest.php b/.tests/php/integration/Wordfence/GeneralTest.php index c95e758b..2fe412aa 100644 --- a/.tests/php/integration/Wordfence/GeneralTest.php +++ b/.tests/php/integration/Wordfence/GeneralTest.php @@ -16,6 +16,7 @@ use HCaptcha\Wordfence\General; use HCaptcha\WP\Login; use ReflectionException; +use tad\FunctionMocker\FunctionMocker; /** * Test General class. @@ -130,4 +131,41 @@ public function test_remove_wp_login_hcaptcha_hooks(): void { self::assertFalse( has_action( 'login_form', [ $wp_login, 'add_captcha' ] ) ); self::assertFalse( has_filter( 'wp_authenticate_user', [ $wp_login, 'check_signature' ] ) ); } + + /** + * Test print_inline_styles(). + * + * @return void + * @noinspection CssUnusedSymbol + */ + public function test_print_inline_styles(): void { + FunctionMocker::replace( + 'defined', + static function ( $constant_name ) { + return 'SCRIPT_DEBUG' === $constant_name; + } + ); + + FunctionMocker::replace( + 'constant', + static function ( $name ) { + return 'SCRIPT_DEBUG' === $name; + } + ); + + $expected = << .h-captcha { + visibility: hidden !important; +} +CSS; + $expected = "\n"; + + $subject = new General(); + + ob_start(); + + $subject->print_inline_styles(); + + self::assertSame( $expected, ob_get_clean() ); + } } diff --git a/.tests/php/integration/_bootstrap.php b/.tests/php/integration/_bootstrap.php index 0dc46ced..5352f4a0 100644 --- a/.tests/php/integration/_bootstrap.php +++ b/.tests/php/integration/_bootstrap.php @@ -28,6 +28,7 @@ 'constant', 'defined', 'filter_input', + 'function_exists', 'setcookie', 'time', 'uniqid', diff --git a/.tests/php/integration/includes/RequestTest.php b/.tests/php/integration/includes/RequestTest.php index 1790edd9..b44d8c29 100644 --- a/.tests/php/integration/includes/RequestTest.php +++ b/.tests/php/integration/includes/RequestTest.php @@ -158,6 +158,62 @@ public function test_hcap_get_error_message(): void { ); } + /** + * Test hcap_check_site_config. + * + * @return void + */ + public function test_hcap_check_site_config(): void { + add_filter( + 'pre_http_request', + static function ( $value, $parsed_args, $url ) use ( &$result ) { + if ( false !== strpos( $url, 'hcaptcha.com' ) ) { + return null === $result ? [] : [ 'body' => wp_json_encode( $result ) ]; + } + + return $value; + }, + 10, + 3 + ); + + // Cannot communicate. + $result = null; + $expected = [ + 'error' => 'Cannot communicate with hCaptcha server.', + ]; + + self::assertSame( $expected, hcap_check_site_config() ); + + // Cannot decode. + $result = []; + $expected = [ + 'error' => 'Cannot decode hCaptcha server response.', + ]; + + self::assertSame( $expected, hcap_check_site_config() ); + + // Error. + $error = 'some error'; + $result = [ + 'pass' => false, + 'error' => $error, + ]; + $expected = [ + 'error' => $error, + ]; + + self::assertSame( $expected, hcap_check_site_config() ); + + // Success. + $result = [ + 'pass' => true, + ]; + $expected = $result; + + self::assertSame( $expected, hcap_check_site_config() ); + } + /** * Test hcaptcha_request_verify(). */ @@ -213,9 +269,9 @@ public function test_hcaptcha_request_verify_not_verified_empty_body(): void { } /** - * Test hcaptcha_verify_POST() with no argument. + * Test hcaptcha_verify_post() with no argument. */ - public function test_hcaptcha_verify_POST_default_success(): void { + public function test_hcaptcha_verify_post_default_success(): void { $hcaptcha_response = 'some response'; $this->prepare_hcaptcha_request_verify( $hcaptcha_response ); @@ -224,16 +280,16 @@ public function test_hcaptcha_verify_POST_default_success(): void { } /** - * Test hcaptcha_verify_POST() with no argument. + * Test hcaptcha_verify_post() with no argument. */ - public function test_hcaptcha_verify_POST_default_empty(): void { + public function test_hcaptcha_verify_post_default_empty(): void { self::assertSame( 'Please complete the hCaptcha.', hcaptcha_verify_post() ); } /** - * Test hcaptcha_verify_POST(). + * Test hcaptcha_verify_post(). */ - public function test_hcaptcha_verify_POST(): void { + public function test_hcaptcha_verify_post(): void { $nonce_field_name = 'some nonce field'; $nonce_action_name = 'some nonce action'; @@ -251,9 +307,9 @@ public function test_hcaptcha_verify_POST(): void { } /** - * Test hcaptcha_verify_POST() not verified. + * Test hcaptcha_verify_post() not verified. */ - public function test_hcaptcha_verify_POST_not_verified(): void { + public function test_hcaptcha_verify_post_not_verified(): void { $nonce_field_name = 'some nonce field'; $nonce_action_name = 'some nonce action'; @@ -263,9 +319,9 @@ public function test_hcaptcha_verify_POST_not_verified(): void { } /** - * Test hcaptcha_verify_POST() not verified with empty POST. + * Test hcaptcha_verify_post() not verified with empty POST. */ - public function test_hcaptcha_verify_POST_not_verified_empty_POST(): void { + public function test_hcaptcha_verify_post_not_verified_empty_POST(): void { $nonce_field_name = 'some nonce field'; $nonce_action_name = 'some nonce action'; @@ -275,9 +331,9 @@ public function test_hcaptcha_verify_POST_not_verified_empty_POST(): void { } /** - * Test hcaptcha_verify_POST() not verified with logged-in user. + * Test hcaptcha_verify_post() not verified with logged-in user. */ - public function test_hcaptcha_verify_POST_not_verified_logged_in(): void { + public function test_hcaptcha_verify_post_not_verified_logged_in(): void { $nonce_field_name = 'some nonce field'; $nonce_action_name = 'some nonce action'; diff --git a/.tests/php/unit/HCaptchaTestCase.php b/.tests/php/unit/HCaptchaTestCase.php index 786c21dc..4aec706f 100644 --- a/.tests/php/unit/HCaptchaTestCase.php +++ b/.tests/php/unit/HCaptchaTestCase.php @@ -812,7 +812,10 @@ protected function get_test_integrations_form_fields(): array { 'type' => 'checkbox', 'options' => [ + 'login' => 'Login Form', + 'lost_pass' => 'Lost Password Form', 'new_topic' => 'New Topic Form', + 'register' => 'Register Form', 'reply' => 'Reply Form', ], ], diff --git a/.tests/php/unit/Helpers/PagesTest.php b/.tests/php/unit/Helpers/PagesTest.php new file mode 100644 index 00000000..ec3d3f51 --- /dev/null +++ b/.tests/php/unit/Helpers/PagesTest.php @@ -0,0 +1,266 @@ +mock_filter_input(); + + // Not in admin. + FunctionMocker::replace( 'is_admin', false ); + self::assertFalse( Pages::is_cf7_edit_page() ); + + // Not a CF7 page. + FunctionMocker::replace( 'is_admin', true ); + self::assertFalse( Pages::is_cf7_edit_page() ); + + $_GET['page'] = 'some'; + self::assertFalse( Pages::is_cf7_edit_page() ); + + $_GET['page'] = 'wpcf7-new'; + self::assertTrue( Pages::is_cf7_edit_page() ); + + $_GET['page'] = 'wpcf7'; + self::assertFalse( Pages::is_cf7_edit_page() ); + + $_GET['post'] = 'some'; + self::assertTrue( Pages::is_cf7_edit_page() ); + } + + /** + * Test is_elementor_pro_edit_page(). + * + * @return void + */ + public function test_is_elementor_pro_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_elementor_pro_edit_page() ); + + // Request 1. + $_GET['post'] = 'some'; + $_SERVER['REQUEST_URI'] = '/wp-admin/post.php'; + $_GET['action'] = 'elementor'; + + self::assertTrue( Pages::is_elementor_pro_edit_page() ); + + // Request 2. + unset( $_GET, $_POST, $_SERVER['REQUEST_URI'] ); + + $_GET['preview_id'] = 'some'; + $_GET['preview_nonce'] = 'some'; + $_GET['preview'] = 'true'; + + self::assertTrue( Pages::is_elementor_pro_edit_page() ); + + // Request 3. + unset( $_GET, $_POST ); + + $_POST['action'] = 'elementor_ajax'; + + self::assertTrue( Pages::is_elementor_pro_edit_page() ); + } + + /** + * Test is_gravity_edit_page(). + * + * @return void + */ + public function test_is_gravity_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_gravity_edit_page() ); + + $_GET['id'] = 'some'; + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'gf_edit_forms'; + + self::assertTrue( Pages::is_gravity_edit_page() ); + } + + /** + * Test is_fluent_edit_page(). + * + * @return void + */ + public function test_is_fluent_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_fluent_edit_page() ); + + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'fluent_forms_settings'; + + self::assertTrue( Pages::is_fluent_edit_page() ); + } + + /** + * Test is_forminator_edit_page(). + * + * @return void + */ + public function test_is_forminator_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_forminator_edit_page() ); + + $_GET['id'] = 'some'; + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'forminator-cform-wizard'; + + self::assertTrue( Pages::is_forminator_edit_page() ); + } + + /** + * Test is_formidable_edit_page(). + * + * @return void + */ + public function test_is_formidable_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_formidable_forms_edit_page() ); + + // Request 1. + $_GET['frm_action'] = 'some'; + $_GET['id'] = 'some'; + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'formidable'; + + self::assertTrue( Pages::is_formidable_forms_edit_page() ); + + // Request 2. + unset( $_GET, $_POST, $_SERVER['REQUEST_URI'] ); + + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'formidable-styles'; + + self::assertTrue( Pages::is_formidable_forms_edit_page() ); + + // Request 3. + unset( $_GET, $_POST, $_SERVER['REQUEST_URI'] ); + + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'formidable-settings'; + + self::assertTrue( Pages::is_formidable_forms_edit_page() ); + } + + /** + * Test is_is_ninja_edit_page(). + * + * @return void + */ + public function test_is_ninja_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_ninja_edit_page() ); + + $_GET['form_id'] = 'some'; + $_SERVER['REQUEST_URI'] = '/wp-admin/admin.php'; + $_GET['page'] = 'ninja-forms'; + + self::assertTrue( Pages::is_ninja_edit_page() ); + } + + /** + * Test is_wpforms_edit_page(). + * + * @return void + */ + public function test_is_wpforms_edit_page(): void { + $this->mock_filter_input(); + + self::assertFalse( Pages::is_wpforms_edit_page() ); + + // Request 1. + $_GET['page'] = 'wpforms-settings'; + $_GET['view'] = 'captcha'; + + FunctionMocker::replace( 'is_admin', true ); + + self::assertTrue( Pages::is_wpforms_edit_page() ); + + // Request 2. + unset( $_GET ); + + $_GET['wpforms_form_preview'] = 'some'; + + FunctionMocker::replace( 'is_admin', false ); + + self::assertTrue( Pages::is_wpforms_edit_page() ); + } + + /** + * Mock filter_input(). + * + * @return void + */ + private function mock_filter_input(): void { + FunctionMocker::replace( + '\HCaptcha\Helpers\Request::filter_input', + static function ( $type, $var_name ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + switch ( $type ) { + case INPUT_GET: + return $_GET[ $var_name ] ?? ''; + case INPUT_POST: + return $_POST[ $var_name ] ?? ''; + case INPUT_SERVER: + return $_SERVER[ $var_name ] ?? ''; + default: + return ''; + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + } + ); + } +} diff --git a/.tests/php/unit/Helpers/RequestTest.php b/.tests/php/unit/Helpers/RequestTest.php index 3b6ca879..5409fafb 100644 --- a/.tests/php/unit/Helpers/RequestTest.php +++ b/.tests/php/unit/Helpers/RequestTest.php @@ -15,7 +15,6 @@ use HCaptcha\Helpers\Request; use HCaptcha\Tests\Unit\HCaptchaTestCase; use Mockery; -use Mockery\Mock; use tad\FunctionMocker\FunctionMocker; use WP_Mock; @@ -31,7 +30,7 @@ class RequestTest extends HCaptchaTestCase { * Tear down test. */ public function tearDown(): void { - unset( $_SERVER['REQUEST_URI'], $_GET['wc-ajax'], $_GET['rest_route'], $_SERVER['REQUEST_METHOD'] ); + unset( $_GET, $_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD'] ); parent::tearDown(); } @@ -148,8 +147,6 @@ public function test_is_wp_ajax(): void { */ public function test_is_rest(): void { // No REQUEST_URI. - unset( $_SERVER['REQUEST_URI'] ); - self::assertFalse( Request::is_rest() ); // Case #1. @@ -245,4 +242,46 @@ public function test_is_post(): void { self::assertTrue( Request::is_post() ); } + + /** + * Test filter_input(). + * + * @return void + */ + public function test_filter_input(): void { + WP_Mock::passthruFunction( 'wp_unslash' ); + WP_Mock::passthruFunction( 'sanitize_text_field' ); + + // Test with GET. + $type = INPUT_GET; + $var_name = 'some_var'; + $value = 'some_value'; + $_GET[ $var_name ] = $value; + + self::assertSame( '', Request::filter_input( $type, 'wrong_var_name' ) ); + self::assertSame( $value, Request::filter_input( $type, $var_name ) ); + + // Test with POST. + unset( $_GET ); + $type = INPUT_POST; + $_POST[ $var_name ] = $value; + + self::assertSame( '', Request::filter_input( $type, 'wrong_var_name' ) ); + self::assertSame( $value, Request::filter_input( $type, $var_name ) ); + + // Test with SERVER. + unset( $_POST ); + $type = INPUT_SERVER; + $_SERVER[ $var_name ] = $value; + + self::assertSame( '', Request::filter_input( $type, 'wrong_var_name' ) ); + self::assertSame( $value, Request::filter_input( $type, $var_name ) ); + + // Test with a wrong input type. + unset( $_SERVER[ $var_name ] ); + $type = 999; + + self::assertSame( '', Request::filter_input( $type, 'wrong_var_name' ) ); + self::assertSame( '', Request::filter_input( $type, $var_name ) ); + } } diff --git a/.tests/php/unit/Settings/Abstracts/SettingsBaseTest.php b/.tests/php/unit/Settings/Abstracts/SettingsBaseTest.php index be81b495..8b5eddd2 100644 --- a/.tests/php/unit/Settings/Abstracts/SettingsBaseTest.php +++ b/.tests/php/unit/Settings/Abstracts/SettingsBaseTest.php @@ -64,6 +64,7 @@ public function test_constructor( bool $is_tab, string $admin_mode, bool $is_net 'radio' => [ $subject, 'print_radio_field' ], 'select' => [ $subject, 'print_select_field' ], 'multiple' => [ $subject, 'print_multiple_select_field' ], + 'file' => [ $subject, 'print_file_field' ], 'table' => [ $subject, 'print_table_field' ], 'button' => [ $subject, 'print_button_field' ], ]; @@ -161,12 +162,33 @@ static function ( $name ) use ( $script_debug ) { } ); + WP_Mock::userFunction( 'is_admin' )->once()->andReturn( true ); + $subject->init(); $min_suffix = $script_debug ? '' : '.min'; self::assertSame( $min_suffix, $this->get_protected_property( $subject, 'min_suffix' ) ); } + /** + * Test init() when not in admin. + * + * @throws ReflectionException ReflectionException. + */ + public function test_init_when_not_in_admin(): void { + $subject = Mockery::mock( SettingsBase::class )->makePartial(); + $subject->shouldAllowMockingProtectedMethods(); + $subject->shouldReceive( 'form_fields' )->once(); + $subject->shouldReceive( 'init_settings' )->once(); + $subject->shouldReceive( 'is_main_menu_page' )->never(); + $subject->shouldReceive( 'is_tab_active' )->never(); + $subject->shouldReceive( 'init_hooks' )->never(); + + WP_Mock::userFunction( 'is_admin' )->once()->andReturn( false ); + + $subject->init(); + } + /** * Data provider for test_init(). * @@ -1196,6 +1218,19 @@ public function test_tabs_callback( bool $is_network_wide ): void { self::assertSame( $expected, ob_get_clean() ); } + /** + * Test tabs_callback() when no tabs. + * + * @throws ReflectionException ReflectionException. + */ + public function test_tabs_callback_when_no_tabs(): void { + $subject = Mockery::mock( SettingsBase::class )->makePartial(); + + ob_start(); + $subject->tabs_callback(); + self::assertSame( '', ob_get_clean() ); + } + /** * Data provider for test_tabs_callback(). * @@ -1710,6 +1745,7 @@ public function test_field_callback( array $arguments, string $expected ): void 'radio' => [ $subject, 'print_radio_field' ], 'select' => [ $subject, 'print_select_field' ], 'multiple' => [ $subject, 'print_multiple_select_field' ], + 'file' => [ $subject, 'print_file_field' ], 'table' => [ $subject, 'print_table_field' ], 'button' => [ $subject, 'print_button_field' ], ]; @@ -1781,6 +1817,7 @@ public function dp_test_field_callback(): array { $this->dp_radio_field_callback(), $this->dp_select_field_callback(), $this->dp_multiple_field_callback(), + $this->dp_file_field_callback(), $this->dp_table_field_callback(), $this->dp_button_field_callback() ); @@ -2240,6 +2277,64 @@ private function dp_not_empty_multiple_field_callback(): array { ]; } + /** + * Data provider for file field. + * + * @return array + */ + private function dp_file_field_callback(): array { + return [ + 'File' => [ + [ + 'label' => 'file', + 'section' => 'some_section', + 'type' => 'file', + 'placeholder' => '', + 'helper' => '', + 'supplemental' => '', + 'default' => 1, + 'field_id' => 'some_id', + 'disabled' => false, + 'multiple' => false, + 'accept' => '', + ], + '', + ], + 'File disabled' => [ + [ + 'label' => 'file', + 'section' => 'some_section', + 'type' => 'file', + 'placeholder' => '', + 'helper' => '', + 'supplemental' => '', + 'default' => 1, + 'field_id' => 'some_id', + 'disabled' => true, + 'multiple' => false, + 'accept' => '', + ], + '', + ], + 'File multiple accept xml' => [ + [ + 'label' => 'file', + 'section' => 'some_section', + 'type' => 'file', + 'placeholder' => '', + 'helper' => '', + 'supplemental' => '', + 'default' => 1, + 'field_id' => 'some_id', + 'disabled' => false, + 'multiple' => true, + 'accept' => '.xml', + ], + '', + ], + ]; + } + /** * Data provider for table field. * @@ -2749,6 +2844,23 @@ public function dp_test_pre_update_option_filter(): array { '_network_wide' => [], ], ], + [ + [ + 'some_file' => [ + 'label' => 'some field', + 'section' => 'some_section', + 'type' => 'file', + 'placeholder' => '', + 'helper' => '', + 'supplemental' => '', + 'default' => [ '' ], + 'disabled' => false, + ], + ], + [ 'some_file' => 'a.xml' ], + [ 'some_file' => 'b.xml' ], + [ '_network_wide' => [] ], + ], [ [], [ diff --git a/.tests/php/unit/Settings/IntegrationsTest.php b/.tests/php/unit/Settings/IntegrationsTest.php index f3e4be74..99dbd362 100644 --- a/.tests/php/unit/Settings/IntegrationsTest.php +++ b/.tests/php/unit/Settings/IntegrationsTest.php @@ -79,6 +79,7 @@ static function ( $function_name ) { WP_Mock::userFunction( 'get_plugins' )->andReturn( $plugins ); WP_Mock::userFunction( 'wp_get_themes' )->andReturn( $themes ); + WP_Mock::userFunction( 'is_admin' )->andReturn( true ); $method = 'init'; $subject->$method(); diff --git a/.tests/php/unit/Settings/SettingsTest.php b/.tests/php/unit/Settings/SettingsTest.php index f88876e0..ecd21ad5 100644 --- a/.tests/php/unit/Settings/SettingsTest.php +++ b/.tests/php/unit/Settings/SettingsTest.php @@ -120,6 +120,25 @@ public function test_get_tabs(): void { self::assertSame( $tabs, $subject->get_tabs() ); } + /** + * Test get_tab(). + * + * @throws ReflectionException ReflectionException. + */ + public function test_get_tab(): void { + $general = Mockery::mock( General::class )->makePartial(); + $integrations = Mockery::mock( Integrations::class )->makePartial(); + + $subject = Mockery::mock( Settings::class )->makePartial(); + + self::assertNull( $subject->get_tab( Integrations::class ) ); + + $tabs = [ $general, $integrations ]; + $this->set_protected_property( $subject, 'tabs', $tabs ); + + self::assertSame( $integrations, $subject->get_tab( Integrations::class ) ); + } + /** * Test get_active_tab_name(). * diff --git a/.tests/php/unit/Settings/SystemInfoTest.php b/.tests/php/unit/Settings/SystemInfoTest.php index 71726f06..b2f82b6c 100644 --- a/.tests/php/unit/Settings/SystemInfoTest.php +++ b/.tests/php/unit/Settings/SystemInfoTest.php @@ -338,7 +338,10 @@ public function test_get_system_info(): void { Back In Stock Notifier: Back In Stock Notifier Form: Off bbPress: + Login Form: Off + Lost Password Form: Off New Topic Form: On + Register Form: Off Reply Form: On Beaver Builder: Contact Form: On diff --git a/.wordpress-org/banner-1544x500.jpg b/.wordpress-org/banner-1544x500.jpg new file mode 100644 index 00000000..a4434c54 Binary files /dev/null and b/.wordpress-org/banner-1544x500.jpg differ diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png deleted file mode 100644 index b8ded94c..00000000 Binary files a/.wordpress-org/banner-1544x500.png and /dev/null differ diff --git a/.wordpress-org/banner-772x250.jpg b/.wordpress-org/banner-772x250.jpg index d7c85c2e..b8cec06d 100644 Binary files a/.wordpress-org/banner-772x250.jpg and b/.wordpress-org/banner-772x250.jpg differ diff --git a/assets/css/settings-base.css b/assets/css/settings-base.css index db090e9d..77415ed8 100644 --- a/assets/css/settings-base.css +++ b/assets/css/settings-base.css @@ -16,6 +16,9 @@ body.settings_page_hcaptcha { } .hcaptcha-settings-tabs { + position: sticky; + top: 0; + z-index: 2; /* +1 to .hcaptcha-header bar z-index, to overlap a bit */ display: flex; flex-wrap: wrap; justify-content: space-between; @@ -45,10 +48,15 @@ body.settings_page_hcaptcha { } .hcaptcha-header-bar { + position: sticky; + top: 60px; + z-index: 1; + background: #f0f2f5; display: flex; justify-content: space-between; align-items: center; - margin: 0; + margin: 0 -20px; + padding: 0 20px; } #hcaptcha-options h2 { diff --git a/assets/js/hcaptcha-mailpoet.js b/assets/js/hcaptcha-mailpoet.js index c6d7a7ba..e76a1ee4 100644 --- a/assets/js/hcaptcha-mailpoet.js +++ b/assets/js/hcaptcha-mailpoet.js @@ -14,9 +14,10 @@ return; } - const urlParams = new URLSearchParams( data ); - const formId = urlParams.get( 'data[form_id]' ); - const $form = $( 'input[name="data[form_id]"][value=' + formId + ']' ).parent( 'form' ); + // eslint-disable-next-line @wordpress/no-global-active-element + const eventTarget = options.context || document.activeElement; + const $form = $( eventTarget.closest( 'form' ) ); + let response = $form.find( '[name="h-captcha-response"]' ).val(); response = response ? response : ''; let id = $form.find( '[name="hcaptcha-widget-id"]' ).val(); diff --git a/assets/js/integrations.js b/assets/js/integrations.js index b2b63c22..50136931 100644 --- a/assets/js/integrations.js +++ b/assets/js/integrations.js @@ -24,12 +24,24 @@ * @param {jQuery} $ The jQuery instance. */ const integrations = function( $ ) { + const adminBar = document.querySelector( '#wpadminbar' ); + const tabs = document.querySelector( '.hcaptcha-settings-tabs' ); + const headerBar = document.querySelector( '.hcaptcha-header-bar' ); const msgSelector = '#hcaptcha-message'; let $message = $( msgSelector ); const $wpWrap = $( '#wpwrap' ); const $adminmenuwrap = $( '#adminmenuwrap' ); const $search = $( '#hcaptcha-integrations-search' ); + function getStickyHeight() { + const isAbsolute = adminBar ? window.getComputedStyle( adminBar ).position === 'absolute' : true; + const adminBarHeight = ( adminBar && ! isAbsolute ) ? adminBar.offsetHeight : 0; + const tabsHeight = tabs ? tabs.offsetHeight : 0; + const headerBarHeight = headerBar ? headerBar.offsetHeight : 0; + + return adminBarHeight + tabsHeight + headerBarHeight; + } + function clearMessage() { $message.remove(); $( '
' ).insertAfter( '.hcaptcha-header-bar' ); @@ -222,7 +234,6 @@ const integrations = function( $ ) { } const $table = $( '.form-table' ).eq( activate ? 0 : 1 ); - const top = $wpWrap.position().top; swapThemes( activate, entity, newTheme ); insertIntoTable( $table, alt, $tr ); @@ -231,7 +242,7 @@ const integrations = function( $ ) { $( 'html, body' ).animate( { - scrollTop: $tr.offset().top - top - $message.outerHeight(), + scrollTop: $tr.offset().top - getStickyHeight(), }, 1000 ); diff --git a/assets/js/settings-base.js b/assets/js/settings-base.js index 97849a98..ecb1b7a2 100644 --- a/assets/js/settings-base.js +++ b/assets/js/settings-base.js @@ -9,8 +9,25 @@ const settingsBase = function( $ ) { const h2Selector = '.hcaptcha-header h2'; const msgSelector = '#hcaptcha-message'; - // Move WP notices to the message area. - $( h2Selector ).siblings().appendTo( msgSelector ); + function setHeaderBarTop() { + const adminBar = document.querySelector( '#wpadminbar' ); + const tabs = document.querySelector( '.hcaptcha-settings-tabs' ); + const headerBar = document.querySelector( '.hcaptcha-header-bar' ); + + const isAbsolute = adminBar ? window.getComputedStyle( adminBar ).position === 'absolute' : true; + const adminBarHeight = ( adminBar && ! isAbsolute ) ? adminBar.offsetHeight : 0; + const tabsHeight = tabs ? tabs.offsetHeight : 0; + // The -1 to put header bar a bit under tabs. It is a precaution when heights are in fractional pixels. + const totalHeight = adminBarHeight + tabsHeight - 1; + + if ( tabs ) { + tabs.style.top = `${ adminBarHeight }px`; + } + + if ( headerBar ) { + headerBar.style.top = `${ totalHeight }px`; + } + } /** * Highlight element if hash is present in the URL. @@ -42,6 +59,15 @@ const settingsBase = function( $ ) { } } + // Move WP notices to the message area. + $( h2Selector ).siblings().appendTo( msgSelector ); + + window.addEventListener( 'resize', function() { + setHeaderBarTop(); + } ); + + setHeaderBarTop(); + highLight(); }; diff --git a/changelog.txt b/changelog.txt index b4f3e57a..63332cac 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,27 @@ += 4.5.0 = +* Added support for Jetpack forms in block theme templates. +* Added support for bbPress Login, Register and Lost Password forms. +* Added the second argument $atts to the 'hcap_hcaptcha_content' filter. +* Added support for MailPoet forms at any placement. +* Added the ability to have multiple MailPoet forms on the same page. +* Improved UX of the Integrations page. +* Fixed error messaging when there are several Jetpack forms on the same page. +* Fixed unconditional forcing hCaptcha in Jetpack forms. +* Fixed appearance of Beaver Builder editor with "Turn Off When Logged In" setting. +* Fixed appearance of Contact Form 7 editor with "Turn Off When Logged In" setting. +* Fixed appearance of Essential Addons editor with "Turn Off When Logged In" setting. +* Fixed appearance of Gravity Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of Fluent Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of Forminator editor with "Turn Off When Logged In" setting. +* Fixed appearance of Formidable Forms with "Turn Off When Logged In" setting. +* Fixed appearance of Ninja Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of WPForms editor with "Turn Off When Logged In" setting. +* Fixed fatal error on Gravity Forms Entries page. +* Fixed Elementor preview. +* Fixed Ninja Forms preview. +* Fixed hCaptcha nonce error on MailPoet admin pages. +* Fixed frontend error when recaptcha was activated in wpDiscuz. + = 4.4.0 = * Added compatibility with Contact Form 7 Stripe integration. * Added compatibility with WPS Hide Login plugin. diff --git a/composer.json b/composer.json index 3f8ac607..d8f657b5 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "codeception/module-webdriver": "^1.4.1", "lucatume/function-mocker": "dev-master", "lucatume/wp-browser": "^3.6.5", - "squizlabs/php_codesniffer": "^3.10.1", + "squizlabs/php_codesniffer": "^3.10.2", "phpcompatibility/php-compatibility": "^9.3.5", "phpcompatibility/phpcompatibility-wp": "^2.1.5", "wp-coding-standards/wpcs": "^3.1.0" diff --git a/hcaptcha.php b/hcaptcha.php index f94b2084..77576458 100644 --- a/hcaptcha.php +++ b/hcaptcha.php @@ -10,7 +10,7 @@ * Plugin Name: hCaptcha for WP * 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.4.0 + * Version: 4.5.0 * Requires at least: 5.3 * Requires PHP: 7.2 * Author: hCaptcha @@ -39,7 +39,7 @@ /** * Plugin version. */ -const HCAPTCHA_VERSION = '4.4.0'; +const HCAPTCHA_VERSION = '4.5.0'; /** * Path to the plugin dir. diff --git a/readme.txt b/readme.txt index 8a3392ff..5c51905c 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: captcha, hcaptcha, antispam, abuse, protect form Requires at least: 5.3 Tested up to: 6.6 Requires PHP: 7.2 -Stable tag: 4.4.0 +Stable tag: 4.5.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -16,7 +16,7 @@ Enables hCaptcha integration with WordPress and popular plugins. Need to keep out bots? hCaptcha protects privacy while offering better protection against spam and abuse. Help build a better web. -[The hCaptcha for WP many advantages over similar captcha plugins](https://kagg.eu/why-choose-hcaptcha-for-wp/). +hCaptcha for WP [makes security easy](https://www.hcaptcha.com/integration-hcaptcha-for-wp) with broad integration support, detailed analytics, and strong protection. Start protecting logins, forms, and more in minutes. == How hCaptcha Works == @@ -36,7 +36,7 @@ To use this plugin, install it and enter your sitekey and secret in the Settings 4. Contact Form 7 with hCaptcha. 5. Contact Form 7 live form in the admin editor. 6. Elementor Pro Form. -7. Elementor Pro From in admin editor. +7. Elementor Pro Form in admin editor. 8. General settings page. 9. Integrations settings page. 10. Activating plugin from the Integration settings page. @@ -196,7 +196,7 @@ Back In Stock Notifier BBPress `$source: 'bbpress/bbpress.php'` -`$form_id: 'new_topic' or 'reply'` +`$form_id: 'new_topic', 'reply', 'login', 'register' or 'lost_password'` Beaver Builder `$source: 'bb-plugin/fl-builder.php'` @@ -232,7 +232,7 @@ Elementor Pro Jetpack `$source: 'jetpack/jetpack.php'` -`$form_id: 'contact'` +`$form_id: 'contact_$form_hash'` Kadence Form `$source: 'kadence-blocks/kadence-blocks.php'` @@ -394,7 +394,7 @@ add_filter( 'hcap_language', 'my_hcap_language' ); = How to whitelist certain IPs = -You can use the following filter: +You can use the following filter. It should be added to your plugin's (or mu-plugin's) main file. This filter won't work being added to a theme's functions.php file. ` /** @@ -494,7 +494,7 @@ If this feature is enabled, anonymized statistics on your plugin configuration, * Asgaros Forum New Topic and Reply Form * Avada Form * Back In Stock Notifier Form -* bbPress New Topic and Reply Forms +* bbPress New Topic, Reply, Login, Register and Lost Password Forms * Beaver Builder Contact and Login Forms * BuddyPress — Create Group and Registration Forms * Classified Listing Contact, Login, Lost Password, and Listing Register Forms @@ -565,6 +565,30 @@ Instructions for popular native integrations are below: == Changelog == += 4.5.0 = +* Added support for Jetpack forms in block theme templates. +* Added support for bbPress Login, Register and Lost Password forms. +* Added the second argument $atts to the 'hcap_hcaptcha_content' filter. +* Added support for MailPoet forms at any placement. +* Added the ability to have multiple MailPoet forms on the same page. +* Improved UX of the Integrations page. +* Fixed error messaging when there are several Jetpack forms on the same page. +* Fixed unconditional forcing hCaptcha in Jetpack forms. +* Fixed appearance of Beaver Builder editor with "Turn Off When Logged In" setting. +* Fixed appearance of Contact Form 7 editor with "Turn Off When Logged In" setting. +* Fixed appearance of Essential Addons editor with "Turn Off When Logged In" setting. +* Fixed appearance of Gravity Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of Fluent Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of Forminator editor with "Turn Off When Logged In" setting. +* Fixed appearance of Formidable Forms with "Turn Off When Logged In" setting. +* Fixed appearance of Ninja Forms editor with "Turn Off When Logged In" setting. +* Fixed appearance of WPForms editor with "Turn Off When Logged In" setting. +* Fixed fatal error on Gravity Forms Entries page. +* Fixed Elementor preview. +* Fixed Ninja Forms preview. +* Fixed hCaptcha nonce error on MailPoet admin pages. +* Fixed frontend error when recaptcha was activated in wpDiscuz. + = 4.4.0 = * Added compatibility with Contact Form 7 Stripe integration. * Added compatibility with WPS Hide Login plugin. diff --git a/src/php/BBPress/Login.php b/src/php/BBPress/Login.php new file mode 100644 index 00000000..bf780b9e --- /dev/null +++ b/src/php/BBPress/Login.php @@ -0,0 +1,73 @@ +is_login_limit_exceeded() ) { + return $output; + } + + $hcaptcha = ''; + + // Check the login status, because class is always loading when bbPress is active. + if ( hcaptcha()->settings()->is( 'bbp_status', 'login' ) ) { + ob_start(); + + $this->add_captcha(); + $hcaptcha = (string) ob_get_clean(); + } + + ob_start(); + + /** + * Display hCaptcha signature. + */ + do_action( 'hcap_signature' ); + + $signatures = (string) ob_get_clean(); + + $pattern = '/(