diff --git a/.distignore b/.distignore index 90d31c2c..c9219907 100644 --- a/.distignore +++ b/.distignore @@ -10,10 +10,8 @@ /.gitignore /.phpcs.cache /.yarnrc.yml -/CHANGELOG.md /README.md /Thumbs.db -/assets/js/*.svg /auth.json /babel.config.json /composer.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..af3ad128 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4e79b83..1ec77fa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,28 +33,16 @@ jobs: with: path: ${{ env.wp-plugin-directory }} - - name: Install PHP + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} extensions: json, mysqli, mbstring, zip - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get Composer cache directory - working-directory: ${{ env.wp-plugin-directory }} - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Set up Composer caching - uses: actions/cache@v3 - env: - cache-name: cache-composer-dependencies + - name: Setup Composer caching + uses: ramsey/composer-install@v2 with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- + working-directory: ${{ env.wp-plugin-directory }} - name: Install dependencies working-directory: ${{ env.wp-plugin-directory }} diff --git a/.github/workflows/create-zip.yml b/.github/workflows/create-zip.yml index 452f14db..35a39d43 100644 --- a/.github/workflows/create-zip.yml +++ b/.github/workflows/create-zip.yml @@ -13,19 +13,8 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Set up Composer caching - uses: actions/cache@v3 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- + - name: Setup Composer caching + uses: ramsey/composer-install@v2 - name: Install dependencies in prod version run: | diff --git a/.github/workflows/deploy-to-wp-org.yml b/.github/workflows/deploy-to-wp-org.yml index e7e2842e..dbe4ba10 100644 --- a/.github/workflows/deploy-to-wp-org.yml +++ b/.github/workflows/deploy-to-wp-org.yml @@ -15,19 +15,8 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Set up Composer caching - uses: actions/cache@v3 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- + - name: Setup Composer caching + uses: ramsey/composer-install@v2 - name: Install dependencies in prod version run: | diff --git a/.gitignore b/.gitignore index 659560a7..5c2fa430 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ assets/css/*.min.css.map assets/js/apps/ assets/js/**/*.min.js assets/js/**/*.min.js.map -assets/*.svg -/coverage/ +coverage/ node_modules/ vendor/ diff --git a/.tests/js/__mocks__/backboneMarionette.js b/.tests/js/__mocks__/backboneMarionette.js index 35e1f9c6..1dbdee81 100644 --- a/.tests/js/__mocks__/backboneMarionette.js +++ b/.tests/js/__mocks__/backboneMarionette.js @@ -1,4 +1,6 @@ // Mock Backbone +// noinspection JSUnresolvedReference + const Backbone = { Radio: { channel() { diff --git a/.tests/js/__mocks__/elementorFrontend.js b/.tests/js/__mocks__/elementorFrontend.js index f9af21da..5f49844b 100644 --- a/.tests/js/__mocks__/elementorFrontend.js +++ b/.tests/js/__mocks__/elementorFrontend.js @@ -1,3 +1,5 @@ +// noinspection JSUnresolvedReference + export const hooks = { addAction: jest.fn(), }; @@ -8,4 +10,5 @@ const elementorFrontend = { global.elementorFrontend = elementorFrontend; +// noinspection JSUnusedGlobalSymbols export default elementorFrontend; diff --git a/.tests/js/__mocks__/elementorModules.js b/.tests/js/__mocks__/elementorModules.js index b06a8ced..6bed599f 100644 --- a/.tests/js/__mocks__/elementorModules.js +++ b/.tests/js/__mocks__/elementorModules.js @@ -1,3 +1,5 @@ +// noinspection JSUnresolvedReference + const elementorModules = { editor: { utils: { @@ -7,4 +9,6 @@ const elementorModules = { }; global.elementorModules = elementorModules; + +// noinspection JSUnusedGlobalSymbols export default elementorModules; diff --git a/.tests/js/__mocks__/elementorPro.js b/.tests/js/__mocks__/elementorPro.js index a955cbf1..585f62dd 100644 --- a/.tests/js/__mocks__/elementorPro.js +++ b/.tests/js/__mocks__/elementorPro.js @@ -1,3 +1,5 @@ +// noinspection JSUnresolvedReference + const elementorPro = { config: { forms: { @@ -14,4 +16,5 @@ const elementorPro = { global.elementorPro = elementorPro; +// noinspection JSUnusedGlobalSymbols export default elementorPro; diff --git a/.tests/js/assets-js-files/hcaptcha-divi.test.js b/.tests/js/assets-js-files/hcaptcha-divi.test.js index a6783ecf..7d08b5ce 100644 --- a/.tests/js/assets-js-files/hcaptcha-divi.test.js +++ b/.tests/js/assets-js-files/hcaptcha-divi.test.js @@ -20,7 +20,11 @@ describe( 'hCaptcha ajaxStop binding', () => { } ); test( 'hCaptchaBindEvents is called when ajaxStop event is triggered', () => { - $( document ).trigger( 'ajaxStop' ); + const xhr = {}; + const settings = {}; + + settings.data = '?some_data&et_pb_contactform_submit_0=some_value'; + $( document ).trigger( 'ajaxSuccess', [ xhr, settings ] ); expect( hCaptchaBindEvents ).toHaveBeenCalledTimes( 1 ); } ); } ); diff --git a/.tests/js/assets-js-files/hcaptcha-support-candy.test.js b/.tests/js/assets-js-files/hcaptcha-support-candy.test.js index 114a96e4..cb10b026 100644 --- a/.tests/js/assets-js-files/hcaptcha-support-candy.test.js +++ b/.tests/js/assets-js-files/hcaptcha-support-candy.test.js @@ -20,7 +20,11 @@ describe( 'hCaptcha ajaxStop binding', () => { } ); test( 'hCaptchaBindEvents is called when ajaxStop event is triggered', () => { - $( document ).trigger( 'ajaxStop' ); + const xhr = {}; + const settings = {}; + + settings.data = '?some_data&action=wpsc_get_ticket_form'; + $( document ).trigger( 'ajaxSuccess', [ xhr, settings ] ); expect( hCaptchaBindEvents ).toHaveBeenCalledTimes( 1 ); } ); } ); diff --git a/.tests/js/setupTests.js b/.tests/js/setupTests.js index 79d614e5..53e41e90 100644 --- a/.tests/js/setupTests.js +++ b/.tests/js/setupTests.js @@ -1,3 +1,5 @@ +// noinspection JSUnresolvedReference + global.fetch = require( 'jest-fetch-mock' ); global.ajaxurl = 'http://ajax-url'; diff --git a/.tests/php/integration/AMainTest.php b/.tests/php/integration/AMainTest.php index be6b0ceb..34a39102 100644 --- a/.tests/php/integration/AMainTest.php +++ b/.tests/php/integration/AMainTest.php @@ -124,7 +124,7 @@ public function test_init() { * Test init() and init_hooks(). * * @param boolean $logged_in User is logged in. - * @param boolean $hcaptcha_off_when_logged_in Option 'hcaptcha_off_when_logged_in' is set. + * @param string $hcaptcha_off_when_logged_in Option 'hcaptcha_off_when_logged_in' is set. * @param boolean|string $whitelisted Whether IP is whitelisted. * @param boolean $hcaptcha_active Plugin should be active. * @@ -132,7 +132,7 @@ public function test_init() { * @noinspection PhpUnitTestsInspection * @throws ReflectionException ReflectionException. */ - public function test_init_and_init_hooks( $logged_in, $hcaptcha_off_when_logged_in, $whitelisted, $hcaptcha_active ) { + public function test_init_and_init_hooks( bool $logged_in, string $hcaptcha_off_when_logged_in, $whitelisted, bool $hcaptcha_active ) { global $current_user; $hcaptcha_wordpress_plugin = hcaptcha(); @@ -141,8 +141,7 @@ public function test_init_and_init_hooks( $logged_in, $hcaptcha_off_when_logged_ 'hcap_whitelist_ip', static function () use ( $whitelisted ) { return $whitelisted; - }, - 10 + } ); // Plugin was loaded by codeception. @@ -268,7 +267,7 @@ static function () use ( $whitelisted ) { * * @return array[] */ - public function dp_test_init() { + public function dp_test_init(): array { return [ 'not logged in, not set, not whitelisted' => [ false, 'off', false, true ], 'not logged in, set, not whitelisted' => [ false, 'on', false, true ], @@ -284,7 +283,7 @@ public function dp_test_init() { /** * Test init() and init_hooks() on Elementor Pro edit page. * - * @param boolean $elementor_pro_status Option 'elementor_pro_status' is set. + * @param string $elementor_pro_status Option 'elementor_pro_status' is set. * @param array $server $_SERVER variable. * @param array $get $_GET variable. * @param array $post $_POST variable. @@ -295,7 +294,11 @@ public function dp_test_init() { * @throws ReflectionException ReflectionException. */ public function test_init_and_init_hooks_on_elementor_pro_edit_page( - $elementor_pro_status, $server, $get, $post, $hcaptcha_active + string $elementor_pro_status, + array $server, + array $get, + array $post, + bool $hcaptcha_active ) { global $current_user; @@ -303,8 +306,7 @@ public function test_init_and_init_hooks_on_elementor_pro_edit_page( 'hcap_whitelist_ip', static function () { return true; - }, - 10 + } ); unset( $current_user ); @@ -389,7 +391,7 @@ static function () { * * @return array */ - public function dp_test_init_and_init_hooks_on_elementor_pro_edit_page() { + public function dp_test_init_and_init_hooks_on_elementor_pro_edit_page(): array { return [ 'elementor option off' => [ 'off', @@ -494,7 +496,8 @@ public function test_print_inline_styles() { $div_logo_url = HCAPTCHA_URL . '/assets/images/hcaptcha-div-logo.svg'; $div_logo_url_white = HCAPTCHA_URL . '/assets/images/hcaptcha-div-logo-white.svg'; - $expected = ' + modules as $module ) { - if ( in_array( $class, (array) $module[2], true ) ) { + if ( in_array( $class_name, (array) $module[2], true ) ) { $source = $module[1]; // For WP Core (empty $source string), return option value. diff --git a/src/php/Helpers/Request.php b/src/php/Helpers/Request.php new file mode 100644 index 00000000..abcae166 --- /dev/null +++ b/src/php/Helpers/Request.php @@ -0,0 +1,81 @@ + $inner_block ) { + if ( + isset( $inner_block['blockName'], $inner_block['attrs']['type'] ) && + 'kadence/advanced-form-captcha' === $inner_block['blockName'] && + 'hcaptcha' === $inner_block['attrs']['type'] + ) { + unset( $block['innerBlocks'][ $index ] ); + + $output[0] = $block; + break; + } + } + + return $output; + } +} diff --git a/src/php/Kadence/AdvancedForm.php b/src/php/Kadence/AdvancedForm.php new file mode 100644 index 00000000..1fcc97a0 --- /dev/null +++ b/src/php/Kadence/AdvancedForm.php @@ -0,0 +1,226 @@ +init_hooks(); + } + + /** + * Add hooks. + * + * @return void + */ + public function init_hooks() { + add_filter( 'render_block', [ $this, 'render_block' ], 10, 3 ); + add_action( 'wp_print_footer_scripts', [ $this, 'dequeue_kadence_hcaptcha_api' ], 8 ); + + if ( Request::is_frontend() ) { + add_filter( + 'block_parser_class', + static function () { + return AdvancedBlockParser::class; + } + ); + + add_action( 'wp_print_footer_scripts', [ $this, 'enqueue_scripts' ], 9 ); + + return; + } + + add_action( 'wp_ajax_kb_process_advanced_form_submit', [ $this, 'process_ajax' ], 9 ); + add_action( 'wp_ajax_nopriv_kb_process_advanced_form_submit', [ $this, 'process_ajax' ], 9 ); + add_filter( + 'pre_option_kadence_blocks_hcaptcha_site_key', + static function () { + return hcaptcha()->settings()->get_site_key(); + } + ); + add_filter( + 'pre_option_kadence_blocks_hcaptcha_secret_key', + static function () { + return hcaptcha()->settings()->get_secret_key(); + } + ); + add_action( 'enqueue_block_editor_assets', [ $this, 'editor_assets' ] ); + } + + /** + * Render block filter. + * + * @param string|mixed $block_content Block content. + * @param array $block Block. + * @param WP_Block $instance Instance. + * + * @return string|mixed + * @noinspection PhpUnusedParameterInspection + * @noinspection HtmlUnknownAttribute + */ + public function render_block( $block_content, array $block, WP_Block $instance ) { + if ( 'kadence/advanced-form-submit' === $block['blockName'] && ! $this->hcaptcha_found ) { + + $search = '
#', + $this->get_hcaptcha(), + (string) $block_content, + 1, + $count + ); + + $this->hcaptcha_found = (bool) $count; + + return $block_content; + } + + /** + * Process ajax. + * + * @return void + */ + public function process_ajax() { + // Nonce is checked by Kadence. + + // phpcs:disable WordPress.Security.NonceVerification.Missing + $hcaptcha_response = isset( $_POST['h-captcha-response'] ) ? + filter_var( wp_unslash( $_POST['h-captcha-response'] ), FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : + ''; + + $error = hcaptcha_request_verify( $hcaptcha_response ); + + if ( null === $error ) { + return; + } + + unset( $_POST['h-captcha-response'], $_POST['g-recaptcha-response'] ); + // phpcs:enable WordPress.Security.NonceVerification.Missing + + $data = [ + 'html' => '
' . $error . '
', + 'console' => __( 'hCaptcha Failed', 'hcaptcha-for-forms-and-more' ), + 'required' => null, + ]; + + wp_send_json_error( $data ); + } + + /** + * Dequeue Kadence hcaptcha API script. + * + * @return void + */ + public function dequeue_kadence_hcaptcha_api() { + wp_dequeue_script( 'kadence-blocks-hcaptcha' ); + wp_deregister_script( 'kadence-blocks-hcaptcha' ); + } + + /** + * Enqueue scripts. + * + * @return void + */ + public static function enqueue_scripts() { + $min = hcap_min_suffix(); + + wp_enqueue_script( + 'hcaptcha-kadence-advanced', + HCAPTCHA_URL . "/assets/js/hcaptcha-kadence-advanced$min.js", + [ 'hcaptcha', 'kadence-blocks-advanced-form' ], + HCAPTCHA_VERSION, + true + ); + } + + /** + * Enqueue editor assets. + * + * @return void + */ + public function editor_assets() { + $min = hcap_min_suffix(); + + wp_enqueue_script( + self::ADMIN_HANDLE, + HCAPTCHA_URL . "/assets/js/admin-kadence-advanced$min.js", + [], + HCAPTCHA_VERSION, + true + ); + + $notice = HCaptcha::get_hcaptcha_plugin_notice(); + + wp_localize_script( + self::ADMIN_HANDLE, + self::OBJECT, + [ + 'noticeLabel' => $notice['label'], + 'noticeDescription' => $notice['description'], + ] + ); + + wp_enqueue_style( + self::ADMIN_HANDLE, + constant( 'HCAPTCHA_URL' ) . "/assets/css/admin-kadence-advanced$min.css", + [], + HCAPTCHA_VERSION + ); + } + + /** + * Get hCaptcha. + * + * @return string + */ + private function get_hcaptcha(): string { + $args = [ + 'id' => [ + 'source' => HCaptcha::get_class_source( __CLASS__ ), + 'form_id' => AdvancedBlockParser::$form_id, + ], + ]; + + return HCaptcha::form( $args ); + } +} diff --git a/src/php/Kadence/BlockParser.php b/src/php/Kadence/BlockParser.php deleted file mode 100644 index aa8d13b5..00000000 --- a/src/php/Kadence/BlockParser.php +++ /dev/null @@ -1,39 +0,0 @@ -).+?(<\/div>)/', - '$1' . HCaptcha::form( $args ) . '$2', - (string) $block_content + $pattern = '/(
).+?(<\/div>)/'; + $block_content = (string) $block_content; + + if ( preg_match( $pattern, $block_content ) ) { + // Do not replace reCaptcha V2. + return $block_content; + } + + if ( false !== strpos( $block_content, 'recaptcha_response' ) ) { + // Do not replace reCaptcha V3. + return $block_content; + } + + $search = '
post_content ) as $block ) { + if ( + isset( $block['blockName'], $block['attrs']['uniqueID'] ) && + 'kadence/form' === $block['blockName'] && + $form_id === $block['attrs']['uniqueID'] && + ! empty( $block['attrs']['recaptcha'] ) + ) { + return true; + } + } + + return false; + } } diff --git a/src/php/Mailchimp/Form.php b/src/php/Mailchimp/Form.php index db9a0e4d..1e888f2a 100644 --- a/src/php/Mailchimp/Form.php +++ b/src/php/Mailchimp/Form.php @@ -53,10 +53,10 @@ private function init_hooks() { * @param array|mixed $messages Messages. * @param MC4WP_Form $form Form. * - * @return array|mixed + * @return array * @noinspection PhpUnusedParameterInspection */ - public function add_hcap_error_messages( $messages, MC4WP_Form $form ) { + public function add_hcap_error_messages( $messages, MC4WP_Form $form ): array { $messages = (array) $messages; foreach ( hcap_get_error_messages() as $error_code => $error_message ) { @@ -110,8 +110,7 @@ public function verify( $errors, MC4WP_Form $form ) { $error_message = hcaptcha_verify_post( self::NAME, self::ACTION ); if ( null !== $error_message ) { - $error_code = array_search( $error_message, hcap_get_error_messages(), true ); - $error_code = $error_code ?: 'empty'; + $error_code = array_search( $error_message, hcap_get_error_messages(), true ) ?: 'empty'; $errors = (array) $errors; $errors[] = $error_code; } diff --git a/src/php/Main.php b/src/php/Main.php index bc41546c..09cff2c7 100644 --- a/src/php/Main.php +++ b/src/php/Main.php @@ -178,13 +178,13 @@ public function init_hooks() { /** * Get plugin class instance. * - * @param string $class Class name. + * @param string $class_name Class name. * * @return object|null */ - public function get( string $class ) { + public function get( string $class_name ) { - return $this->loaded_classes[ $class ] ?? null; + return $this->loaded_classes[ $class_name ] ?? null; } /** @@ -320,12 +320,8 @@ public function print_inline_styles() { $div_logo_url = HCAPTCHA_URL . '/assets/images/hcaptcha-div-logo.svg'; $div_logo_white_url = HCAPTCHA_URL . '/assets/images/hcaptcha-div-logo-white.svg'; - ob_start(); ?> -