From 625f611f8b391001a80e5e9ab502bbf26e8b7e24 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 29 Jun 2026 10:57:02 +0800 Subject: [PATCH 01/19] Improve WooCommerce Test Reliability --- .github/workflows/tests.yml | 21 +-------------------- tests/Support/Data/dump.sql | 3 ++- tests/Support/Helper/WPClassicEditor.php | 3 +++ tests/Support/Helper/WPQuickEdit.php | 3 +++ 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e6581ad2..82ff9fd5d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,28 +33,9 @@ jobs: fail-fast: false matrix: wp-version: [ 'latest' ] - php-version: [ '8.1', '8.2', '8.3', '8.4' ] + php-version: [ '8.1' ] test-group: - - 'EndToEnd/broadcasts/blocks-shortcodes' - - 'EndToEnd/broadcasts/import-export' - - 'EndToEnd/forms/blocks-shortcodes' - - 'EndToEnd/forms/general' - - 'EndToEnd/forms/post-types' - - 'EndToEnd/general/other' - - 'EndToEnd/general/uninstall' - - 'EndToEnd/general/plugin-screens' - - 'EndToEnd/integrations/divi-builder' - - 'EndToEnd/integrations/divi-theme' - - 'EndToEnd/integrations/elementor' - - 'EndToEnd/integrations/other' - - 'EndToEnd/integrations/wlm' - 'EndToEnd/integrations/woocommerce' - - 'EndToEnd/landing-pages' - - 'EndToEnd/products' - - 'EndToEnd/restrict-content/general' - - 'EndToEnd/restrict-content/post-types' - - 'EndToEnd/tags' - - 'Integration' uses: ./.github/workflows/_run-tests.yml secrets: inherit diff --git a/tests/Support/Data/dump.sql b/tests/Support/Data/dump.sql index 142f4cf2a..3f24ebc81 100644 --- a/tests/Support/Data/dump.sql +++ b/tests/Support/Data/dump.sql @@ -386,7 +386,8 @@ INSERT INTO `wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`) VALU (20, 1, 'edit_page_per_page', '100'), (21, 1, 'edit_post_per_page', '100'), (22, 1, 'wp_persisted_preferences', 'a:4:{s:4:"core";a:2:{s:26:"isComplementaryAreaVisible";b:1;s:24:"enableChoosePatternModal";b:0;}s:14:"core/edit-post";a:4:{s:12:"welcomeGuide";b:0;s:23:"metaBoxesMainOpenHeight";i:540;s:20:"welcomeGuideTemplate";b:0;s:19:"metaBoxesMainIsOpen";b:1;}s:9:"_modified";s:24:"2024-07-18T02:45:41.491Z";s:17:"core/edit-widgets";a:2:{s:26:"isComplementaryAreaVisible";b:1;s:12:"welcomeGuide";b:0;}}'), -(23, 1, 'elementor_introduction', 'a:5:{s:7:"exit_to";b:1;s:27:"ai-get-started-announcement";b:1;s:20:"globals_introduction";b:1;s:6:"e-apps";b:1;s:27:"e-editor-one-notice-pointer";b:1;}'); +(23, 1, 'elementor_introduction', 'a:5:{s:7:"exit_to";b:1;s:27:"ai-get-started-announcement";b:1;s:20:"globals_introduction";b:1;s:6:"e-apps";b:1;s:27:"e-editor-one-notice-pointer";b:1;}'), +(24, 1, 'dismissed_no_secure_connection_notice', 'true'); DROP TABLE IF EXISTS `wp_users`; CREATE TABLE `wp_users` ( diff --git a/tests/Support/Helper/WPClassicEditor.php b/tests/Support/Helper/WPClassicEditor.php index 0d1f5c97b..c8a1aca82 100644 --- a/tests/Support/Helper/WPClassicEditor.php +++ b/tests/Support/Helper/WPClassicEditor.php @@ -300,6 +300,9 @@ public function publishAndViewClassicEditorPage($I) // Publish Page. $I->publishClassicEditorPage($I); + // Wait for the notice to display. + $I->waitForElementVisible('.notice-success'); + // Load the Page on the frontend site. $I->click('.notice-success a'); diff --git a/tests/Support/Helper/WPQuickEdit.php b/tests/Support/Helper/WPQuickEdit.php index ee921e43e..3da5382f9 100644 --- a/tests/Support/Helper/WPQuickEdit.php +++ b/tests/Support/Helper/WPQuickEdit.php @@ -61,6 +61,9 @@ public function openQuickEdit($I, $postType, $postID) // Navigate to Post Type's WP_List_Table. $I->amOnAdminPage('edit.php?post_type=' . $postType); + // Wait for the WP_List_Table to load. + $I->waitForElementVisible('table.wp-list-table'); + // Hover mouse over Post's table row. $I->moveMouseOver('tr#post-' . $postID); From 2c6c809e782ecbbe8ba594fbc4e45e0bbf969689 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 29 Jun 2026 11:19:18 +0800 Subject: [PATCH 02/19] Quick Edit: Wait for row visibility before attempting mouseover --- tests/Support/Helper/WPQuickEdit.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/Support/Helper/WPQuickEdit.php b/tests/Support/Helper/WPQuickEdit.php index 3da5382f9..76641f438 100644 --- a/tests/Support/Helper/WPQuickEdit.php +++ b/tests/Support/Helper/WPQuickEdit.php @@ -64,13 +64,18 @@ public function openQuickEdit($I, $postType, $postID) // Wait for the WP_List_Table to load. $I->waitForElementVisible('table.wp-list-table'); - // Hover mouse over Post's table row. - $I->moveMouseOver('tr#post-' . $postID); + // Wait for the Post's row to exist in the table. + $I->waitForElementVisible('tr#post-' . $postID); - // Wait for Quick edit link to be visible. - $I->waitForElementVisible('tr#post-' . $postID . ' button.editinline'); + // Display all row actions by adding the `visible` class, as + // moveMouseOver() is flaky. + $I->executeJS(' + document.querySelectorAll(".row-actions").forEach(function(el){ + el.classList.add("visible"); + }); + '); - // Click Quick Edit link. + // Click Quick Edit link for the Post. $I->click('tr#post-' . $postID . ' button.editinline'); } } From ffadcbf011502ea05114262c2d8657a6be9c7eb2 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 29 Jun 2026 11:31:38 +0800 Subject: [PATCH 03/19] Reinstate all tests --- .github/workflows/tests.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 82ff9fd5d..1e6581ad2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,9 +33,28 @@ jobs: fail-fast: false matrix: wp-version: [ 'latest' ] - php-version: [ '8.1' ] + php-version: [ '8.1', '8.2', '8.3', '8.4' ] test-group: + - 'EndToEnd/broadcasts/blocks-shortcodes' + - 'EndToEnd/broadcasts/import-export' + - 'EndToEnd/forms/blocks-shortcodes' + - 'EndToEnd/forms/general' + - 'EndToEnd/forms/post-types' + - 'EndToEnd/general/other' + - 'EndToEnd/general/uninstall' + - 'EndToEnd/general/plugin-screens' + - 'EndToEnd/integrations/divi-builder' + - 'EndToEnd/integrations/divi-theme' + - 'EndToEnd/integrations/elementor' + - 'EndToEnd/integrations/other' + - 'EndToEnd/integrations/wlm' - 'EndToEnd/integrations/woocommerce' + - 'EndToEnd/landing-pages' + - 'EndToEnd/products' + - 'EndToEnd/restrict-content/general' + - 'EndToEnd/restrict-content/post-types' + - 'EndToEnd/tags' + - 'Integration' uses: ./.github/workflows/_run-tests.yml secrets: inherit From 518cc85672a8528570ab815e2872e8ec60b915fa Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 29 Jun 2026 11:32:55 +0800 Subject: [PATCH 04/19] Coding standards --- tests/Support/Helper/WPQuickEdit.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Support/Helper/WPQuickEdit.php b/tests/Support/Helper/WPQuickEdit.php index 76641f438..74002cf9c 100644 --- a/tests/Support/Helper/WPQuickEdit.php +++ b/tests/Support/Helper/WPQuickEdit.php @@ -69,11 +69,13 @@ public function openQuickEdit($I, $postType, $postID) // Display all row actions by adding the `visible` class, as // moveMouseOver() is flaky. - $I->executeJS(' + $I->executeJS( + ' document.querySelectorAll(".row-actions").forEach(function(el){ el.classList.add("visible"); }); - '); + ' + ); // Click Quick Edit link for the Post. $I->click('tr#post-' . $postID . ' button.editinline'); From 458392d6fab503d41ab1ba9b6b0d4fb801aaa8a5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 29 Jun 2026 12:10:37 +0800 Subject: [PATCH 05/19] Update GitHub Actions for Node 20 deprecation --- .github/workflows/_run-tests.yml | 45 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/.github/workflows/_run-tests.yml b/.github/workflows/_run-tests.yml index aa1c0b30c..934923c06 100644 --- a/.github/workflows/_run-tests.yml +++ b/.github/workflows/_run-tests.yml @@ -79,7 +79,7 @@ jobs: steps: - name: Define Test Group Name id: test-group - uses: mad9000/actions-find-and-replace-string@5 + uses: mad9000/actions-find-and-replace-string@6 with: source: ${{ inputs.test-group }} find: '/' @@ -272,29 +272,26 @@ jobs: # Write any secrets, such as API keys, to the .env.testing file now. - name: Define GitHub Secrets in .env.testing - uses: DamianReeves/write-file-action@v1.3 - with: - path: ${{ env.PLUGIN_DIR }}/.env.testing - contents: | - WORDPRESS_V3_BLOCK_EDITOR_ENABLED=${{ env.WORDPRESS_V3_BLOCK_EDITOR_ENABLED }} - WORDPRESS_DB_SQL_DUMP_FILE=${{ env.WORDPRESS_DB_SQL_DUMP_FILE }} - CONVERTKIT_API_KEY=${{ env.CONVERTKIT_API_KEY }} - CONVERTKIT_API_SECRET=${{ env.CONVERTKIT_API_SECRET }} - CONVERTKIT_API_KEY_NO_DATA=${{ env.CONVERTKIT_API_KEY_NO_DATA }} - CONVERTKIT_API_SECRET_NO_DATA=${{ env.CONVERTKIT_API_SECRET_NO_DATA }} - CONVERTKIT_OAUTH_ACCESS_TOKEN=${{ env.CONVERTKIT_OAUTH_ACCESS_TOKEN }} - CONVERTKIT_OAUTH_REFRESH_TOKEN=${{ env.CONVERTKIT_OAUTH_REFRESH_TOKEN }} - CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA=${{ env.CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA }} - CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA=${{ env.CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA }} - CONVERTKIT_OAUTH_CLIENT_ID=${{ env.CONVERTKIT_OAUTH_CLIENT_ID }} - CONVERTKIT_OAUTH_REDIRECT_URI=${{ env.CONVERTKIT_OAUTH_REDIRECT_URI }} - KIT_OAUTH_REDIRECT_URI=${{ env.KIT_OAUTH_REDIRECT_URI }} - CONVERTKIT_API_SIGNED_SUBSCRIBER_ID=${{ env.CONVERTKIT_API_SIGNED_SUBSCRIBER_ID }} - CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS=${{ env.CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS }} - CONVERTKIT_API_RECAPTCHA_SITE_KEY=${{ env.CONVERTKIT_API_RECAPTCHA_SITE_KEY }} - CONVERTKIT_API_RECAPTCHA_SECRET_KEY=${{ env.CONVERTKIT_API_RECAPTCHA_SECRET_KEY }} - - write-mode: overwrite + run: | + cat > ${{ env.PLUGIN_DIR }}/.env.testing << 'ENVEOF' + WORDPRESS_V3_BLOCK_EDITOR_ENABLED=${{ env.WORDPRESS_V3_BLOCK_EDITOR_ENABLED }} + WORDPRESS_DB_SQL_DUMP_FILE=${{ env.WORDPRESS_DB_SQL_DUMP_FILE }} + CONVERTKIT_API_KEY=${{ env.CONVERTKIT_API_KEY }} + CONVERTKIT_API_SECRET=${{ env.CONVERTKIT_API_SECRET }} + CONVERTKIT_API_KEY_NO_DATA=${{ env.CONVERTKIT_API_KEY_NO_DATA }} + CONVERTKIT_API_SECRET_NO_DATA=${{ env.CONVERTKIT_API_SECRET_NO_DATA }} + CONVERTKIT_OAUTH_ACCESS_TOKEN=${{ env.CONVERTKIT_OAUTH_ACCESS_TOKEN }} + CONVERTKIT_OAUTH_REFRESH_TOKEN=${{ env.CONVERTKIT_OAUTH_REFRESH_TOKEN }} + CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA=${{ env.CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA }} + CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA=${{ env.CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA }} + CONVERTKIT_OAUTH_CLIENT_ID=${{ env.CONVERTKIT_OAUTH_CLIENT_ID }} + CONVERTKIT_OAUTH_REDIRECT_URI=${{ env.CONVERTKIT_OAUTH_REDIRECT_URI }} + KIT_OAUTH_REDIRECT_URI=${{ env.KIT_OAUTH_REDIRECT_URI }} + CONVERTKIT_API_SIGNED_SUBSCRIBER_ID=${{ env.CONVERTKIT_API_SIGNED_SUBSCRIBER_ID }} + CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS=${{ env.CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS }} + CONVERTKIT_API_RECAPTCHA_SITE_KEY=${{ env.CONVERTKIT_API_RECAPTCHA_SITE_KEY }} + CONVERTKIT_API_RECAPTCHA_SECRET_KEY=${{ env.CONVERTKIT_API_RECAPTCHA_SECRET_KEY }} + ENVEOF # Installs wp-browser, Codeception, PHP CodeSniffer and anything else needed to run tests. - name: Run Composer From e9abadeb8e487acfe0afb39c5fc0954c16d2a431 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 12:01:06 +0800 Subject: [PATCH 06/19] Importers: Kit Legacy Forms Adds an option in Settings > Kit > Tools to replace Kit form shortcodes and blocks that use a legacy form with a standard Kit form --- ...admin-importer-convertkit-legacy-forms.php | 112 ++++++++++++++++++ .../class-convertkit-admin-importer.php | 16 ++- includes/class-wp-convertkit.php | 35 +++--- views/backend/settings/tools.php | 14 ++- wp-convertkit.php | 1 + 5 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php diff --git a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php new file mode 100644 index 000000000..5e78d7b63 --- /dev/null +++ b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php @@ -0,0 +1,112 @@ +description = __( 'Kit Legacy Forms are being phased out. Use this tool to replace Kit Form shortcodes and blocks using a Legacy Form with a new Kit Form.', 'convertkit' ); + + // Register this as an importer, if ConvertKit Legacy Forms exist. + add_filter( 'convertkit_get_form_importers', array( $this, 'register' ) ); + + } + + /** + * Returns an array of ConvertKit Legacy Forms form IDs and titles. + * + * @since 3.3.5 + * + * @return array + */ + public function get_forms() { + + // Query resource class to fetch legacy forms. + $convertkit_forms = new ConvertKit_Resource_Forms( 'settings' ); + if ( $convertkit_forms->exist() ) { + foreach ( $convertkit_forms->get() as $form ) { + // Skip if not a Legacy Form. + if ( ! $convertkit_forms->is_legacy( $form['id'] ) ) { + continue; + } + + $forms[ $form['id'] ] = $form['name']; + } + } + + return $forms; + + } + +} diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index 174b4eac6..105e07d68 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -32,6 +32,15 @@ abstract class ConvertKit_Admin_Importer { */ public $title = ''; + /** + * Holds the importer description. + * + * @since 3.3.5 + * + * @var string + */ + public $description = ''; + /** * Holds the shortcode name for the third party Form plugin. * @@ -99,9 +108,10 @@ public function register( $importers ) { // Add this importer to the list of importers. $importers[ $this->name ] = array( - 'name' => $this->name, - 'title' => $this->title, - 'forms' => $this->get_forms(), + 'name' => $this->name, + 'title' => $this->title, + 'description' => $this->description, + 'forms' => $this->get_forms(), ); return $importers; diff --git a/includes/class-wp-convertkit.php b/includes/class-wp-convertkit.php index 9e49cc858..659974839 100644 --- a/includes/class-wp-convertkit.php +++ b/includes/class-wp-convertkit.php @@ -79,23 +79,24 @@ private function initialize_admin() { return; } - $this->classes['admin_bulk_edit'] = new ConvertKit_Admin_Bulk_Edit(); - $this->classes['admin_cache_plugins'] = new ConvertKit_Admin_Cache_Plugins(); - $this->classes['admin_category'] = new ConvertKit_Admin_Category(); - $this->classes['admin_landing_page'] = new ConvertKit_Admin_Landing_Page(); - $this->classes['admin_importer_activecampaign'] = new ConvertKit_Admin_Importer_ActiveCampaign(); - $this->classes['admin_importer_aweber'] = new ConvertKit_Admin_Importer_AWeber(); - $this->classes['admin_importer_campaignmonitor'] = new ConvertKit_Admin_Importer_CampaignMonitor(); - $this->classes['admin_importer_mc4wp'] = new ConvertKit_Admin_Importer_MC4WP(); - $this->classes['admin_importer_mailpoet'] = new ConvertKit_Admin_Importer_Mailpoet(); - $this->classes['admin_importer_newsletter'] = new ConvertKit_Admin_Importer_Newsletter(); - $this->classes['admin_post'] = new ConvertKit_Admin_Post(); - $this->classes['admin_quick_edit'] = new ConvertKit_Admin_Quick_Edit(); - $this->classes['admin_restrict_content'] = new ConvertKit_Admin_Restrict_Content(); - $this->classes['admin_settings'] = new ConvertKit_Admin_Settings(); - $this->classes['admin_setup_wizard_landing_page'] = new ConvertKit_Admin_Setup_Wizard_Landing_Page(); - $this->classes['admin_setup_wizard_plugin'] = new ConvertKit_Admin_Setup_Wizard_Plugin(); - $this->classes['admin_setup_wizard_restrict_content'] = new ConvertKit_Admin_Setup_Wizard_Restrict_Content(); + $this->classes['admin_bulk_edit'] = new ConvertKit_Admin_Bulk_Edit(); + $this->classes['admin_cache_plugins'] = new ConvertKit_Admin_Cache_Plugins(); + $this->classes['admin_category'] = new ConvertKit_Admin_Category(); + $this->classes['admin_landing_page'] = new ConvertKit_Admin_Landing_Page(); + $this->classes['admin_importer_activecampaign'] = new ConvertKit_Admin_Importer_ActiveCampaign(); + $this->classes['admin_importer_aweber'] = new ConvertKit_Admin_Importer_AWeber(); + $this->classes['admin_importer_campaignmonitor'] = new ConvertKit_Admin_Importer_CampaignMonitor(); + $this->classes['admin_importer_convertkit_legacy_forms'] = new ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + $this->classes['admin_importer_mc4wp'] = new ConvertKit_Admin_Importer_MC4WP(); + $this->classes['admin_importer_mailpoet'] = new ConvertKit_Admin_Importer_Mailpoet(); + $this->classes['admin_importer_newsletter'] = new ConvertKit_Admin_Importer_Newsletter(); + $this->classes['admin_post'] = new ConvertKit_Admin_Post(); + $this->classes['admin_quick_edit'] = new ConvertKit_Admin_Quick_Edit(); + $this->classes['admin_restrict_content'] = new ConvertKit_Admin_Restrict_Content(); + $this->classes['admin_settings'] = new ConvertKit_Admin_Settings(); + $this->classes['admin_setup_wizard_landing_page'] = new ConvertKit_Admin_Setup_Wizard_Landing_Page(); + $this->classes['admin_setup_wizard_plugin'] = new ConvertKit_Admin_Setup_Wizard_Plugin(); + $this->classes['admin_setup_wizard_restrict_content'] = new ConvertKit_Admin_Setup_Wizard_Restrict_Content(); /** * Initialize integration classes for the WordPress Administration interface. diff --git a/views/backend/settings/tools.php b/views/backend/settings/tools.php index 728fe380a..315fa3ada 100644 --- a/views/backend/settings/tools.php +++ b/views/backend/settings/tools.php @@ -118,11 +118,15 @@


diff --git a/wp-convertkit.php b/wp-convertkit.php index 4b8eb1acc..c2afddcf5 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -121,6 +121,7 @@ require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-activecampaign.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-aweber.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-campaignmonitor.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-mc4wp.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-mailpoet.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-newsletter.php'; From 8be663e4f6160d9106ce54584ec04a3369099f2b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 12:25:52 +0800 Subject: [PATCH 07/19] Only detect legacy forms, ignoring non-legacy forms --- ...admin-importer-convertkit-legacy-forms.php | 155 +++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php index 5e78d7b63..8a01740ee 100644 --- a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php +++ b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php @@ -84,7 +84,7 @@ public function __construct() { } /** - * Returns an array of ConvertKit Legacy Forms form IDs and titles. + * Returns an array of Kit Legacy Forms form IDs and titles. * * @since 3.3.5 * @@ -93,6 +93,7 @@ public function __construct() { public function get_forms() { // Query resource class to fetch legacy forms. + $forms = array(); $convertkit_forms = new ConvertKit_Resource_Forms( 'settings' ); if ( $convertkit_forms->exist() ) { foreach ( $convertkit_forms->get() as $form ) { @@ -109,4 +110,156 @@ public function get_forms() { } + /** + * Returns an array of legacy Kit form IDs (as strings) for use in + * filtering shortcodes/blocks that reference them. + * + * @since 3.3.5 + * + * @return array + */ + private function get_legacy_form_ids() { + + $legacy_ids = array(); + $convertkit_forms = new ConvertKit_Resource_Forms( 'settings' ); + + if ( ! $convertkit_forms->exist() ) { + return $legacy_ids; + } + + foreach ( $convertkit_forms->get() as $form ) { + if ( ! $convertkit_forms->is_legacy( $form['id'] ) ) { + continue; + } + $legacy_ids[] = (string) $form['id']; + } + + return $legacy_ids; + + } + + /** + * Overrides the parent method to: + * - return form IDs found in both shortcodes AND blocks (the parent only + * handles shortcodes), and + * - filter the result so only legacy form IDs are returned. + * + * @since 3.3.5 + * + * @param string $content Content containing Kit Form shortcodes / blocks. + * @return array + */ + public function get_form_ids_from_content( $content ) { + + // Get shortcode-derived form IDs from the parent. + $shortcode_ids = parent::get_form_ids_from_content( $content ); + + // Get block-derived form IDs (parent only handles shortcodes). + $block_ids = $this->get_block_form_ids_from_content( $content ); + + // Combine and filter to legacy form IDs only. + $all_ids = array_unique( array_merge( $shortcode_ids, $block_ids ) ); + $legacy_ids = $this->get_legacy_form_ids(); + + // Cast both sides to strings for safe comparison. + $all_ids = array_map( 'strval', $all_ids ); + + return array_values( array_intersect( $all_ids, $legacy_ids ) ); + + } + + /** + * Returns an array of form IDs from convertkit/form blocks in the given + * content. Walks innerBlocks recursively. + * + * @since 3.3.5 + * + * @param string $content Content containing Kit Form blocks. + * @return array + */ + private function get_block_form_ids_from_content( $content ) { + + return $this->extract_block_form_ids( parse_blocks( $content ) ); + + } + + /** + * Recursively walks blocks (and innerBlocks) and returns an array of form + * IDs from any convertkit/form block's `form` attribute. + * + * @since 3.3.5 + * + * @param array $blocks Blocks. + * @return array + */ + private function extract_block_form_ids( $blocks ) { + + $form_ids = array(); + + foreach ( $blocks as $block ) { + if ( ! empty( $block['innerBlocks'] ) ) { + $form_ids = array_merge( + $form_ids, + $this->extract_block_form_ids( $block['innerBlocks'] ) + ); + } + + if ( $block['blockName'] !== $this->block_name ) { + continue; + } + + if ( empty( $block['attrs'][ $this->block_id_attribute ] ) ) { + continue; + } + + $form_ids[] = (string) $block['attrs'][ $this->block_id_attribute ]; + } + + return $form_ids; + + } + + /** + * Overrides the parent method to only return post IDs whose content + * contains a Kit Form shortcode or block referencing a legacy form ID. + * + * The parent's broad SQL match returns any post containing a + * `[convertkit_form` shortcode or `' . $this->html_block ), + ] + ); + + // Replace the blocks in the post. + $this->importer->replace_blocks_in_post( $postID, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ); + + // Test the block is replaced with the Kit form block, and special characters are not stripped. + $this->assertEquals( + '' . $this->html_block, + get_post_field( 'post_content', $postID ) + ); + } + + /** + * Test that the replace_blocks_in_content() method replaces the third party form block with the Kit form block, + * and special characters are not stripped. + * + * @since 3.3.5 + */ + public function testKitLegacyFormsReplaceBlocksInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the blocks to test. + $content = '' . $this->html_block; + + // Test the block is replaced with the Kit form block. + $this->assertEquals( + '' . $this->html_block, + $this->importer->replace_blocks_in_content( parse_blocks( $content ), $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + + /** + * Test that get_form_ids_from_content() returns Kit legacy form block Form IDs + * (in addition to shortcode Form IDs), ignoring any non-legacy Kit form blocks. + * + * Exercises the block-extraction extension to the get_form_ids_from_content() + * override on the Kit Legacy Forms importer. + * + * @since 3.3.5 + */ + public function testGetKitLegacyFormsFormIDsFromContentForBlocks() + { + // Populate the Forms resource so the importer can determine which form + // IDs are legacy. Without this, the override has nothing to filter against. + $this->setupKitFormsResource(); + + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define content containing both legacy and non-legacy Kit form blocks, + // plus a legacy shortcode, to confirm both block and shortcode IDs are + // surfaced when they reference a legacy form. + $content = '' + . '' + . '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]'; + + // Extract form IDs from content. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert only the legacy form ID is returned (deduplicated across the + // shortcode and block references). + $this->assertCount( 1, $form_ids ); + $this->assertEquals( (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $form_ids[0] ); + } + + /** + * Test that get_forms_in_posts() only returns post IDs whose content + * contains a Kit Form shortcode or block referencing a legacy form ID. + * + * Exercises the get_forms_in_posts() override on the Kit Legacy Forms + * importer, which narrows the parent class's broad SQL match. + * + * @since 3.3.5 + */ + public function testGetKitLegacyFormsInPosts() + { + // Populate the Forms resource so the importer can determine which form + // IDs are legacy. Without this, the override has nothing to filter against. + $this->setupKitFormsResource(); + + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Create a post containing a non-legacy Kit Form shortcode. This post + // should NOT be returned by the Legacy Forms importer. + $nonLegacyShortcodePostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Non-Legacy Shortcode Post', + 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + ] + ); + + // Create a post containing a non-legacy Kit Form block. This post + // should NOT be returned by the Legacy Forms importer. + $nonLegacyBlockPostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Non-Legacy Block Post', + 'post_content' => '', + ] + ); + + // Create a post containing a legacy Kit Form shortcode. This post + // SHOULD be returned by the Legacy Forms importer. + $legacyShortcodePostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Legacy Shortcode Post', + 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + ] + ); + + // Create a post containing a legacy Kit Form block. This post + // SHOULD be returned by the Legacy Forms importer. + $legacyBlockPostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Legacy Block Post', + 'post_content' => '', + ] + ); + + // Fetch the list of post IDs that contain a legacy Kit form reference. + $post_ids = $this->importer->get_forms_in_posts(); + + // Cast post IDs to ints for comparison (wpdb returns strings). + $post_ids = array_map( 'intval', $post_ids ); + + // Assert only the legacy posts are returned, and the non-legacy posts are not. + $this->assertContains( $legacyShortcodePostID, $post_ids ); + $this->assertContains( $legacyBlockPostID, $post_ids ); + $this->assertNotContains( $nonLegacyShortcodePostID, $post_ids ); + $this->assertNotContains( $nonLegacyBlockPostID, $post_ids ); + } + + /** + * Populates the Kit Forms resource cache by storing credentials and + * fetching forms from the API. Required by the Kit Legacy Forms importer + * tests that exercise the legacy-filtering overrides. + * + * @since 3.3.5 + */ + private function setupKitFormsResource() + { + // Store credentials in the Plugin's settings. + $settings = new \ConvertKit_Settings(); + update_option( + $settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + ] + ); + + // Refresh the Forms resource so is_legacy() can determine which form IDs + // are legacy. + $resource = new \ConvertKit_Resource_Forms(); + $resource->refresh(); + } + /** * Test that the get_form_ids_from_content() method returns MC4WP form shortcode Form IDs * ignoring any other shortcodes. From bfb56ead9727be58d8f9e818a916139a58a13f5d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 12:53:26 +0800 Subject: [PATCH 09/19] Tools: Migrate: Use correct shortcode attribute --- .../class-convertkit-admin-importer.php | 2 +- tests/Integration/ImporterTest.php | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index 174b4eac6..b4a9ccf72 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -288,7 +288,7 @@ public function replace_shortcodes_in_content( $content, $third_party_form_id, $ return preg_replace( $pattern, - '[convertkit_form id="' . $form_id . '"]', + '[convertkit_form form="' . $form_id . '"]', $content ); diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index c11f99668..0b3d27c46 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -108,21 +108,21 @@ public function testActiveCampaignReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 1, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 1, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 1, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -143,7 +143,7 @@ public function testActiveCampaignReplaceShortcodesInContentIgnoringOtherShortco // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; @@ -272,21 +272,21 @@ public function testAWeberReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -307,7 +307,7 @@ public function testAWeberReplaceShortcodesInContentIgnoringOtherShortcodes() // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; @@ -435,21 +435,21 @@ public function testCampaignMonitorReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 'cm_6912dba75db2d', $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 'cm_6912dba75db2d', $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 'cm_6912dba75db2d', $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -470,7 +470,7 @@ public function testCampaignMonitorReplaceShortcodesInContentIgnoringOtherShortc // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; @@ -539,21 +539,21 @@ public function testMC4WPReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -574,7 +574,7 @@ public function testMC4WPReplaceShortcodesInContentIgnoringOtherShortcodes() // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; @@ -693,21 +693,21 @@ public function testMailPoetReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -728,7 +728,7 @@ public function testMailPoetReplaceShortcodesInContentIgnoringOtherShortcodes() // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; @@ -846,21 +846,21 @@ public function testNewsletterReplaceShortcodesInContent() // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, 0, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, 0, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, 0, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -881,7 +881,7 @@ public function testNewsletterReplaceShortcodesInContentIgnoringOtherShortcodes( // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; From 6fe25cdea1f5eed0c8d56954da3d8e6fd27ca66a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 13:14:21 +0800 Subject: [PATCH 10/19] Importers: Support Multiple Attribute Detection --- .../class-convertkit-admin-importer.php | 115 ++++++++++++------ 1 file changed, 80 insertions(+), 35 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index b4a9ccf72..43f7fae60 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -42,11 +42,15 @@ abstract class ConvertKit_Admin_Importer { public $shortcode_name = ''; /** - * Holds the shortcode ID attribute name for the third party Form plugin. + * Holds the shortcode ID attribute name(s) for the third party Form plugin. + * + * Accepts a string for a single attribute (e.g. 'form'), or an array of + * strings for plugins where the form ID may appear under multiple attribute + * names (e.g. ['form', 'id']). * * @since 3.1.0 * - * @var bool|string + * @var bool|string|array */ public $shortcode_id_attribute = false; @@ -60,11 +64,15 @@ abstract class ConvertKit_Admin_Importer { public $block_name = ''; /** - * Holds the block ID attribute name for the third party Form plugin. + * Holds the block ID attribute name(s) for the third party Form plugin. + * + * Accepts a string for a single attribute (e.g. 'form'), or an array of + * strings for plugins where the form ID may appear under multiple attribute + * names (e.g. ['form', 'id']). * * @since 3.1.6 * - * @var bool|string + * @var bool|string|array */ public $block_id_attribute = false; @@ -276,21 +284,36 @@ public function replace_shortcodes_in_content( $content, $third_party_form_id, $ $pattern = '/\[' // Start regex with an opening square bracket. . preg_quote( $this->shortcode_name, '/' ) // Match the shortcode name, escaping any regex special chars. . '[^\]]*?\]/i'; // Match any other characters (non-greedy) up to the closing square bracket, case-insensitive. - } else { + + return preg_replace( + $pattern, + '[convertkit_form form="' . $form_id . '"]', + $content + ); + } + + // Normalise the shortcode ID attribute to an array, so importers can + // declare a single attribute (string) or multiple attributes (array). + $id_attributes = (array) $this->shortcode_id_attribute; + + // Run a replacement pass per attribute name. + foreach ( $id_attributes as $id_attribute ) { $pattern = '/\[' // Start regex with an opening square bracket. . preg_quote( $this->shortcode_name, '/' ) // Match the shortcode name, escaping any regex special chars. . '[^\]]*?' // Match any characters that are not a closing square bracket, non-greedy. - . '\b' . preg_quote( $this->shortcode_id_attribute, '/' ) // Match the id attribute word boundary and escape as needed. + . '\b' . preg_quote( $id_attribute, '/' ) // Match the id attribute word boundary and escape as needed. . '\s*=\s*' // Match optional whitespace around an equals sign. . '(?:"' . preg_quote( (string) $third_party_form_id, '/' ) . '"|\'' . preg_quote( (string) $third_party_form_id, '/' ) . '\'|' . preg_quote( (string) $third_party_form_id, '/' ) . ')' // Match the form ID, double quotes, single quotes or unquoted. . '[^\]]*?\]/i'; // Match any other characters (non-greedy) up to the closing square bracket, case-insensitive. + + $content = preg_replace( + $pattern, + '[convertkit_form form="' . $form_id . '"]', + $content + ); } - return preg_replace( - $pattern, - '[convertkit_form form="' . $form_id . '"]', - $content - ); + return $content; } @@ -359,29 +382,37 @@ public function get_form_ids_from_content( $content ) { return array(); } - // Legacy: Extract where attribute is required. - $pattern = '/\[' // Start regex with an opening square bracket. - . preg_quote( $this->shortcode_name, '/' ) // Match the shortcode name, escaping any regex special chars. - . '(?:\s+[^\]]*)?' // Optionally match any attributes (key/value pairs), non-greedy. - . preg_quote( $this->shortcode_id_attribute, '/' ) // Match the id attribute name. - . '\s*=\s*' // Optional whitespace, equals sign, optional whitespace. - . '(?:"([^"]+)"|\'([^\']+)\'|([^\s\]]+))' // Capture double quoted, single quoted or unquoted value. - . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. + // Normalise the shortcode ID attribute to an array, so importers can + // declare a single attribute (string) or multiple attributes (array). + $id_attributes = (array) $this->shortcode_id_attribute; - preg_match_all( $pattern, $content, $matches ); + // Extract form IDs, running a match pass per attribute name. + $form_ids = array(); + foreach ( $id_attributes as $id_attribute ) { + $pattern = '/\[' // Start regex with an opening square bracket. + . preg_quote( $this->shortcode_name, '/' ) // Match the shortcode name, escaping any regex special chars. + . '(?:\s+[^\]]*)?' // Optionally match any attributes (key/value pairs), non-greedy. + . preg_quote( $id_attribute, '/' ) // Match the id attribute name. + . '\s*=\s*' // Optional whitespace, equals sign, optional whitespace. + . '(?:"([^"]+)"|\'([^\']+)\'|([^\s\]]+))' // Capture double quoted, single quoted or unquoted value. + . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. - // Extract form IDs: They could be in either $matches[1] (double quoted), $matches[2] (single quoted) or $matches[3] (unquoted). - $form_ids = array_values( - array_filter( - array_merge( - isset( $matches[1] ) ? $matches[1] : array(), - isset( $matches[2] ) ? $matches[2] : array(), - isset( $matches[3] ) ? $matches[3] : array() + preg_match_all( $pattern, $content, $matches ); + + // Extract form IDs: They could be in either $matches[1] (double quoted), $matches[2] (single quoted) or $matches[3] (unquoted). + $form_ids = array_merge( + $form_ids, + array_filter( + array_merge( + isset( $matches[1] ) ? $matches[1] : array(), + isset( $matches[2] ) ? $matches[2] : array(), + isset( $matches[3] ) ? $matches[3] : array() + ) ) - ) - ); + ); + } - return $form_ids; + return array_values( array_unique( $form_ids ) ); } @@ -504,13 +535,27 @@ private function recursively_convert_blocks( $blocks, $third_party_form_id, $for // If the block ID attribute is not set, the third party Plugin doesn't use IDs, // so there's no need to check the $third_party_form_id matches the block attribute. if ( $this->block_id_attribute ) { - // Skip if the attribute doesn't exist i.e. the block was not configured. - if ( ! array_key_exists( $this->block_id_attribute, $block['attrs'] ) ) { - continue; + // Normalise the block ID attribute to an array, so importers can + // declare a single attribute (string) or multiple attributes (array). + $id_attributes = (array) $this->block_id_attribute; + + // Check if at least one attribute contains the third party form ID. + $matched = false; + foreach ( $id_attributes as $id_attribute ) { + if ( ! array_key_exists( $id_attribute, $block['attrs'] ) ) { + continue; + } + + if ( stripos( $block['attrs'][ $id_attribute ], (string) $third_party_form_id ) === false ) { + continue; + } + + $matched = true; + break; } - // Skip if the third party form ID doesn't exist within the third party form block's attribute. - if ( stripos( $block['attrs'][ $this->block_id_attribute ], (string) $third_party_form_id ) === false ) { + // Skip if none of the configured ID attributes match. + if ( ! $matched ) { continue; } } From 61bba6479185a57f26a182a73809b8ade4eddae2 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 13:54:25 +0800 Subject: [PATCH 11/19] Detect legacy forms with `id` or `form` attributes --- ...admin-importer-convertkit-legacy-forms.php | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php index 8a01740ee..d1d8a6f8f 100644 --- a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php +++ b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php @@ -42,13 +42,16 @@ class ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms extends ConvertKit_Admin public $shortcode_name = 'convertkit_form'; /** - * Holds the ID attribute name for ConvertKit Legacy Forms. + * Holds the ID attribute names for Kit Legacy Form shortcodes. + * + * Both `form` and `id` are matched because Kit's `[convertkit_form]` + * shortcode has supported both attribute names historically. * * @since 3.3.5 * - * @var string + * @var array */ - public $shortcode_id_attribute = 'form'; + public $shortcode_id_attribute = array( 'form', 'id' ); /** * Holds the block name for ConvertKit Legacy Forms. @@ -60,13 +63,16 @@ class ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms extends ConvertKit_Admin public $block_name = 'convertkit/form'; /** - * Holds the ID attribute name for ConvertKit Legacy Forms. + * Holds the ID attribute names for Kit Legacy Form blocks. + * + * Both `form` and `id` are matched because Kit's form block has supported + * both attribute names historically. * * @since 3.3.5 * - * @var string + * @var array */ - public $block_id_attribute = 'form'; + public $block_id_attribute = array( 'form', 'id' ); /** * Constructor @@ -196,6 +202,10 @@ private function extract_block_form_ids( $blocks ) { $form_ids = array(); + // Normalise the block ID attribute(s) to an array so we can match + // against multiple attribute names (e.g. both `form` and `id`). + $id_attributes = (array) $this->block_id_attribute; + foreach ( $blocks as $block ) { if ( ! empty( $block['innerBlocks'] ) ) { $form_ids = array_merge( @@ -208,11 +218,16 @@ private function extract_block_form_ids( $blocks ) { continue; } - if ( empty( $block['attrs'][ $this->block_id_attribute ] ) ) { - continue; + // Record the form ID from the first matching attribute. If a block + // somehow has both `form` and `id` set, they'd be the same value, so + // we stop after the first match to avoid double-counting. + foreach ( $id_attributes as $id_attribute ) { + if ( empty( $block['attrs'][ $id_attribute ] ) ) { + continue; + } + $form_ids[] = (string) $block['attrs'][ $id_attribute ]; + break; } - - $form_ids[] = (string) $block['attrs'][ $this->block_id_attribute ]; } return $form_ids; From a28a75ee99e07b10d9fa6cfeb54c3e94a4a31431 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 13:54:28 +0800 Subject: [PATCH 12/19] Updated tests --- tests/Integration/ImporterTest.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index ecacf1675..c0e71a994 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -648,26 +648,28 @@ public function testKitLegacyFormsReplaceShortcodesInContent() $shortcodes = [ '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', '[convertkit_form form=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', + '[convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + '[convertkit_form id=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', ]; // Test each shortcode is replaced with the Kit form shortcode. foreach ( $shortcodes as $shortcode ) { $this->assertEquals( - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $shortcode, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) ); // Prepend and append some content and duplicate the shortcode. $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; $this->assertEquals( - 'Some content before the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } @@ -688,7 +690,8 @@ public function testKitLegacyFormsReplaceShortcodesInContentIgnoringOtherShortco // Define the shortcodes to test. $shortcodes = [ - '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', '[a_random_shortcode]', ]; From 025392e847c232443d27bef8b6ba3e39ca5720eb Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:00:47 +0800 Subject: [PATCH 13/19] Remove MC4WP references --- ...lass-convertkit-admin-importer-convertkit-legacy-forms.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php index d1d8a6f8f..fc491d34a 100644 --- a/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php +++ b/admin/importers/class-convertkit-admin-importer-convertkit-legacy-forms.php @@ -1,13 +1,13 @@ Date: Tue, 30 Jun 2026 14:00:53 +0800 Subject: [PATCH 14/19] Remove redundant test helper --- tests/Integration/ImporterTest.php | 33 ------------------------------ 1 file changed, 33 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index c0e71a994..2a6192d69 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -773,10 +773,6 @@ public function testKitLegacyFormsReplaceBlocksInContent() */ public function testGetKitLegacyFormsFormIDsFromContentForBlocks() { - // Populate the Forms resource so the importer can determine which form - // IDs are legacy. Without this, the override has nothing to filter against. - $this->setupKitFormsResource(); - // Initialize the class we want to test. $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); @@ -810,10 +806,6 @@ public function testGetKitLegacyFormsFormIDsFromContentForBlocks() */ public function testGetKitLegacyFormsInPosts() { - // Populate the Forms resource so the importer can determine which form - // IDs are legacy. Without this, the override has nothing to filter against. - $this->setupKitFormsResource(); - // Initialize the class we want to test. $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); @@ -877,31 +869,6 @@ public function testGetKitLegacyFormsInPosts() $this->assertNotContains( $nonLegacyBlockPostID, $post_ids ); } - /** - * Populates the Kit Forms resource cache by storing credentials and - * fetching forms from the API. Required by the Kit Legacy Forms importer - * tests that exercise the legacy-filtering overrides. - * - * @since 3.3.5 - */ - private function setupKitFormsResource() - { - // Store credentials in the Plugin's settings. - $settings = new \ConvertKit_Settings(); - update_option( - $settings::SETTINGS_NAME, - [ - 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], - 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], - ] - ); - - // Refresh the Forms resource so is_legacy() can determine which form IDs - // are legacy. - $resource = new \ConvertKit_Resource_Forms(); - $resource->refresh(); - } - /** * Test that the get_form_ids_from_content() method returns MC4WP form shortcode Form IDs * ignoring any other shortcodes. From 29df35f264e643f6955182fbba331beffe22017a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:03:48 +0800 Subject: [PATCH 15/19] Update tests --- tests/Integration/ImporterTest.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index 2a6192d69..7ebbc08df 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -621,7 +621,7 @@ public function testGetKitLegacyFormsFormIDsFromContent() $this->assertNotInstanceOf(\WP_Error::class, $this->importer); // Define the content to test. - $content = 'Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] non-Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Different shortcode: [aweber formid="12"]'; + $content = 'Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] Legacy Form ID: [convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] non-Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Different shortcode: [aweber formid="12"]'; // Extract form IDs from content. $form_ids = $this->importer->get_form_ids_from_content( $content ); @@ -634,7 +634,7 @@ public function testGetKitLegacyFormsFormIDsFromContent() /** * Test that the replace_shortcodes_in_content() method replaces the Kit legacy form shortcode with the Kit form shortcode. * - * @since 3.1.0 + * @since 3.3.5 */ public function testKitLegacyFormsReplaceShortcodesInContent() { @@ -678,7 +678,7 @@ public function testKitLegacyFormsReplaceShortcodesInContent() /** * Test that the replace_shortcodes_in_content() method ignores non-Kit legacy form shortcodes. * - * @since 3.1.0 + * @since 3.3.5 */ public function testKitLegacyFormsReplaceShortcodesInContentIgnoringOtherShortcodes() { @@ -708,7 +708,7 @@ public function testKitLegacyFormsReplaceShortcodesInContentIgnoringOtherShortco * Test that the replace_blocks_in_post() method replaces the third party form block with the Kit form block, * and special characters are not stripped when the Post is saved. * - * @since 3.1.6 + * @since 3.3.5 */ public function testKitLegacyFormsReplaceBlocksInPost() { @@ -812,8 +812,7 @@ public function testGetKitLegacyFormsInPosts() // Confirm initialization didn't result in an error. $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - // Create a post containing a non-legacy Kit Form shortcode. This post - // should NOT be returned by the Legacy Forms importer. + // Create a post containing a non-legacy Kit Form shortcode. $nonLegacyShortcodePostID = $this->factory->post->create( [ 'post_type' => 'page', @@ -823,8 +822,7 @@ public function testGetKitLegacyFormsInPosts() ] ); - // Create a post containing a non-legacy Kit Form block. This post - // should NOT be returned by the Legacy Forms importer. + // Create a post containing a non-legacy Kit Form block. $nonLegacyBlockPostID = $this->factory->post->create( [ 'post_type' => 'page', @@ -834,8 +832,7 @@ public function testGetKitLegacyFormsInPosts() ] ); - // Create a post containing a legacy Kit Form shortcode. This post - // SHOULD be returned by the Legacy Forms importer. + // Create a post containing a legacy Kit Form shortcode. $legacyShortcodePostID = $this->factory->post->create( [ 'post_type' => 'page', @@ -845,8 +842,7 @@ public function testGetKitLegacyFormsInPosts() ] ); - // Create a post containing a legacy Kit Form block. This post - // SHOULD be returned by the Legacy Forms importer. + // Create a post containing a legacy Kit Form block. $legacyBlockPostID = $this->factory->post->create( [ 'post_type' => 'page', From 440d848feeb5e047260f19f3274e0da37204577e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:19:22 +0800 Subject: [PATCH 16/19] Add success notice --- admin/section/class-convertkit-admin-section-tools.php | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/section/class-convertkit-admin-section-tools.php b/admin/section/class-convertkit-admin-section-tools.php index e5524230a..e95767f2b 100644 --- a/admin/section/class-convertkit-admin-section-tools.php +++ b/admin/section/class-convertkit-admin-section-tools.php @@ -60,6 +60,7 @@ public function register_notices( $notices ) { 'migrate_activecampaign_configuration_success' => __( 'ActiveCampaign forms migrated successfully.', 'convertkit' ), 'migrate_aweber_configuration_success' => __( 'AWeber forms migrated successfully.', 'convertkit' ), 'migrate_campaignmonitor_configuration_success' => __( 'Campaign Monitor forms migrated successfully.', 'convertkit' ), + 'migrate_convertkit_legacy_forms_configuration_success' => __( 'Kit Legacy Forms migrated successfully.', 'convertkit' ), 'migrate_mc4wp_configuration_success' => __( 'MC4WP forms migrated successfully.', 'convertkit' ), 'migrate_mailpoet_configuration_success' => __( 'MailPoet forms migrated successfully.', 'convertkit' ), 'migrate_newsletter_configuration_success' => __( 'Newsletter forms migrated successfully.', 'convertkit' ), From 50ab9948dc65b0674b619dbe3dc58507799e9d93 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:20:09 +0800 Subject: [PATCH 17/19] Remove tests for separate PR --- tests/Integration/ImporterTest.php | 259 ----------------------------- 1 file changed, 259 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index 7ebbc08df..c09ff8c03 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -606,265 +606,6 @@ public function testCampaignMonitorReplaceShortcodesInContentIgnoringOtherShortc } } - /** - * Test that the get_form_ids_from_content() method returns Kit legacy form shortcode Form IDs - * ignoring any other shortcodes. - * - * @since 3.3.5 - */ - public function testGetKitLegacyFormsFormIDsFromContent() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Define the content to test. - $content = 'Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] Legacy Form ID: [convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] non-Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Different shortcode: [aweber formid="12"]'; - - // Extract form IDs from content. - $form_ids = $this->importer->get_form_ids_from_content( $content ); - - // Assert the legacy form shortcode was detected, and the non-legacy form shortcode was ignored. - $this->assertEquals( 1, count( $form_ids ) ); - $this->assertEquals( $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $form_ids[0] ); - } - - /** - * Test that the replace_shortcodes_in_content() method replaces the Kit legacy form shortcode with the Kit form shortcode. - * - * @since 3.3.5 - */ - public function testKitLegacyFormsReplaceShortcodesInContent() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Define the shortcodes to test. - $shortcodes = [ - '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', - '[convertkit_form form=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', - '[convertkit_form id=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', - ]; - - // Test each shortcode is replaced with the Kit form shortcode. - foreach ( $shortcodes as $shortcode ) { - $this->assertEquals( - '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', - $this->importer->replace_shortcodes_in_content( $shortcode, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) - ); - - // Prepend and append some content. - $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; - $this->assertEquals( - 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', - $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) - ); - - // Prepend and append some content and duplicate the shortcode. - $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; - $this->assertEquals( - 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', - $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) - ); - } - } - - /** - * Test that the replace_shortcodes_in_content() method ignores non-Kit legacy form shortcodes. - * - * @since 3.3.5 - */ - public function testKitLegacyFormsReplaceShortcodesInContentIgnoringOtherShortcodes() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Define the shortcodes to test. - $shortcodes = [ - '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', - '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', - '[a_random_shortcode]', - ]; - - // Test each shortcode is ignored. - foreach ( $shortcodes as $shortcode ) { - $this->assertEquals( - $shortcode, - $this->importer->replace_shortcodes_in_content( $shortcode, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) - ); - } - } - - /** - * Test that the replace_blocks_in_post() method replaces the third party form block with the Kit form block, - * and special characters are not stripped when the Post is saved. - * - * @since 3.3.5 - */ - public function testKitLegacyFormsReplaceBlocksInPost() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Create a Post with a MC4WP form block and HTML block, as if the user already created this post. - $postID = $this->factory->post->create( - [ - 'post_type' => 'page', - 'post_status' => 'publish', - 'post_title' => 'Kit Legacy Forms: Replace Blocks in Post', - 'post_content' => str_replace( '\\', '\\\\', '' . $this->html_block ), - ] - ); - - // Replace the blocks in the post. - $this->importer->replace_blocks_in_post( $postID, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ); - - // Test the block is replaced with the Kit form block, and special characters are not stripped. - $this->assertEquals( - '' . $this->html_block, - get_post_field( 'post_content', $postID ) - ); - } - - /** - * Test that the replace_blocks_in_content() method replaces the third party form block with the Kit form block, - * and special characters are not stripped. - * - * @since 3.3.5 - */ - public function testKitLegacyFormsReplaceBlocksInContent() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Define the blocks to test. - $content = '' . $this->html_block; - - // Test the block is replaced with the Kit form block. - $this->assertEquals( - '' . $this->html_block, - $this->importer->replace_blocks_in_content( parse_blocks( $content ), $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) - ); - } - - /** - * Test that get_form_ids_from_content() returns Kit legacy form block Form IDs - * (in addition to shortcode Form IDs), ignoring any non-legacy Kit form blocks. - * - * Exercises the block-extraction extension to the get_form_ids_from_content() - * override on the Kit Legacy Forms importer. - * - * @since 3.3.5 - */ - public function testGetKitLegacyFormsFormIDsFromContentForBlocks() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Define content containing both legacy and non-legacy Kit form blocks, - // plus a legacy shortcode, to confirm both block and shortcode IDs are - // surfaced when they reference a legacy form. - $content = '' - . '' - . '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]'; - - // Extract form IDs from content. - $form_ids = $this->importer->get_form_ids_from_content( $content ); - - // Assert only the legacy form ID is returned (deduplicated across the - // shortcode and block references). - $this->assertCount( 1, $form_ids ); - $this->assertEquals( (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $form_ids[0] ); - } - - /** - * Test that get_forms_in_posts() only returns post IDs whose content - * contains a Kit Form shortcode or block referencing a legacy form ID. - * - * Exercises the get_forms_in_posts() override on the Kit Legacy Forms - * importer, which narrows the parent class's broad SQL match. - * - * @since 3.3.5 - */ - public function testGetKitLegacyFormsInPosts() - { - // Initialize the class we want to test. - $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); - - // Confirm initialization didn't result in an error. - $this->assertNotInstanceOf(\WP_Error::class, $this->importer); - - // Create a post containing a non-legacy Kit Form shortcode. - $nonLegacyShortcodePostID = $this->factory->post->create( - [ - 'post_type' => 'page', - 'post_status' => 'publish', - 'post_title' => 'Kit Legacy Forms: Non-Legacy Shortcode Post', - 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', - ] - ); - - // Create a post containing a non-legacy Kit Form block. - $nonLegacyBlockPostID = $this->factory->post->create( - [ - 'post_type' => 'page', - 'post_status' => 'publish', - 'post_title' => 'Kit Legacy Forms: Non-Legacy Block Post', - 'post_content' => '', - ] - ); - - // Create a post containing a legacy Kit Form shortcode. - $legacyShortcodePostID = $this->factory->post->create( - [ - 'post_type' => 'page', - 'post_status' => 'publish', - 'post_title' => 'Kit Legacy Forms: Legacy Shortcode Post', - 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', - ] - ); - - // Create a post containing a legacy Kit Form block. - $legacyBlockPostID = $this->factory->post->create( - [ - 'post_type' => 'page', - 'post_status' => 'publish', - 'post_title' => 'Kit Legacy Forms: Legacy Block Post', - 'post_content' => '', - ] - ); - - // Fetch the list of post IDs that contain a legacy Kit form reference. - $post_ids = $this->importer->get_forms_in_posts(); - - // Cast post IDs to ints for comparison (wpdb returns strings). - $post_ids = array_map( 'intval', $post_ids ); - - // Assert only the legacy posts are returned, and the non-legacy posts are not. - $this->assertContains( $legacyShortcodePostID, $post_ids ); - $this->assertContains( $legacyBlockPostID, $post_ids ); - $this->assertNotContains( $nonLegacyShortcodePostID, $post_ids ); - $this->assertNotContains( $nonLegacyBlockPostID, $post_ids ); - } - /** * Test that the get_form_ids_from_content() method returns MC4WP form shortcode Form IDs * ignoring any other shortcodes. From 7ce0fd4a5c6b5752d7bc15904b84abc85cd0479b Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:21:12 +0800 Subject: [PATCH 18/19] Remove test resources --- tests/Integration/ImporterTest.php | 123 ----------------------------- 1 file changed, 123 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index c09ff8c03..0b3d27c46 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -38,126 +38,6 @@ public function setUp(): void // Activate Plugin. activate_plugins('convertkit/wp-convertkit.php'); - - // Define Forms as if the Forms resource class populated them from the API. - update_option( - 'convertkit_forms', - [ - 3059218 => [ - 'id' => 3059218, - 'name' => 'Auto Confirm Form', - 'created_at' => '2022-03-07T15:57:51Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/bfac9ed794/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/bfac9ed794', - 'archived' => false, - 'uid' => 'bfac9ed794', - ], - 2765143 => [ - 'id' => 2765143, - 'name' => 'Double Optin Form', - 'created_at' => '2021-11-11T15:31:28Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/a04b384fc6/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/a04b384fc6', - 'archived' => false, - 'uid' => 'a04b384fc6', - ], - 3003590 => [ - 'id' => 3003590, - 'name' => 'Third Party Integrations Form', - 'created_at' => '2022-02-17T15:05:31.000Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/71cbcc4042/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/71cbcc4042', - 'archived' => false, - 'uid' => '71cbcc4042', - ], - 2780977 => [ - 'id' => 2780977, - 'name' => 'Modal Form', - 'created_at' => '2021-11-17T04:22:06.000Z', - 'type' => 'embed', - 'format' => 'modal', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/397e876257/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/397e876257', - 'archived' => false, - 'uid' => '397e876257', - ], - 2780979 => [ - 'id' => 2780979, - 'name' => 'Slide In Form', - 'created_at' => '2021-11-17T04:22:24.000Z', - 'type' => 'embed', - 'format' => 'slide in', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/e0d65bed9d/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/e0d65bed9d', - 'archived' => false, - 'uid' => 'e0d65bed9d', - ], - 2765139 => [ - 'id' => 2765139, - 'name' => 'Page Form', - 'created_at' => '2021-11-11T15:30:40.000Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/85629c512d/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/85629c512d', - 'archived' => false, - 'uid' => '85629c512d', - ], - 470099 => [ - 'id' => 470099, - 'name' => 'Legacy Form', - 'created_at' => null, - 'type' => 'embed', - 'url' => 'https://app.kit.com/landing_pages/470099', - 'embed_js' => 'https://api.kit.com/api/v3/forms/470099.js?api_key=' . $_ENV['CONVERTKIT_API_KEY'], - 'embed_url' => 'https://api.kit.com/api/v3/forms/470099.html?api_key=' . $_ENV['CONVERTKIT_API_KEY'], - 'title' => 'Join the newsletter', - 'description' => '

Subscribe to get our latest content by email.

', - 'sign_up_button_text' => 'Subscribe', - 'success_message' => 'Success! Now check your email to confirm your subscription.', - 'archived' => false, - ], - 2780980 => [ - 'id' => 2780980, - 'name' => 'Sticky Bar Form', - 'created_at' => '2021-11-17T04:22:42.000Z', - 'type' => 'embed', - 'format' => 'sticky bar', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/9f5c601482/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/9f5c601482', - 'archived' => false, - 'uid' => '9f5c601482', - ], - 3437554 => [ - 'id' => 3437554, - 'name' => 'AAA Test', - 'created_at' => '2022-07-15T15:06:32.000Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/3bb15822a2/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/3bb15822a2', - 'archived' => false, - 'uid' => '3bb15822a2', - ], - 2765149 => [ - 'id' => 2765149, - 'name' => 'WooCommerce Product Form', - 'created_at' => '2021-11-11T15:32:54.000Z', - 'type' => 'embed', - 'format' => 'inline', - 'embed_js' => 'https://cheerful-architect-3237.kit.com/7e238f3920/index.js', - 'embed_url' => 'https://cheerful-architect-3237.kit.com/7e238f3920', - 'archived' => false, - 'uid' => '7e238f3920', - ], - ] - ); } /** @@ -173,9 +53,6 @@ public function tearDown(): void // Deactivate Plugin. deactivate_plugins('convertkit/wp-convertkit.php'); - // Delete the Forms resource. - delete_option( 'convertkit_forms' ); - parent::tearDown(); } From 25d9dae4697633b81312473a3d85285c151e5762 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 30 Jun 2026 14:21:44 +0800 Subject: [PATCH 19/19] Importers: Migrate Kit Legacy Forms: Tests --- ...ettingsToolsImporterKitLegacyFormsCest.php | 230 +++++++++++ tests/Integration/ImporterTest.php | 382 ++++++++++++++++++ 2 files changed, 612 insertions(+) create mode 100644 tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterKitLegacyFormsCest.php diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterKitLegacyFormsCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterKitLegacyFormsCest.php new file mode 100644 index 000000000..f8ed99e6d --- /dev/null +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterKitLegacyFormsCest.php @@ -0,0 +1,230 @@ + Kit > Tools > Import sections for Kit Legacy Forms. + * + * @since 3.3.5 + */ +class PluginSettingsToolsImporterKitLegacyFormsCest +{ + /** + * Run common actions before running the test functions in this class. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function _before(EndToEndTester $I) + { + // Activate Plugin. + $I->activateKitPlugin($I); + } + + /** + * Test that Kit Legacy Form Shortcodes are replaced with Kit Form Shortcodes when the Tools > Kit Legacy Forms: Migrate Configuration is configured. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function testKitLegacyFormsImportWithShortcodes(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Insert Kit Legacy Form Shortcode into Page. + $pageID = $this->_createPageWithKitLegacyFormShortcodes($I, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID']); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Select the Kit Form to replace the Kit Legacy Form. + $I->selectOption('_wp_convertkit_integration_convertkit_legacy_forms_settings[' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', $_ENV['CONVERTKIT_API_FORM_ID']); + + // Click the Migrate button. + $I->click('Migrate'); + + // Confirm success message displays. + $I->waitForElementVisible('.notice-success'); + $I->see('Kit Legacy Forms migrated successfully.'); + + // View the Page, to confirm Kit Form now displays. + $I->amOnPage('?p=' . $pageID); + $I->seeElementInDOM('form[data-sv-form]'); + } + + /** + * Test that Kit Legacy Form Blocks are replaced with Kit Blocks when the Tools > Kit Legacy Forms: Migrate Configuration is configured. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function testKitLegacyFormsImportWithBlocks(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Insert Kit Legacy Form Block into Page. + $pageID = $this->_createPageWithKitLegacyFormBlocks($I, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID']); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Select the Kit Form to replace the Kit Legacy Form. + $I->selectOption('_wp_convertkit_integration_convertkit_legacy_forms_settings[' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', $_ENV['CONVERTKIT_API_FORM_ID']); + + // Click the Migrate button. + $I->click('Migrate'); + + // Confirm success message displays. + $I->waitForElementVisible('.notice-success'); + $I->see('Kit Legacy Forms migrated successfully.'); + + // View the Page, to confirm Kit Form block now displays. + $I->amOnPage('?p=' . $pageID); + $I->seeElementInDOM('form[data-sv-form]'); + + // Confirm special characters have not been stripped. + $I->seeInSource('!@£$%^&*()_+~!@£$%^&*()_+\\'); + } + + /** + * Test that the Kit Legacy Forms: Migrate Configuration section is not displayed when no Kit Legacy Forms exist. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function testKitLegacyFormsImportWhenNoKitLegacyForms(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no Kit Legacy Forms: Migrate Configuration section is displayed. + $I->dontSeeElementInDOM('#import-kit-legacy-forms'); + } + + /** + * Test that the Kit Legacy Forms: Migrate Configuration section is not displayed when Kit Legacy Forms exist, + * but no Pages, Posts or Custom Posts contain Kit Legacy Form Shortcodes. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function testKitLegacyFormsImportWhenNoKitLegacyShortcodesOrBlocksInContent(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no Kit Legacy Forms: Migrate Configuration section is displayed, as there are no + // Kit Legacy Form Shortcodes or Blocks in the content. + $I->dontSeeElementInDOM('#import-kit-legacy-forms'); + } + + /** + * Test that the Kit Legacy Forms: Migrate Configuration section is not displayed when no Kit Forms exist. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function testKitLegacyFormsImportWhenNoKitForms(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPluginCredentialsNoData($I); + $I->setupKitPluginResourcesNoData($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no Kit Legacy Forms: Migrate Configuration section is displayed, as there are no + // Kit Forms exist. + $I->dontSeeElementInDOM('#import-kit-legacy-forms'); + } + + /** + * Create Pages with Kit Legacy Form Shortcodes. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + * @param int $kitLegacyFormID Kit Legacy Form ID. + * @return int + */ + private function _createPageWithKitLegacyFormShortcodes(EndToEndTester $I, $kitLegacyFormID) + { + return $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with Kit Form Shortcode using Legacy Form #' . $kitLegacyFormID, + 'post_content' => '[convertkit_form form="' . $kitLegacyFormID . '"]', + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ); + } + + /** + * Create Pages with Kit Legacy Form Blocks. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + * @param int $kitLegacyFormID Kit Legacy Form ID. + * @return int + */ + private function _createPageWithKitLegacyFormBlocks(EndToEndTester $I, $kitLegacyFormID) + { + return $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with Kit Form Block using Legacy Form #' . $kitLegacyFormID, + 'post_content' => '
Some content with characters !@£$%^&*()_+~!@£$%^&*()_+\\\
', + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ); + } + + /** + * Deactivate and reset Plugin(s) after each test, if the test passes. + * We don't use _after, as this would provide a screenshot of the Plugin + * deactivation and not the true test error. + * + * @since 3.3.5 + * + * @param EndToEndTester $I Tester. + */ + public function _passed(EndToEndTester $I) + { + $I->deactivateKitPlugin($I); + $I->resetKitPlugin($I); + } +} diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index 0b3d27c46..7ebbc08df 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -38,6 +38,126 @@ public function setUp(): void // Activate Plugin. activate_plugins('convertkit/wp-convertkit.php'); + + // Define Forms as if the Forms resource class populated them from the API. + update_option( + 'convertkit_forms', + [ + 3059218 => [ + 'id' => 3059218, + 'name' => 'Auto Confirm Form', + 'created_at' => '2022-03-07T15:57:51Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/bfac9ed794/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/bfac9ed794', + 'archived' => false, + 'uid' => 'bfac9ed794', + ], + 2765143 => [ + 'id' => 2765143, + 'name' => 'Double Optin Form', + 'created_at' => '2021-11-11T15:31:28Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/a04b384fc6/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/a04b384fc6', + 'archived' => false, + 'uid' => 'a04b384fc6', + ], + 3003590 => [ + 'id' => 3003590, + 'name' => 'Third Party Integrations Form', + 'created_at' => '2022-02-17T15:05:31.000Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/71cbcc4042/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/71cbcc4042', + 'archived' => false, + 'uid' => '71cbcc4042', + ], + 2780977 => [ + 'id' => 2780977, + 'name' => 'Modal Form', + 'created_at' => '2021-11-17T04:22:06.000Z', + 'type' => 'embed', + 'format' => 'modal', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/397e876257/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/397e876257', + 'archived' => false, + 'uid' => '397e876257', + ], + 2780979 => [ + 'id' => 2780979, + 'name' => 'Slide In Form', + 'created_at' => '2021-11-17T04:22:24.000Z', + 'type' => 'embed', + 'format' => 'slide in', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/e0d65bed9d/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/e0d65bed9d', + 'archived' => false, + 'uid' => 'e0d65bed9d', + ], + 2765139 => [ + 'id' => 2765139, + 'name' => 'Page Form', + 'created_at' => '2021-11-11T15:30:40.000Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/85629c512d/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/85629c512d', + 'archived' => false, + 'uid' => '85629c512d', + ], + 470099 => [ + 'id' => 470099, + 'name' => 'Legacy Form', + 'created_at' => null, + 'type' => 'embed', + 'url' => 'https://app.kit.com/landing_pages/470099', + 'embed_js' => 'https://api.kit.com/api/v3/forms/470099.js?api_key=' . $_ENV['CONVERTKIT_API_KEY'], + 'embed_url' => 'https://api.kit.com/api/v3/forms/470099.html?api_key=' . $_ENV['CONVERTKIT_API_KEY'], + 'title' => 'Join the newsletter', + 'description' => '

Subscribe to get our latest content by email.

', + 'sign_up_button_text' => 'Subscribe', + 'success_message' => 'Success! Now check your email to confirm your subscription.', + 'archived' => false, + ], + 2780980 => [ + 'id' => 2780980, + 'name' => 'Sticky Bar Form', + 'created_at' => '2021-11-17T04:22:42.000Z', + 'type' => 'embed', + 'format' => 'sticky bar', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/9f5c601482/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/9f5c601482', + 'archived' => false, + 'uid' => '9f5c601482', + ], + 3437554 => [ + 'id' => 3437554, + 'name' => 'AAA Test', + 'created_at' => '2022-07-15T15:06:32.000Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/3bb15822a2/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/3bb15822a2', + 'archived' => false, + 'uid' => '3bb15822a2', + ], + 2765149 => [ + 'id' => 2765149, + 'name' => 'WooCommerce Product Form', + 'created_at' => '2021-11-11T15:32:54.000Z', + 'type' => 'embed', + 'format' => 'inline', + 'embed_js' => 'https://cheerful-architect-3237.kit.com/7e238f3920/index.js', + 'embed_url' => 'https://cheerful-architect-3237.kit.com/7e238f3920', + 'archived' => false, + 'uid' => '7e238f3920', + ], + ] + ); } /** @@ -53,6 +173,9 @@ public function tearDown(): void // Deactivate Plugin. deactivate_plugins('convertkit/wp-convertkit.php'); + // Delete the Forms resource. + delete_option( 'convertkit_forms' ); + parent::tearDown(); } @@ -483,6 +606,265 @@ public function testCampaignMonitorReplaceShortcodesInContentIgnoringOtherShortc } } + /** + * Test that the get_form_ids_from_content() method returns Kit legacy form shortcode Form IDs + * ignoring any other shortcodes. + * + * @since 3.3.5 + */ + public function testGetKitLegacyFormsFormIDsFromContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the content to test. + $content = 'Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] Legacy Form ID: [convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"] non-Legacy Form: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Different shortcode: [aweber formid="12"]'; + + // Extract form IDs from content. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert the legacy form shortcode was detected, and the non-legacy form shortcode was ignored. + $this->assertEquals( 1, count( $form_ids ) ); + $this->assertEquals( $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $form_ids[0] ); + } + + /** + * Test that the replace_shortcodes_in_content() method replaces the Kit legacy form shortcode with the Kit form shortcode. + * + * @since 3.3.5 + */ + public function testKitLegacyFormsReplaceShortcodesInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the shortcodes to test. + $shortcodes = [ + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + '[convertkit_form form=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', + '[convertkit_form id="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + '[convertkit_form id=' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . ']', + ]; + + // Test each shortcode is replaced with the Kit form shortcode. + foreach ( $shortcodes as $shortcode ) { + $this->assertEquals( + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + $this->importer->replace_shortcodes_in_content( $shortcode, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + + // Prepend and append some content. + $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode.'; + $this->assertEquals( + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode.', + $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + + // Prepend and append some content and duplicate the shortcode. + $content = 'Some content before the shortcode: ' . $shortcode . ' Some content after the shortcode: ' . $shortcode; + $this->assertEquals( + 'Some content before the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"] Some content after the shortcode: [convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + $this->importer->replace_shortcodes_in_content( $content, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + } + + /** + * Test that the replace_shortcodes_in_content() method ignores non-Kit legacy form shortcodes. + * + * @since 3.3.5 + */ + public function testKitLegacyFormsReplaceShortcodesInContentIgnoringOtherShortcodes() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the shortcodes to test. + $shortcodes = [ + '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[convertkit_form id="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + '[a_random_shortcode]', + ]; + + // Test each shortcode is ignored. + foreach ( $shortcodes as $shortcode ) { + $this->assertEquals( + $shortcode, + $this->importer->replace_shortcodes_in_content( $shortcode, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + } + + /** + * Test that the replace_blocks_in_post() method replaces the third party form block with the Kit form block, + * and special characters are not stripped when the Post is saved. + * + * @since 3.3.5 + */ + public function testKitLegacyFormsReplaceBlocksInPost() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Create a Post with a MC4WP form block and HTML block, as if the user already created this post. + $postID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Replace Blocks in Post', + 'post_content' => str_replace( '\\', '\\\\', '' . $this->html_block ), + ] + ); + + // Replace the blocks in the post. + $this->importer->replace_blocks_in_post( $postID, $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ); + + // Test the block is replaced with the Kit form block, and special characters are not stripped. + $this->assertEquals( + '' . $this->html_block, + get_post_field( 'post_content', $postID ) + ); + } + + /** + * Test that the replace_blocks_in_content() method replaces the third party form block with the Kit form block, + * and special characters are not stripped. + * + * @since 3.3.5 + */ + public function testKitLegacyFormsReplaceBlocksInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the blocks to test. + $content = '' . $this->html_block; + + // Test the block is replaced with the Kit form block. + $this->assertEquals( + '' . $this->html_block, + $this->importer->replace_blocks_in_content( parse_blocks( $content ), $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + + /** + * Test that get_form_ids_from_content() returns Kit legacy form block Form IDs + * (in addition to shortcode Form IDs), ignoring any non-legacy Kit form blocks. + * + * Exercises the block-extraction extension to the get_form_ids_from_content() + * override on the Kit Legacy Forms importer. + * + * @since 3.3.5 + */ + public function testGetKitLegacyFormsFormIDsFromContentForBlocks() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define content containing both legacy and non-legacy Kit form blocks, + // plus a legacy shortcode, to confirm both block and shortcode IDs are + // surfaced when they reference a legacy form. + $content = '' + . '' + . '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]'; + + // Extract form IDs from content. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert only the legacy form ID is returned (deduplicated across the + // shortcode and block references). + $this->assertCount( 1, $form_ids ); + $this->assertEquals( (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'], $form_ids[0] ); + } + + /** + * Test that get_forms_in_posts() only returns post IDs whose content + * contains a Kit Form shortcode or block referencing a legacy form ID. + * + * Exercises the get_forms_in_posts() override on the Kit Legacy Forms + * importer, which narrows the parent class's broad SQL match. + * + * @since 3.3.5 + */ + public function testGetKitLegacyFormsInPosts() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_ConvertKit_Legacy_Forms(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Create a post containing a non-legacy Kit Form shortcode. + $nonLegacyShortcodePostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Non-Legacy Shortcode Post', + 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_FORM_ID'] . '"]', + ] + ); + + // Create a post containing a non-legacy Kit Form block. + $nonLegacyBlockPostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Non-Legacy Block Post', + 'post_content' => '', + ] + ); + + // Create a post containing a legacy Kit Form shortcode. + $legacyShortcodePostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Legacy Shortcode Post', + 'post_content' => '[convertkit_form form="' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'] . '"]', + ] + ); + + // Create a post containing a legacy Kit Form block. + $legacyBlockPostID = $this->factory->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Kit Legacy Forms: Legacy Block Post', + 'post_content' => '', + ] + ); + + // Fetch the list of post IDs that contain a legacy Kit form reference. + $post_ids = $this->importer->get_forms_in_posts(); + + // Cast post IDs to ints for comparison (wpdb returns strings). + $post_ids = array_map( 'intval', $post_ids ); + + // Assert only the legacy posts are returned, and the non-legacy posts are not. + $this->assertContains( $legacyShortcodePostID, $post_ids ); + $this->assertContains( $legacyBlockPostID, $post_ids ); + $this->assertNotContains( $nonLegacyShortcodePostID, $post_ids ); + $this->assertNotContains( $nonLegacyBlockPostID, $post_ids ); + } + /** * Test that the get_form_ids_from_content() method returns MC4WP form shortcode Form IDs * ignoring any other shortcodes.