diff --git a/README.md b/README.md index c4145c6..811332a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ ### Fluent Forms +### Gravity Forms + +### Forminator + ## Advance Features * Able to trigger adCAPTCHA on the "Place order" button. diff --git a/adcaptcha.php b/adcaptcha.php index c343201..96ac568 100644 --- a/adcaptcha.php +++ b/adcaptcha.php @@ -2,7 +2,7 @@ /** * Plugin Name: adCAPTCHA for WordPress * Description: Secure your site. Elevate your brand. Boost Ad Revenue. - * Version: 1.6.0 + * Version: 1.7.0 * Requires at least: 6.4.2 * Requires PHP: 7.4 * Author: adCAPTCHA @@ -21,7 +21,7 @@ use AdCaptcha\Instantiate; -const PLUGIN_VERSION_ADCAPTCHA = '1.6.0'; +const PLUGIN_VERSION_ADCAPTCHA = '1.7.0'; define('ADCAPTCHA_ERROR_MESSAGE', __( 'Please complete the I am human box.', 'adcaptcha' )); if ( ! function_exists( 'adcaptcha_uninstall' ) ) { diff --git a/assets/adcaptcha_icon.png b/assets/adcaptcha_icon.png new file mode 100644 index 0000000..337652f Binary files /dev/null and b/assets/adcaptcha_icon.png differ diff --git a/assets/forminator.jpeg b/assets/forminator.jpeg new file mode 100644 index 0000000..65cdfae Binary files /dev/null and b/assets/forminator.jpeg differ diff --git a/assets/gravity-forms_logo.svg b/assets/gravity-forms_logo.svg new file mode 100644 index 0000000..7a60671 --- /dev/null +++ b/assets/gravity-forms_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/readme.txt b/readme.txt index 8fc40c7..3cfafc9 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Contributors: adCAPTCHA Tags: spam, anti-spam, block bots, security, adCAPTCHA Requires at least: 6.0 Tested up to: 6.5.2 -Stable tag: 1.6.0 +Stable tag: 1.7.0 Requires PHP: 7.4 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -46,6 +46,10 @@ adCAPTCHA offers a unique proposition in the digital space by combining Security **Fluent Forms** +**Gravity Forms** + +**Forminator** + == Advance Features == **Woocommerce** @@ -157,3 +161,6 @@ During verification, the adCAPTCHA service may briefly receive the user's IP add = 1.6.0 = - Added feature to disable the WooCommerce checkout endpoint. + += 1.7.0 = +- Feature: Added support for Gravity Forms and Forminator. diff --git a/src/Instantiate.php b/src/Instantiate.php index 99b584c..c300fdc 100644 --- a/src/Instantiate.php +++ b/src/Instantiate.php @@ -17,6 +17,8 @@ use AdCaptcha\Plugin\WPForms\Forms as WPForms; use AdCaptcha\Plugin\Elementor\Forms as Elementor; use AdCaptcha\Plugin\FluentForms\Forms as FluentForms; +use AdCaptcha\Plugin\Forminator\Forms as Forminator; +use AdCaptcha\Plugin\GravityForms\Forms as GravityForms; class Instantiate { @@ -83,6 +85,15 @@ public function setup() { 'instance' => FluentForms::class, 'plugin' => [ 'fluentform/fluentform.php' ], ], + + 'Forminator_Forms' => [ + 'instance' => Forminator::class, + 'plugin' => [ 'forminator/forminator.php' ], + ], + 'GravityForms_Forms' => [ + 'instance' => GravityForms::class, + 'plugin' => [ 'gravityforms/gravityforms.php' ] + ] ]; $selected_plugins = get_option('adcaptcha_selected_plugins') ? get_option('adcaptcha_selected_plugins') : array(); diff --git a/src/Plugin/Forminator/Forms.php b/src/Plugin/Forminator/Forms.php new file mode 100644 index 0000000..869c461 --- /dev/null +++ b/src/Plugin/Forminator/Forms.php @@ -0,0 +1,85 @@ +verify = new Verify(); + } + + public function setup() { + add_action('forminator_before_form_render', [$this, 'before_form_render'], 10, 5); + add_action('forminator_before_form_render', [AdCaptcha::class, 'enqueue_scripts'], 9); + add_action('forminator_before_form_render', [Verify::class, 'get_success_token'], 9); + add_filter('forminator_render_button_markup', [$this, 'captcha_trigger_filter'], 10, 2); + add_action( 'wp_enqueue_scripts', [ $this, 'reset_captcha_script' ], 9 ); + add_filter('forminator_cform_form_is_submittable', [$this, 'verify'], 10, 3); + } + + public function before_form_render($id, $form_type, $post_id, $form_fields, $form_settings) { + $this->has_captcha = $this->has_adcaptcha_field($form_fields); + } + + private function has_adcaptcha_field($form_fields) { + foreach ($form_fields as $field) { + if (!empty($field['type']) && $field['type'] === 'captcha' && !empty($field['captcha_provider']) && $field['captcha_provider'] === 'adcaptcha') { + return true; + } + } + return false; + } + + public function captcha_trigger_filter($html, string $button) { + if ($this->has_captcha) { + return $html; + } + return AdCaptcha::ob_captcha_trigger() . $html; + } + + + public function reset_captcha_script() { + add_action('wp_footer', function() { + echo ""; + }); + } + + public function verify($can_show, $form_id, $field_data_array) { + $successToken = sanitize_text_field(wp_unslash($_POST['adcaptcha_successToken'] ?? '')); + if (empty($successToken)) { + return [ + 'can_submit' => false, + 'error' => __('Please complete the AdCaptcha verification.', 'adcaptcha'), + ]; + } + $response = $this->verify->verify_token($successToken); + if (!$response) { + return [ + 'can_submit' => false, + 'error' => __('AdCaptcha verification failed. Please try again.', 'adcaptcha'), + ]; + } + return $can_show; + } +} diff --git a/src/Plugin/GravityForms/Field.php b/src/Plugin/GravityForms/Field.php new file mode 100644 index 0000000..f607cf4 --- /dev/null +++ b/src/Plugin/GravityForms/Field.php @@ -0,0 +1,237 @@ +type = 'adcaptcha'; + $this->verify = new Verify(); + $this->setup(); + } + + private function setup(): void { + + if (!class_exists('GF_Fields')) { + return; + } + + if (GF_Fields::get('adcaptcha')) { + return; + } + + try { + if (!GF_Fields::get('adcaptcha')) { + GF_Fields::register($this); + } + } catch (Exception $e) { + throw $e; + } + + $this->setup_hooks(); + } + + private function setup_hooks(): void { + add_action('wp_enqueue_scripts', [AdCaptcha::class, 'enqueue_scripts'], 9); + add_action( 'wp_enqueue_scripts', [ Verify::class, 'get_success_token' ] ); + add_filter('gform_field_groups_form_editor', [$this, 'add_to_field_groups']); + add_filter('gform_field_content', [$this, 'modify_gform_field_content'], 10, 2); + add_filter('gform_validation', [$this, 'verify_captcha']); + add_action('admin_head', [$this, 'custom_admin_field_icon_style']); + add_action('admin_init', [$this, 'update_adcaptcha_label']); + add_action('admin_footer', [$this, 'enqueue_admin_script']); + add_filter('gform_pre_render', [$this, 'handle_adcaptcha_token']); + add_action('gform_preview_body_open', [$this, 'enqueue_preview_scripts']); + } + + public function add_to_field_groups($field_groups): array { + foreach ($field_groups['advanced_fields']['fields'] as $field) { + if ($field['data-type'] === 'adcaptcha') { + return $field_groups; + } + } + $field_groups['advanced_fields']['fields'][] = [ + 'data-type' => (string) $this->type, + 'value' => $this->get_form_editor_field_title(), + 'label' => $this->get_form_editor_field_title(), + ]; + + return $field_groups; + } + + public function modify_gform_field_content($content, $field) { + if ($field->type === 'adcaptcha') { + return str_replace($field->get_field_label(false, ''), '', $content); + } + return $content; + } + + public function verify_captcha($validation_result) { + $form = $validation_result['form']; + $is_valid = $validation_result['is_valid']; + foreach ($form['fields'] as &$field) { + if ($field->type === 'adcaptcha') { + $successToken = sanitize_text_field(wp_unslash($_POST['adcaptcha_successToken'] ?? '')); + if (empty($successToken) || trim($successToken) === '') { + $field->failed_validation = true; + $field->validation_message = __('Incomplete CAPTCHA, Please try again.', 'adcaptcha'); + $is_valid = false; + } + if($is_valid) { + $response = $this->verify->verify_token($successToken); + if (!$response) { + $field->failed_validation = true; + $field->validation_message = __('Invalid token.', 'adcaptcha'); + $is_valid = false; + } + } + } + } + $validation_result['is_valid'] = $is_valid; + $validation_result['form'] = $form; + return $validation_result; + } + + public function custom_admin_field_icon_style() { + echo ''; + } + + public function update_adcaptcha_label() { + $forms = GFAPI::get_forms(); + if (!$forms || !is_array($forms)) { + return; + } + foreach ($forms as $form) { + $updated = false; + foreach ($form['fields'] as &$field) { + if ($field->type === 'adcaptcha' && $field->label !== __('adCAPTCHA', 'adcaptcha')) { + $field->label = __('adCAPTCHA', 'adcaptcha'); + $updated = true; + } + } + if ($updated) { + GFAPI::update_form($form); + } + } + } + + public function enqueue_admin_script() { + ?> + + + document.addEventListener('DOMContentLoaded', function() { + let adCaptchaField = document.querySelector('.adcaptcha_successToken'); + if (adCaptchaField) { + setTimeout(function() { + if (window.adcap) { + window.adcap.setVerificationState('success'); + adCaptchaField.value = '{$successToken}'; + } + }, 500); + } + }); + "; + } + return $form; + } + + public function enqueue_preview_scripts($form_id) { + echo ""; + } + + public function get_field_input($form, $value = '', $entry = null) { + $form_id = $form['id'] ?? null; + if ($form_id === null) { + return ''; + } + $field_id = (int) $this->id; + if ($this->is_form_editor()) { + return "
adCAPTCHA will be rendered here.
"; + } + $captcha_html = AdCaptcha::ob_captcha_trigger(); + $input = "
" . + $captcha_html . + "
"; + $input .= ""; + + return $input; + } + + public function get_form_editor_field_title() { + return esc_html__('adCAPTCHA', 'adcaptcha'); + } + + public function get_form_editor_field_settings() { + return [ 'description_setting', 'error_message_setting', 'label_placement_setting', 'css_class_setting',]; + } + + public function get_form_editor_field_description() { + return esc_attr__( + 'Adds an adCAPTCHA verification field to enhance security and prevent spam submissions on your forms.', + 'adcaptcha-for-forms' + ); + } + + public function get_form_editor_field_icon() { + return plugin_dir_url( __FILE__ ) . '../../../assets/adcaptcha_icon.png'; + } +} diff --git a/src/Plugin/GravityForms/Forms.php b/src/Plugin/GravityForms/Forms.php new file mode 100644 index 0000000..dc1ad95 --- /dev/null +++ b/src/Plugin/GravityForms/Forms.php @@ -0,0 +1,27 @@ +register_adcaptcha_field(); + }, 10, 0); + } + + + public function register_adcaptcha_field() { + if (!GF_Fields::get('adcaptcha')) { + new Field(); + } + } +} diff --git a/src/Settings/Plugins.php b/src/Settings/Plugins.php index ec41a74..2e279ff 100644 --- a/src/Settings/Plugins.php +++ b/src/Settings/Plugins.php @@ -53,6 +53,18 @@ public function render_plugins_settings() { 'options' => array('Forms'), 'message' => '' ), + array( + 'label' => 'Forminator', + 'logo' => 'forminator.jpeg', + 'options' => array('Forms'), + 'message' => '' + ), + array( + 'label' => 'GravityForms', + 'logo' => 'gravity-forms_logo.svg', + 'options' => array('Forms'), + 'message' => '' + ), ); $saved_setting = false; diff --git a/src/Settings/Settings.php b/src/Settings/Settings.php index 192572e..14f3384 100644 --- a/src/Settings/Settings.php +++ b/src/Settings/Settings.php @@ -81,6 +81,6 @@ public function change_admin_footer_text() { } public function change_admin_footer_version() { - return 'Version 1.6.0'; + return 'Version 1.7.0'; } } diff --git a/tests/Plugin/Forminator/ForminatorTest.php b/tests/Plugin/Forminator/ForminatorTest.php new file mode 100644 index 0000000..3fee1f4 --- /dev/null +++ b/tests/Plugin/Forminator/ForminatorTest.php @@ -0,0 +1,235 @@ +alias(function ($text) { + return $text; + }); + Functions\when('get_option')->alias(function ($option_name) { + $mock_values = [ + 'adcaptcha_placement_id' => 'mocked-placement-id', + ]; + return $mock_values[$option_name] ?? null; + }); + Functions\when('sanitize_text_field')->alias(function($input) { + $sanitized = strip_tags($input); + $sanitized = preg_replace('/[\r\n\t]/', ' ', $sanitized); + $sanitized = trim($sanitized); + return $sanitized; + }); + Functions\when('__')->alias(function ($text, $domain = null) { + return $text; + }); + + $this->forms = new Forms(); + + $this->verifyMock = $this->createMock(Verify::class); + $reflection = new \ReflectionClass($this->forms); + $property = $reflection->getProperty('verify'); + $property->setAccessible(true); + $property->setValue($this->forms, $this->verifyMock); + } + + public function tearDown(): void { + global $mocked_filters; + $mocked_filters = []; + Mockery::close(); + Monkey\tearDown(); + parent::tearDown(); + } + + // Test method that verifies the existence of the 'setup' method and checks that specific WordPress hooks are properly registered with the expected callbacks and priorities. + public function testSetup() { + $this->assertTrue(method_exists($this->forms, 'setup'), 'Method setup does not exist'); + global $mocked_actions, $mocked_filters; + $this->assertContains( + ['hook' => 'forminator_before_form_render', 'callback' => [$this->forms, 'before_form_render'], 'priority' => 10, 'accepted_args' => 5], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'forminator_before_form_render', 'callback' => [AdCaptcha::class, 'enqueue_scripts'], 'priority' => 9, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'forminator_before_form_render', 'callback' => [Verify::class, 'get_success_token'], 'priority' => 9, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'wp_enqueue_scripts', 'callback' => [$this->forms, 'reset_captcha_script'], 'priority' => 9, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'forminator_render_button_markup', 'callback' => [$this->forms, 'captcha_trigger_filter'], 'priority' => 10, 'accepted_args' => 2], + $mocked_filters + ); + $this->assertContains( + ['hook' => 'forminator_cform_form_is_submittable', 'callback' => [$this->forms, 'verify'], 'priority' => 10, 'accepted_args' => 3], + $mocked_filters + ); + } + + // Tests if 'beforeFormRender' sets 'has_captcha' to true for 'adcaptcha' and false otherwise. + public function testBeforeFormRender() { + $reflection = new \ReflectionClass($this->forms); + $property = $reflection->getProperty('has_captcha'); + $property->setAccessible(true); + + $this->forms->before_form_render(1, 'contact_form', 123, [ + ['type' => 'captcha', 'captcha_provider' => 'adcaptcha'] + ], []); + $this->assertTrue($property->getValue($this->forms), "Expected has_captcha to be true"); + $this->forms->before_form_render(1, 'contact_form', 123, [ + ['type' => 'captcha', 'captcha_provider' => 'nocaptcha'] + ], []); + $this->assertFalse($property->getValue($this->forms), "Expected has_captcha to be false"); + } + + // Tests if 'hasAdcaptchaField' correctly identifies the presence of an 'adcaptcha' field in various form field scenarios. + public function testHasAdcaptchaField() { + $this->assertTrue(method_exists($this->forms, 'has_adcaptcha_field'), 'Method has_adcaptcha_field does not exist'); + + $reflection = new \ReflectionClass($this->forms); + $method = $reflection->getMethod('has_adcaptcha_field'); + $method->setAccessible(true); + + $testCases = [ + 'Valid adCaptcha field' => [ + 'input' => [['type' => 'captcha', 'captcha_provider' => 'adcaptcha']], + 'expected' => true + ], + 'Different captcha provider' => [ + 'input' => [['type' => 'captcha', 'captcha_provider' => 'nocaptcha']], + 'expected' => false + ], + 'Missing captcha provider' => [ + 'input' => [['type' => 'captcha']], + 'expected' => false + ], + 'Missing type' => [ + 'input' => [['captcha_provider' => 'adcaptcha']], + 'expected' => false + ], + 'Empty fields' => [ + 'input' => [], + 'expected' => false + ], + 'Different field type' => [ + 'input' => [['type' => 'text', 'captcha_provider' => 'adcaptcha']], + 'expected' => false + ], + 'Multiple fields with valid captcha' => [ + 'input' => [ + ['type' => 'text'], + ['type' => 'captcha', 'captcha_provider' => 'adcaptcha'] + ], + 'expected' => true + ] + ]; + foreach ($testCases as $description => $testCase) { + $result = $method->invoke($this->forms, $testCase['input']); + $this->assertSame($testCase['expected'], $result, "Failed: {$description}"); + } + } + + // Tests if 'captchaTriggerFilter' returns the original HTML when 'has_captcha' is true and prepends the captcha trigger when false. + public function testCaptchaTriggerFilter() { + $this->assertTrue(method_exists($this->forms, 'captcha_trigger_filter'), 'Method captcha_trigger_filter does not exist in Forms class'); + + $reflection = new \ReflectionClass($this->forms); + $property = $reflection->getProperty('has_captcha'); + $property->setAccessible(true); + $property->setValue($this->forms, true); + + $mockHtml = ''; + $result = $this->forms->captcha_trigger_filter($mockHtml, 'button'); + $this->assertEquals($mockHtml, $result, 'Expected HTML when has_captcha is true'); + + $property->setValue($this->forms, false); + $expectedHtml = AdCaptcha::ob_captcha_trigger() . $mockHtml; + $result = $this->forms->captcha_trigger_filter($mockHtml, 'button'); + $this->assertEquals($expectedHtml, $result, 'Expected HTML when has_captcha is false'); + } + + // Tests if 'reset_captcha_script' correctly registers a JavaScript snippet via 'wp_footer' to reset AdCaptcha after form submission. + public function testResetCaptchaScript() { + $this->assertTrue(method_exists($this->forms, 'reset_captcha_script'), 'Method reset_captcha_script does not exist in Forms class'); + Functions\expect('add_action') + ->once() + ->with('wp_footer', \Mockery::type('callable')) + ->andReturnUsing(function($hook, $callback) { + ob_start(); + $callback(); + $output = ob_get_clean(); + $expectedScript = ''; + + $this->assertStringContainsString($expectedScript, $output); + }); + $this->forms->reset_captcha_script(); + } + + public function testVerifyFailsWhenTokenIsMissing() { + Functions\when('wp_unslash')->justReturn(''); + $_POST['adcaptcha_successToken'] = ''; + $result = $this->forms->verify(true, 123, []); + + $this->assertFalse($result['can_submit']); + $this->assertEquals('Please complete the AdCaptcha verification.', $result['error']); + } + + public function testVerifyFailsWhenTokenVerificationFails() { + Functions\when('wp_unslash')->justReturn('invalid_token'); + $this->verifyMock->method('verify_token')->willReturn(false); + $result = $this->forms->verify(true, 123, []); + + $this->assertFalse($result['can_submit']); + $this->assertEquals('AdCaptcha verification failed. Please try again.', $result['error']); + } + + public function testVerifySucceedsWhenTokenIsValid() { + Functions\when('wp_unslash')->justReturn('valid_token'); + $this->verifyMock->method('verify_token')->willReturn(true); + $result = $this->forms->verify(true, 123, []); + $this->assertTrue(method_exists($this->forms, 'verify'), 'Method verify does not exist in Forms class'); + $this->assertTrue($result); + } + } + \ No newline at end of file diff --git a/tests/Plugin/GravityForms/GravityFormsTest.php b/tests/Plugin/GravityForms/GravityFormsTest.php new file mode 100644 index 0000000..9d9c873 --- /dev/null +++ b/tests/Plugin/GravityForms/GravityFormsTest.php @@ -0,0 +1,441 @@ +type = $type; + $this->id = 123; + $this->isFormEditor = $isFormEditor; + } + + public function get_field_label($input1 = false, $input2 = '') { + return 'AdCaptcha Label'; + } + + public function is_form_editor() { + return $this->isFormEditor; + } +} + + use PHPUnit\Framework\TestCase; + use AdCaptcha\Plugin\GravityForms\Forms; + use AdCaptcha\Plugin\GravityForms\Field; + use AdCaptcha\Widget\AdCaptcha; + use AdCaptcha\Widget\Verify; + use Brain\Monkey; + use Brain\Monkey\Functions; + use Mockery; + use ReflectionClass; + + class GravityFormsTest extends TestCase { + private $forms; + private $verifyMock; + private $field; + private $mockGFAPI; + + public function setUp(): void { + parent::setUp(); + Monkey\setUp(); + + global $mocked_actions, $mocked_filters; + $mocked_actions = []; + $mocked_filters = []; + + if (!class_exists('GF_Fields', false)) { + class_alias(MockGF_Fields::class, 'GF_Fields'); + } + if(!class_exists('GF_Field', false)) { + class_alias(MockGF_Field::class, 'GF_Field'); + } + + Functions\when('esc_html__')->alias(function ($text, $domain) { + return "[{$domain}] {$text}"; + }); + Functions\when('esc_attr__')->alias(function ($text, $domain) { + return "[{$domain}] {$text}"; + }); + Functions\when('esc_js')->alias(function ($text) { + return $text; + }); + Functions\when('esc_attr')->alias(function ($text) { + return $text; + }); + Functions\when('get_option')->alias(function ($option_name) { + $mock_values = [ + 'adcaptcha_placement_id' => 'mocked-placement-id', + ]; + return $mock_values[$option_name] ?? null; + }); + Functions\when('sanitize_text_field')->alias(function($input) { + $sanitized = strip_tags($input); + $sanitized = preg_replace('/[\r\n\t]/', ' ', $sanitized); + $sanitized = trim($sanitized); + return $sanitized; + }); + Functions\when('__')->alias(function ($text, $domain = null) { + return $text; + }); + Functions\when('plugin_dir_url')->alias(function ($path) { + return ''; + }); + + $this->mockGFAPI = Mockery::mock('alias:GFAPI'); + $this->forms = new Forms(); + $this->field = new Field($this->mockGFAPI); + + $this->verifyMock = $this->createMock(Verify::class); + $reflection = new \ReflectionClass($this->field); + $property = $reflection->getProperty('verify'); + $property->setAccessible(true); + $property->setValue($this->field, $this->verifyMock); + } + + public function tearDown(): void { + global $mocked_filters; + $mocked_filters = []; + Mockery::close(); + Monkey\tearDown(); + parent::tearDown(); + } + + // Tests if the 'setup' method exists and properly registers the 'gform_loaded' action with the expected parameters. + public function testSetup() { + $this->assertTrue(method_exists($this->forms, 'setup'), 'Method setup does not exist'); + global $mocked_actions; + $this->forms->setup(); + $registeredAction = array_filter($mocked_actions, function ($action) { + return $action['hook'] === 'gform_loaded'; + }); + $this->assertNotEmpty($registeredAction, 'The gform_loaded action was not registered.'); + $registeredAction = array_values($registeredAction)[0]; + $this->assertEquals(10, $registeredAction['priority'], 'Expected priority 10 for gform_loaded.'); + $this->assertEquals(0, $registeredAction['accepted_args'], 'Expected 0 accepted args for gform_loaded.'); + $this->assertInstanceOf(\Closure::class, $registeredAction['callback'], 'Expected a Closure as the callback.'); + } + + // Tests if the 'register_adcaptcha_field' method exists and returns null as expected. + public function testRegisterAdcaptchaField() { + $this->assertTrue(method_exists($this->forms, 'register_adcaptcha_field'), 'Method register_adcaptcha_field does not exist'); + $result = $this->forms->register_adcaptcha_field(); + $this->assertNull($result, 'Expected null return value from register_adcaptcha_field'); + } + + // Tests if the 'setup_hooks' method exists and correctly registers expected actions and filters. + public function testSetupHooks() { + $this->assertTrue(method_exists($this->field, 'setup_hooks'), 'Method setup_hooks does not exist'); + + global $mocked_actions, $mocked_filters; + $this->assertContains( + ['hook' => 'wp_enqueue_scripts', 'callback' => [AdCaptcha::class, 'enqueue_scripts'], 'priority' => 9, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'wp_enqueue_scripts', 'callback' => [Verify::class, 'get_success_token'], 'priority' => 10, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'admin_head', 'callback' => [$this->field, 'custom_admin_field_icon_style'], 'priority' => 10, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'admin_init', 'callback' => [$this->field, 'update_adcaptcha_label'], 'priority' => 10, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'admin_footer', 'callback' => [$this->field, 'enqueue_admin_script'], 'priority' => 10, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'gform_preview_body_open', 'callback' => [$this->field, 'enqueue_preview_scripts'], 'priority' => 10, 'accepted_args' => 1], + $mocked_actions + ); + $this->assertContains( + ['hook' => 'gform_field_groups_form_editor', 'callback' => [$this->field, 'add_to_field_groups'], 'priority' => 10, 'accepted_args' => 1], + $mocked_filters + ); + $this->assertContains( + ['hook' => 'gform_field_content', 'callback' => [$this->field, 'modify_gform_field_content'], 'priority' => 10, 'accepted_args' => 2], + $mocked_filters + ); + $this->assertContains( + ['hook' => 'gform_validation', 'callback' => [$this->field, 'verify_captcha'], 'priority' => 10, 'accepted_args' => 1], + $mocked_filters + ); + $this->assertContains( + ['hook' => 'gform_pre_render', 'callback' => [$this->field, 'handle_adcaptcha_token'], 'priority' => 10, 'accepted_args' => 1], + $mocked_filters + ); + } + + // Tests if 'add_to_field_groups' correctly adds the adCAPTCHA field to advanced fields and avoids duplication. + public function testAddToFieldGroups() { + $this->assertTrue(method_exists($this->field, 'add_to_field_groups'), 'Method add_to_field_groups does not exist'); + $field_groups = [ + 'advanced_fields' => [ + 'fields' => [ + ['data-type' => 'text'], + ['data-type' => 'email'], + ] + ] + ]; + $field_groups_adcaptcha = [ + 'advanced_fields' => [ + 'fields' => [ + ['data-type' => 'adcaptcha'], + ] + ] + ]; + $result = $this->field->add_to_field_groups($field_groups); + $this->assertContains( + ['data-type' => 'adcaptcha', 'value' => $this->field->get_form_editor_field_title(), 'label' => $this->field->get_form_editor_field_title()], + $result['advanced_fields']['fields'] + ); + $fieldWithAdcaptcha = $this->field->add_to_field_groups($field_groups_adcaptcha); + $this->assertEquals($field_groups_adcaptcha, $fieldWithAdcaptcha, 'Expected the field groups to remain unchanged if adcaptcha field is already present'); + } + + // Tests if 'modify_gform_field_content' removes the label from adCAPTCHA fields while keeping other content unchanged. + public function testModifyGformFieldContent() { + $this->assertTrue(method_exists($this->field, 'modify_gform_field_content'), 'Method modify_gform_field_content does not exist'); + $field = new MockGF_Field('adcaptcha'); + + $content = ''; + $expectedModifiedContent = ''; + + $modifiedContent = $this->field->modify_gform_field_content($content, $field); + $this->assertEquals($expectedModifiedContent, $modifiedContent, 'Expected label to be removed for adcaptcha fields'); + } + + // Tests if 'verify_captcha' fails validation when the CAPTCHA token is empty. + public function testVerifyCaptchaFailsWhenTokenIsEmpty() { + Functions\when('wp_unslash')->justReturn(''); + $validation_result = [ + 'form' => ['fields' => [(object) ['type' => 'adcaptcha']]], + 'is_valid' => true + ]; + $result = $this->field->verify_captcha($validation_result); + $this->assertFalse($result['is_valid']); + $this->assertTrue($result['form']['fields'][0]->failed_validation); + $this->assertEquals('Incomplete CAPTCHA, Please try again.', $result['form']['fields'][0]->validation_message); + } + + // Tests if 'verify_captcha' fails validation when token verification fails. + public function testVerifyCaptchaFailsWhenTokenVerificationFails() { + Functions\when('wp_unslash')->justReturn('invalid_token'); + $this->verifyMock->method('verify_token')->willReturn(false); + $validation_result = [ + 'form' => ['fields' => [(object) ['type' => 'adcaptcha']]], + 'is_valid' => true + ]; + $result = $this->field->verify_captcha($validation_result); + $this->assertFalse($result['is_valid']); + $this->assertTrue($result['form']['fields'][0]->failed_validation); + $this->assertEquals('Invalid token.', $result['form']['fields'][0]->validation_message); + } + + // Tests if 'verify_captcha' passes validation when a valid token is provided. + public function testVerifyCaptchaSucceedsWhenTokenIsValid() { + Functions\when('wp_unslash')->justReturn('valid_token'); + $this->verifyMock->method('verify_token')->willReturn(true); + $validation_result = [ + 'form' => [ + 'fields' => [ + (object) [ + 'type' => 'adcaptcha', + 'failed_validation' => false, + 'validation_message' => '' + ] + ] + ], + 'is_valid' => true + ]; + $result = $this->field->verify_captcha($validation_result); + $this->assertTrue($result['is_valid']); + $this->assertFalse($result['form']['fields'][0]->failed_validation); + $this->assertEmpty($result['form']['fields'][0]->validation_message); + } + + // Tests if 'custom_admin_field_icon_style' outputs the expected CSS styles. + public function testCustomAdminFieldIconStyle() { + $this->assertTrue(method_exists($this->field, 'custom_admin_field_icon_style'), 'Method custom_admin_field_icon_style does not exist'); + ob_start(); + $this->field->custom_admin_field_icon_style(); + $capturedOutput = ob_get_clean(); + $this->assertStringContainsString("#sidebar_field_info #sidebar_field_icon img { + width: 16px !important; + }", $capturedOutput, 'Expected the style to be output'); + } + + // Tests that 'update_adcaptcha_label' returns null when there are no forms to update. + public function testUpdateAdcaptchaLabelFormEmpty() { + $this->assertTrue(method_exists($this->field, 'update_adcaptcha_label'), 'Method update_adcaptcha_label does not exist'); + $this->mockGFAPI->shouldReceive('get_forms') + ->once() + ->andReturn([]); + $result = $this->field->update_adcaptcha_label(); + $this->assertNull($result, 'Expected null return value from update_adcaptcha_label'); + } + + // Tests that 'enqueue_admin_script' outputs the expected JavaScript for limiting adCAPTCHA field additions. + public function testEnqueueAdminScript() { + $this->assertTrue(method_exists($this->field, 'enqueue_admin_script'), 'Method enqueue_admin_script does not exist'); + ob_start(); + $this->field->enqueue_admin_script(); + $capturedOutput = ob_get_clean(); + $this->assertStringContainsString("if (typeof window.CanFieldBeAdded !== 'function') { + return; + }", $capturedOutput, 'Expected the script to be output'); + $this->assertStringContainsString("let originalFunction = window.CanFieldBeAdded;", $capturedOutput, 'Expected the script to be output'); + $this->assertStringContainsString('window.CanFieldBeAdded = function(type) { + if (type === "adcaptcha") { + if (GetFieldsByType(["adcaptcha"]).length > 0) { + gform.instances.dialogAlert("Field Error", "Only one adCAPTCHA field can be added."); + return false; + } + } + return originalFunction(type); + };', $capturedOutput, 'Expected the script to be output'); + } + + // Tests that 'handle_adcaptcha_token' correctly returns the form and outputs a script when a success token is present. + public function testHandleAdcaptchaToken() { + $this->assertTrue(method_exists($this->field, 'handle_adcaptcha_token'), 'Method handle_adcaptcha_token does not exist'); + + $_POST = []; + $form = ['id' => 1]; + ob_start(); + $result = $this->field->handle_adcaptcha_token($form); + $capturedOutput = ob_get_clean(); + $this->assertEquals($form, $result, 'Expected the form to be returned unchanged'); + $this->assertEmpty($capturedOutput, 'Expected no script output when success token is missing'); + + $_POST['adcaptcha_successToken'] = 'mocked-success-token'; + ob_start(); + $this->field->handle_adcaptcha_token($form); + $capturedOutput = ob_get_clean(); + $this->assertStringContainsString("document.addEventListener('DOMContentLoaded', function()", $capturedOutput, 'Expected the script to be output'); + $this->assertStringContainsString("let adCaptchaField = document.querySelector('.adcaptcha_successToken');", $capturedOutput, 'Expected the script to be output'); + $this->assertStringContainsString("if (adCaptchaField) { + setTimeout(function() { + if (window.adcap) { + window.adcap.setVerificationState('success'); + adCaptchaField.value = 'mocked-success-token'; + } + }, 500); + }", $capturedOutput, 'Expected the script to be output'); + } + + // Tests that 'enqueue_preview_scripts' outputs the expected JavaScript for rendering adCAPTCHA in preview mode. + public function testEnqueuePreviewScripts() { + $this->assertTrue(method_exists($this->field, 'enqueue_preview_scripts'), 'Method enqueue_preview_scripts does not exist'); + $formID = 1; + ob_start(); + $this->field->enqueue_preview_scripts($formID); + $capturedOutput = ob_get_clean(); + $this->assertStringContainsString("document.addEventListener('DOMContentLoaded', function()", $capturedOutput, 'The enqueue_preview_scripts method was not called correctly.'); + $this->assertStringContainsString("let captchaContainer = document.querySelector('.ginput_container_adcaptcha');", $capturedOutput, 'The enqueue_preview_scripts method was not called correctly.'); + $this->assertStringContainsString("if (captchaContainer) { + let messageDiv = document.createElement('div'); + messageDiv.className = 'ginput_container adcaptcha-message'; + messageDiv.innerText = 'adCAPTCHA will be rendered here.'; + captchaContainer.prepend(messageDiv); + }", $capturedOutput, 'The enqueue_preview_scripts method was not called correctly.'); + } + + // Tests that 'get_field_input' returns the correct HTML output based on form ID and editor mode. + public function testGetFieldInput() { + $this->assertTrue(method_exists($this->field, 'get_field_input'), 'Method get_field_input does not exist'); + $form = ['id' => null]; + $result = $this->field->get_field_input($form, '', null); + $this->assertEmpty($result, 'Expected an empty string when form ID is missing'); + $form = ['id' => 1]; + $result = $this->field->get_field_input($form, '', null); + $this->assertEquals("
adCAPTCHA will be rendered here.
", $result, 'Expected the adCAPTCHA message to be rendered'); + + $form_id = 1; + $field_id = 123; + $this->field->isFormEditor = false; + $result = $this->field->get_field_input($form, '', null); + $this->assertStringContainsString("document.addEventListener('DOMContentLoaded', function() { + var hiddenToken = document.querySelector('.adcaptcha_successToken'); + if (hiddenToken) { + hiddenToken.id = 'input_{$form_id}_{$field_id}'; + } + });", $result, 'Expected CAPTCHA HTML to be rendered when is_form_editor is false'); + } + + // Tests that 'get_form_editor_field_title' returns the expected title with the correct text domain. + public function testGetFormEditorFieldTitle() { + $this->assertTrue( + method_exists($this->field, 'get_form_editor_field_title'), + 'Method get_form_editor_field_title does not exist' + ); + $expectedText = 'adCAPTCHA'; + $expectedDomain = 'adcaptcha'; + $result = $this->field->get_form_editor_field_title(); + $this->assertEquals( + "[{$expectedDomain}] {$expectedText}", + $result, + 'Expected the title to match the expected text with correct text domain' + ); + } + + // Tests that 'get_form_editor_field_settings' returns an array with the expected field settings. + public function testGetFormEditorFieldSettings() { + $this->assertTrue(method_exists($this->field, 'get_form_editor_field_settings'), 'Method get_form_editor_field_settings does not exist'); + $data = [ 'description_setting', 'error_message_setting', 'label_placement_setting', 'css_class_setting',]; + $result = $this->field->get_form_editor_field_settings(); + $this->assertIsArray($result, 'Expected an array to be returned'); + $this->assertEquals($data, $result, 'Expected the array keys to match the expected data'); + } + + // Tests that 'get_form_editor_field_description' returns the expected description with the correct text domain. + public function testGetFormEditorFieldDescription() { + $this->assertTrue( + method_exists($this->field, 'get_form_editor_field_description'), + 'Method get_form_editor_field_description does not exist' + ); + $expectedText = 'Adds an adCAPTCHA verification field to enhance security and prevent spam submissions on your forms.'; + $expectedDomain = 'adcaptcha-for-forms'; + $result = $this->field->get_form_editor_field_description(); + $this->assertEquals( + "[{$expectedDomain}] {$expectedText}", + $result, + 'Expected the description to match the expected text with correct text domain' + ); + } + + // Tests that 'get_form_editor_field_icon' returns the correct icon URL. + public function testGetFromEditorFieldIcon() { + $this->assertTrue(method_exists($this->field, 'get_form_editor_field_icon'), 'Method get_form_editor_field_icon does not exist'); + $icon_url = $this->field->get_form_editor_field_icon(); + $this->assertStringContainsString( + '../../../assets/adcaptcha_icon.png', + $icon_url, + 'Expected the icon URL to be correctly formed' + ); + } + } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8935796..3bb7f7e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@ errors); + $code = array_key_first($this->errors); } - return isset($this->errors[$code]) ? $this->errors[$code][0] : null; + + return isset($this->errors[$code]) ? $this->errors[$code][0] : ''; } - public function get_error_data($code = '') { return isset($this->error_data[$code]) ? $this->error_data[$code] : null; } diff --git a/tests/test_helpers.php b/tests/test_helpers.php index 49c2b75..1c4a93f 100644 --- a/tests/test_helpers.php +++ b/tests/test_helpers.php @@ -47,4 +47,4 @@ function check_hook_registration($mocked_hooks, $hook_name, $priority = 10, $acc } } return false; -} \ No newline at end of file +}