From 44dbf23fc7ad52b792dccaad0bc6bf96191b55ef Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 16:59:22 +0800 Subject: [PATCH 01/11] Importer: Aweber --- ...class-convertkit-admin-importer-aweber.php | 112 ++++++++++++++++++ .../class-convertkit-admin-section-tools.php | 40 ++++++- views/backend/settings/tools.php | 55 +++++++++ wp-convertkit.php | 1 + 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 admin/importers/class-convertkit-admin-importer-aweber.php diff --git a/admin/importers/class-convertkit-admin-importer-aweber.php b/admin/importers/class-convertkit-admin-importer-aweber.php new file mode 100644 index 000000000..c07f64c4f --- /dev/null +++ b/admin/importers/class-convertkit-admin-importer-aweber.php @@ -0,0 +1,112 @@ +get_col( + $wpdb->prepare( + " + SELECT ID + FROM {$wpdb->posts} + WHERE post_status = %s + AND post_content LIKE %s + ", + 'publish', + '%[' . $this->shortcode_name . '%' + ) + ); + + return $results ? $results : array(); + + } + + /** + * Returns an array of AWeber form IDs and titles. + * + * @since 3.1.5 + * + * @return array + */ + public function get_forms() { + + global $aweber_webform_plugin; + + // Bail if the AWeber Plugin is not active, as the only way to fetch forms is via their API. + // There is no cache of form data. + if ( is_null( $aweber_webform_plugin ) ) { + return array(); + } + + // Get OAuth 1 and 2 tokens. + $plugin_admin_options = get_option( $aweber_webform_plugin->adminOptionsName ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $oauth2_tokens_options = get_option( $aweber_webform_plugin->oauth2TokensOptions ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + + // Fetch Aweber account, using OAuth1 or OAuth2. + $response = $aweber_webform_plugin->getAWeberAccount( $plugin_admin_options, $oauth2_tokens_options ); + + // If no account is returned, return an empty array. + if ( ! isset( $response['account'] ) ) { + return array(); + } + + // Get account, which contains forms and form split tests. + $account = $response['account']; + $web_forms = $account->getWebForms(); + $web_form_split_tests = $account->getWebFormSplitTests(); + + // Build array of forms. + $forms = array(); + foreach ( $web_forms as $form ) { + $forms[ $form->id ] = sprintf( '%s: %s', __( 'Sign Up Form', 'convertkit' ), $form->name ); + } + foreach ( $web_form_split_tests as $form ) { + $forms[ $form->id ] = sprintf( '%s: %s', __( 'Split Tests', 'convertkit' ), $form->name ); + } + + // Return forms. + return $forms; + + } + +} diff --git a/admin/section/class-convertkit-admin-section-tools.php b/admin/section/class-convertkit-admin-section-tools.php index 87129de98..fe62b0091 100644 --- a/admin/section/class-convertkit-admin-section-tools.php +++ b/admin/section/class-convertkit-admin-section-tools.php @@ -57,6 +57,7 @@ public function register_notices( $notices ) { 'import_configuration_invalid_file_type' => __( 'The uploaded configuration file isn\'t valid.', 'convertkit' ), 'import_configuration_empty' => __( 'The uploaded configuration file contains no settings.', 'convertkit' ), 'import_configuration_success' => __( 'Configuration imported successfully.', 'convertkit' ), + 'migrate_aweber_configuration_success' => __( 'AWeber forms migrated successfully.', 'convertkit' ), 'migrate_mc4wp_configuration_success' => __( 'MC4WP forms migrated successfully.', 'convertkit' ), ) ); @@ -76,6 +77,7 @@ private function maybe_perform_actions() { $this->maybe_download_system_info(); $this->maybe_export_configuration(); $this->maybe_import_configuration(); + $this->maybe_migrate_aweber_configuration(); $this->maybe_migrate_mc4wp_configuration(); } @@ -316,6 +318,41 @@ private function maybe_import_configuration() { } + /** + * Replaces AWeber Form Shortcodes with Kit Form Shortcodes, if the user submitted the + * AWeber Migrate Configuration section. + * + * @since 3.1.5 + */ + private function maybe_migrate_aweber_configuration() { + + // Bail if nonce verification fails. + if ( ! isset( $_REQUEST['_convertkit_settings_tools_nonce'] ) ) { + return; + } + + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_convertkit_settings_tools_nonce'] ), 'convertkit-settings-tools' ) ) { + return; + } + + // Bail if no AWeber Form IDs were submitted. + if ( ! isset( $_REQUEST['_wp_convertkit_integration_aweber_settings'] ) ) { + return; + } + + // Initialise the importer. + $aweber = new ConvertKit_Admin_Importer_AWeber(); + + // Iterate through the AWeber Form IDs and replace the shortcodes with the Kit Form Shortcodes. + foreach ( array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['_wp_convertkit_integration_aweber_settings'] ) ) as $aweber_form_id => $kit_form_id ) { + $aweber->replace_shortcodes_in_posts( (int) $aweber_form_id, (int) $kit_form_id ); + } + + // Redirect to Tools screen. + $this->redirect_with_success_notice( 'migrate_aweber_configuration_success' ); + + } + /** * Replaces MC4WP Form Shortcodes with Kit Form Shortcodes, if the user submitted the * MC4WP Migrate Configuration section. @@ -373,7 +410,8 @@ public function render() { $forms = new ConvertKit_Resource_Forms(); // Get Importers. - $mc4wp = new ConvertKit_Admin_Importer_MC4WP(); + $aweber = new ConvertKit_Admin_Importer_AWeber(); + $mc4wp = new ConvertKit_Admin_Importer_MC4WP(); // Output view. require_once CONVERTKIT_PLUGIN_PATH . '/views/backend/settings/tools.php'; diff --git a/views/backend/settings/tools.php b/views/backend/settings/tools.php index 907c97115..597fb4d72 100644 --- a/views/backend/settings/tools.php +++ b/views/backend/settings/tools.php @@ -102,6 +102,61 @@ has_forms_in_posts() && $aweber->has_forms() && $forms->exist() ) { + ?> +
+

+ +

+
+

+ + + + + + + + + + get_forms() as $aweber_form_id => $aweber_form_title ) { + ?> + + + + + + +
+ +
+ +

+ +

+
+ has_forms_in_posts() && $mc4wp->has_forms() && $forms->exist() ) { ?> diff --git a/wp-convertkit.php b/wp-convertkit.php index c7666a76e..ff39d9d3b 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -117,6 +117,7 @@ require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-setup-wizard.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-wp-list-table.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer.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-mc4wp.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-base.php'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-broadcasts.php'; From 18a7dd4b9a02364d248b0cfc32c790f5c4c9c807 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 17:06:24 +0800 Subject: [PATCH 02/11] Coding standards --- .../class-convertkit-admin-importer-aweber.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer-aweber.php b/admin/importers/class-convertkit-admin-importer-aweber.php index c07f64c4f..eba5fc256 100644 --- a/admin/importers/class-convertkit-admin-importer-aweber.php +++ b/admin/importers/class-convertkit-admin-importer-aweber.php @@ -78,12 +78,12 @@ public function get_forms() { return array(); } - // Get OAuth 1 and 2 tokens. - $plugin_admin_options = get_option( $aweber_webform_plugin->adminOptionsName ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $oauth2_tokens_options = get_option( $aweber_webform_plugin->oauth2TokensOptions ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - // Fetch Aweber account, using OAuth1 or OAuth2. - $response = $aweber_webform_plugin->getAWeberAccount( $plugin_admin_options, $oauth2_tokens_options ); + // This is how the AWeber Plugin fetches the account data. + $response = $aweber_webform_plugin->getAWeberAccount( + get_option( $aweber_webform_plugin->adminOptionsName ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + get_option( $aweber_webform_plugin->oauth2TokensOptions ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + ); // If no account is returned, return an empty array. if ( ! isset( $response['account'] ) ) { From b197209b65e782f9cb86b095133015d9ccc68546 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 17:14:34 +0800 Subject: [PATCH 03/11] Integration Tests --- tests/Integration/ImporterTest.php | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index 25a3f6dc8..a851b1e37 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -47,6 +47,86 @@ public function tearDown(): void parent::tearDown(); } + /** + * Test that the replace_shortcodes_in_content() method replaces the third party form shortcode with the Kit form shortcode. + * + * @since 3.1.5 + */ + public function testAWeberReplaceShortcodesInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_AWeber(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the shortcodes to test. + $shortcodes = [ + '[aweber formid=10]', + '[aweber formid="10"]', + '[aweber formid=10 listid=11]', + '[aweber formid="10" listid="11"]', + '[aweber formid=10 listid=11 formtype=webform]', + '[aweber formid="10" listid="11" formtype="webform"]', + '[aweber listid=11 formid=10]', + '[aweber listid="11" formid="10"]', + '[aweber listid=11 formid=10 formtype=webform]', + '[aweber listid="11" formid="10" formtype="webform"]', + '[aweber formtype=webform listid=11 formid=10]', + '[aweber formtype="webform" listid="11" formid="10"]', + ]; + + // Test each shortcode is replaced with the Kit form shortcode. + foreach ( $shortcodes as $shortcode ) { + $this->assertEquals( + '[convertkit_form id="' . $_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.', + $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'] . '"]', + $this->importer->replace_shortcodes_in_content( $content, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + } + + /** + * Test that the replace_shortcodes_in_content() method ignores non-AWeber shortcodes. + * + * @since 3.1.5 + */ + public function testAWeberReplaceShortcodesInContentIgnoringOtherShortcodes() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_AWeber(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the shortcodes to test. + $shortcodes = [ + '[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, 10, $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + } + /** * Test that the replace_shortcodes_in_content() method replaces the third party form shortcode with the Kit form shortcode. * From b429e8a3d9cd032f24c360f701847476eac081a4 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 17:22:51 +0800 Subject: [PATCH 04/11] Tests: Import: AWeber --- .github/workflows/tests.yml | 2 +- .../PluginSettingsToolsImporterAweberCest.php | 197 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c59a882fb..ea7d1b70e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: DB_PASS: root DB_HOST: localhost WORDPRESS_DB_SQL_DUMP_FILE: tests/Support/Data/dump.sql # Used to populate the test site database for WordPress 6.2.8 - INSTALL_PLUGINS: "admin-menu-editor autoptimize beaver-builder-lite-version block-visibility contact-form-7 classic-editor custom-post-type-ui debloat elementor forminator jetpack-boost mailchimp-for-wp rocket-lazy-load woocommerce wordpress-seo wpforms-lite litespeed-cache wp-crontrol wp-super-cache w3-total-cache wp-fastest-cache wp-optimize sg-cachepress" # Don't include this repository's Plugin here. + INSTALL_PLUGINS: "admin-menu-editor autoptimize aweber-web-form-widget beaver-builder-lite-version block-visibility contact-form-7 classic-editor custom-post-type-ui debloat elementor forminator jetpack-boost mailchimp-for-wp rocket-lazy-load woocommerce wordpress-seo wpforms-lite litespeed-cache wp-crontrol wp-super-cache w3-total-cache wp-fastest-cache wp-optimize sg-cachepress" # Don't include this repository's Plugin here. INSTALL_PLUGINS_URLS: "https://downloads.wordpress.org/plugin/convertkit-for-woocommerce.1.6.4.zip http://cktestplugins.wpengine.com/wp-content/uploads/2024/01/convertkit-action-filter-tests.zip http://cktestplugins.wpengine.com/wp-content/uploads/2024/11/disable-doing-it-wrong-notices.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode-js_composer.7.8.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode-core.zip" # URLs to specific third party Plugins INSTALL_THEMES_URLS: "http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/04/Divi.zip http://cktestplugins.wpengine.com/wp-content/uploads/2026/01/impeka.zip" CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }} # ConvertKit API Key, stored in the repository's Settings > Secrets diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php new file mode 100644 index 000000000..f8f220d82 --- /dev/null +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php @@ -0,0 +1,197 @@ + Kit > Tools > Import sections for AWeber. + * + * @since 3.1.5 + */ +class PluginSettingsToolsImporterAweberCest +{ + /** + * Run common actions before running the test functions in this class. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function _before(EndToEndTester $I) + { + // Activate Plugins. + $I->activateKitPlugin($I); + $I->activateThirdPartyPlugin($I, 'aweber-web-form-widget'); + } + + /** + * Test that AWeber Forms are replaced with Kit Forms when the Tools > AWeber: Migrate Configuration is configured. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAWeberImport(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Create Aweber Forms. + $aweberFormIDs = $this->_createAWeberForms($I); + + // Insert AWeber Form Shortcodes into Pages. + $pageIDs = $this->_createPagesWithAWeberFormShortcodes($I, $aweberFormIDs); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Select the Kit Forms to replace the AWeber Forms. + foreach ($aweberFormIDs as $aweberFormID) { + $I->selectOption('_wp_convertkit_integration_aweber_settings[' . $aweberFormID . ']', $_ENV['CONVERTKIT_API_FORM_ID']); + } + + // Click the Migrate button. + $I->click('Migrate'); + + // Confirm success message displays. + $I->waitForElementVisible('.notice-success'); + $I->see('AWeber forms migrated successfully.'); + + // View the Pages, to confirm Kit Forms now display. + foreach ($pageIDs as $pageID) { + $I->amOnPage('?p=' . $pageID); + $I->seeElementInDOM('form[data-sv-form]'); + } + } + + /** + * Test that the AWeber: Migrate Configuration section is not displayed when no AWeber Forms exist. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAWeberImportWhenNoAWeberForms(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no AWeber: Migrate Configuration section is displayed. + $I->dontSeeElementInDOM('#import-aweber'); + } + + /** + * Test that the AWeber: Migrate Configuration section is not displayed when AWeber Forms exist, + * but no Pages, Posts or Custom Posts contain AWeber Form Shortcodes. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAWeberImportWhenNoAWeberShortcodesInContent(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Create AWeber Forms. + $aweberFormIDs = $this->_createAWeberForms($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no AWeber: Migrate Configuration section is displayed, as there are no + // AWeber Form Shortcodes in the content. + $I->dontSeeElementInDOM('#import-aweber'); + } + + /** + * Test that the AWeber: Migrate Configuration section is not displayed when no Kit Forms exist. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function testAWeberImportWhenNoKitForms(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPluginCredentialsNoData($I); + $I->setupKitPluginResourcesNoData($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no AWeber: Migrate Configuration section is displayed, as there are no + // AWeber Form Shortcodes in the content. + $I->dontSeeElementInDOM('#import-aweber'); + } + + /** + * Create AWeber Forms. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + * @return array + */ + private function _createAWeberForms(EndToEndTester $I) + { + // @TODO. + } + + /** + * Create Pages with AWeber Form Shortcodes. + * + * @since 3.1.5 + * + * @param EndToEndTester $I Tester. + * @param array $aweberFormIDs AWeber Form IDs. + * @return array + */ + private function _createPagesWithAWeberFormShortcodes(EndToEndTester $I, $aweberFormIDs) + { + $pageIDs = array(); + + foreach ($aweberFormIDs as $aweberFormID) { + $pageIDs[] = $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with AWeber Form #' . $aweberFormID, + 'post_content' => '[aweber formid="' . $aweberFormID . '"]', + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ); + } + + return $pageIDs; + } + + /** + * 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.1.5 + * + * @param EndToEndTester $I Tester. + */ + public function _passed(EndToEndTester $I) + { + $I->deactivateThirdPartyPlugin($I, 'aweber-web-form-widget'); + $I->deactivateKitPlugin($I); + $I->resetKitPlugin($I); + } +} From 9e8efd94e478964d66325e7ff59a6c4e1e5d8f18 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 18:07:48 +0800 Subject: [PATCH 05/11] =?UTF-8?q?Add=20fallback=20method=20if=20AWeber=20P?= =?UTF-8?q?lugin=20isn=E2=80=99t=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...class-convertkit-admin-importer-aweber.php | 61 ++++++------- .../class-convertkit-admin-importer-mc4wp.php | 29 ------ .../class-convertkit-admin-importer.php | 88 ++++++++++++++++++- 3 files changed, 110 insertions(+), 68 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer-aweber.php b/admin/importers/class-convertkit-admin-importer-aweber.php index eba5fc256..fd982f283 100644 --- a/admin/importers/class-convertkit-admin-importer-aweber.php +++ b/admin/importers/class-convertkit-admin-importer-aweber.php @@ -32,35 +32,6 @@ class ConvertKit_Admin_Importer_AWeber extends ConvertKit_Admin_Importer { */ public $shortcode_id_attribute = 'formid'; - /** - * Returns an array of post IDs that contain the AWeber form shortcode. - * - * @since 3.1.5 - * - * @return array - */ - public function get_forms_in_posts() { - - global $wpdb; - - // Search post_content for [aweber] shortcode and return array of post IDs. - $results = $wpdb->get_col( - $wpdb->prepare( - " - SELECT ID - FROM {$wpdb->posts} - WHERE post_status = %s - AND post_content LIKE %s - ", - 'publish', - '%[' . $this->shortcode_name . '%' - ) - ); - - return $results ? $results : array(); - - } - /** * Returns an array of AWeber form IDs and titles. * @@ -72,22 +43,21 @@ public function get_forms() { global $aweber_webform_plugin; - // Bail if the AWeber Plugin is not active, as the only way to fetch forms is via their API. - // There is no cache of form data. + // If the AWeber Plugin is not active, fall back to showing the AWeber Form IDs found in the posts, if any. if ( is_null( $aweber_webform_plugin ) ) { - return array(); + return $this->get_forms_detected_in_posts(); } - // Fetch Aweber account, using OAuth1 or OAuth2. - // This is how the AWeber Plugin fetches the account data. + // Fetch AWeber account, using OAuth1 or OAuth2. + // This is how the AWeber Plugin fetches the account data, as nothing is cached in their Plugin or the database. $response = $aweber_webform_plugin->getAWeberAccount( get_option( $aweber_webform_plugin->adminOptionsName ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase get_option( $aweber_webform_plugin->oauth2TokensOptions ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ); - // If no account is returned, return an empty array. + // If no account is returned, fall back to showing the AWeber Form IDs found in the posts, if any. if ( ! isset( $response['account'] ) ) { - return array(); + return $this->get_forms_detected_in_posts(); } // Get account, which contains forms and form split tests. @@ -109,4 +79,23 @@ public function get_forms() { } + /** + * Returns an array of AWeber form IDs and titles found in the posts. + * + * @since 3.1.5 + * + * @return array + */ + private function get_forms_detected_in_posts() { + + $forms = array(); + + foreach ( $this->get_form_ids_in_posts() as $form_id ) { + $forms[ $form_id ] = sprintf( 'AWeber Form ID #%s', $form_id ); + } + + return $forms; + + } + } diff --git a/admin/importers/class-convertkit-admin-importer-mc4wp.php b/admin/importers/class-convertkit-admin-importer-mc4wp.php index ff18b17af..84ed6e071 100644 --- a/admin/importers/class-convertkit-admin-importer-mc4wp.php +++ b/admin/importers/class-convertkit-admin-importer-mc4wp.php @@ -32,35 +32,6 @@ class ConvertKit_Admin_Importer_MC4WP extends ConvertKit_Admin_Importer { */ public $shortcode_id_attribute = 'id'; - /** - * Returns an array of post IDs that contain the MC4WP form shortcode. - * - * @since 3.1.0 - * - * @return array - */ - public function get_forms_in_posts() { - - global $wpdb; - - // Search post_content for [mc4wp_form] shortcode and return array of post IDs. - $results = $wpdb->get_col( - $wpdb->prepare( - " - SELECT ID - FROM {$wpdb->posts} - WHERE post_status = %s - AND post_content LIKE %s - ", - 'publish', - '%[' . $this->shortcode_name . '%' - ) - ); - - return $results ? $results : array(); - - } - /** * Returns an array of MC4WP form IDs and titles. * diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index 74632cb57..be55e6819 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -42,13 +42,33 @@ abstract class ConvertKit_Admin_Importer { abstract public function get_forms(); /** - * Returns an array of post IDs that contain the third party form shortcode. + * Returns an array of post IDs that contain the AWeber form shortcode. * - * @since 3.1.0 + * @since 3.1.5 * * @return array */ - abstract public function get_forms_in_posts(); + public function get_forms_in_posts() { + + global $wpdb; + + // Search post_content for [aweber] shortcode and return array of post IDs. + $results = $wpdb->get_col( + $wpdb->prepare( + " + SELECT ID + FROM {$wpdb->posts} + WHERE post_status = %s + AND post_content LIKE %s + ", + 'publish', + '%[' . $this->shortcode_name . '%' + ) + ); + + return $results ? $results : array(); + + } /** * Returns whether any third party forms exist. @@ -145,4 +165,66 @@ public function replace_shortcodes_in_content( $content, $third_party_form_id, $ } + /** + * Returns an array of all unique form IDs from the posts that contain the third party form shortcode. + * + * @since 3.1.5 + * + * @return array + */ + public function get_form_ids_in_posts() { + + // Get Post IDs that contain the AWeber form shortcode. + $post_ids = $this->get_forms_in_posts(); + + // If no post IDs are found, return an empty array. + if ( ! count( $post_ids ) ) { + return array(); + } + + // Iterate through Posts, extracting the Form IDs from the AWeber form shortcodes. + $form_ids = array(); + foreach ( $post_ids as $post_id ) { + $content_form_ids = $this->get_form_ids_from_content( get_post_field( 'post_content', $post_id ) ); + $form_ids = array_merge( $form_ids, $content_form_ids ); + } + + $form_ids = array_values( array_unique( $form_ids ) ); + + return $form_ids; + + } + + /** + * Returns an array of form IDs within the shortcode for the third party Form plugin. + * + * @since 3.1.5 + * + * @param string $content Content containing third party Form Shortcodes. + * @return array + */ + public function get_form_ids_from_content( $content ) { + + $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 quoted or unquoted value. + . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. + + preg_match_all( $pattern, $content, $matches ); + + // Extract form IDs: They could be in either $matches[1] (quoted) or $matches[2] (unquoted). + $form_ids = array_filter( + array_merge( + isset( $matches[1] ) ? $matches[1] : array(), + isset( $matches[2] ) ? $matches[2] : array() + ) + ); + + return $form_ids; + + } + } From 2063985111147d0e10a8ea51332c3c68ae33f3e0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 18:14:42 +0800 Subject: [PATCH 06/11] Added Tests --- .github/workflows/tests.yml | 2 +- tests/EndToEnd.suite.yml | 1 - .../PluginSettingsToolsImporterAweberCest.php | 5 +- tests/Integration/ImporterTest.php | 50 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea7d1b70e..c59a882fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: DB_PASS: root DB_HOST: localhost WORDPRESS_DB_SQL_DUMP_FILE: tests/Support/Data/dump.sql # Used to populate the test site database for WordPress 6.2.8 - INSTALL_PLUGINS: "admin-menu-editor autoptimize aweber-web-form-widget beaver-builder-lite-version block-visibility contact-form-7 classic-editor custom-post-type-ui debloat elementor forminator jetpack-boost mailchimp-for-wp rocket-lazy-load woocommerce wordpress-seo wpforms-lite litespeed-cache wp-crontrol wp-super-cache w3-total-cache wp-fastest-cache wp-optimize sg-cachepress" # Don't include this repository's Plugin here. + INSTALL_PLUGINS: "admin-menu-editor autoptimize beaver-builder-lite-version block-visibility contact-form-7 classic-editor custom-post-type-ui debloat elementor forminator jetpack-boost mailchimp-for-wp rocket-lazy-load woocommerce wordpress-seo wpforms-lite litespeed-cache wp-crontrol wp-super-cache w3-total-cache wp-fastest-cache wp-optimize sg-cachepress" # Don't include this repository's Plugin here. INSTALL_PLUGINS_URLS: "https://downloads.wordpress.org/plugin/convertkit-for-woocommerce.1.6.4.zip http://cktestplugins.wpengine.com/wp-content/uploads/2024/01/convertkit-action-filter-tests.zip http://cktestplugins.wpengine.com/wp-content/uploads/2024/11/disable-doing-it-wrong-notices.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode-js_composer.7.8.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode-core.zip" # URLs to specific third party Plugins INSTALL_THEMES_URLS: "http://cktestplugins.wpengine.com/wp-content/uploads/2025/03/uncode.zip http://cktestplugins.wpengine.com/wp-content/uploads/2025/04/Divi.zip http://cktestplugins.wpengine.com/wp-content/uploads/2026/01/impeka.zip" CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }} # ConvertKit API Key, stored in the repository's Settings > Secrets diff --git a/tests/EndToEnd.suite.yml b/tests/EndToEnd.suite.yml index 7cf15b9ed..d8ef7596f 100644 --- a/tests/EndToEnd.suite.yml +++ b/tests/EndToEnd.suite.yml @@ -55,7 +55,6 @@ modules: capabilities: "goog:chromeOptions": args: - - "--headless" - "--disable-gpu" - "--disable-dev-shm-usage" - "--disable-software-rasterizer" diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php index f8f220d82..575e3bf93 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php @@ -142,7 +142,10 @@ public function testAWeberImportWhenNoKitForms(EndToEndTester $I) */ private function _createAWeberForms(EndToEndTester $I) { - // @TODO. + // AWeber doesn't cache Forms or store them in the database, so we mock the data that would be returned from their API. + return [ + '10', '11' + ]; } /** diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index a851b1e37..bb8abc890 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -47,6 +47,31 @@ public function tearDown(): void parent::tearDown(); } + /** + * Test that the get_form_ids_from_content() method returns third party form shortcode Form IDs. + * + * @since 3.1.5 + */ + public function testGetAWeberFormIDsFromContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_AWeber(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the content to test. + $content = '[aweber formid="10"] some content [aweber formid="11"] some other content'; + + // Extract form IDs from content. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert the correct number of form IDs are returned. + $this->assertEquals( 2, count( $form_ids ) ); + $this->assertEquals( 10, $form_ids[0] ); + $this->assertEquals( 11, $form_ids[1] ); + } + /** * Test that the replace_shortcodes_in_content() method replaces the third party form shortcode with the Kit form shortcode. * @@ -127,6 +152,31 @@ public function testAWeberReplaceShortcodesInContentIgnoringOtherShortcodes() } } + /** + * Test that the get_form_ids_from_content() method returns third party form shortcode Form IDs. + * + * @since 3.1.5 + */ + public function testGetMC4WPFormIDsFromContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_MC4WP(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the content to test. + $content = '[mc4wp_form id="10"] some content [mc4wp_form id="11"] some other content'; + + // Extract form IDs from content. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert the correct number of form IDs are returned. + $this->assertEquals( 2, count( $form_ids ) ); + $this->assertEquals( 10, $form_ids[0] ); + $this->assertEquals( 11, $form_ids[1] ); + } + /** * Test that the replace_shortcodes_in_content() method replaces the third party form shortcode with the Kit form shortcode. * From 06a3bbfab1adf9066fef35a48cd40b6e71a887de Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 18:15:53 +0800 Subject: [PATCH 07/11] Reinstate headless mode --- tests/EndToEnd.suite.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/EndToEnd.suite.yml b/tests/EndToEnd.suite.yml index d8ef7596f..7cf15b9ed 100644 --- a/tests/EndToEnd.suite.yml +++ b/tests/EndToEnd.suite.yml @@ -55,6 +55,7 @@ modules: capabilities: "goog:chromeOptions": args: + - "--headless" - "--disable-gpu" - "--disable-dev-shm-usage" - "--disable-software-rasterizer" From 81eedd7dc069222c66d0472b95bbf3b4a77f4a7e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 20:13:10 +0800 Subject: [PATCH 08/11] Tests: Removed third party plugin activation/deactivation --- .../plugin-screens/PluginSettingsToolsImporterAweberCest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php index 575e3bf93..e385e9cb4 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php @@ -22,7 +22,6 @@ public function _before(EndToEndTester $I) { // Activate Plugins. $I->activateKitPlugin($I); - $I->activateThirdPartyPlugin($I, 'aweber-web-form-widget'); } /** @@ -193,7 +192,6 @@ private function _createPagesWithAWeberFormShortcodes(EndToEndTester $I, $aweber */ public function _passed(EndToEndTester $I) { - $I->deactivateThirdPartyPlugin($I, 'aweber-web-form-widget'); $I->deactivateKitPlugin($I); $I->resetKitPlugin($I); } From d344c6687492619bdee7c114caeec95a9631ce4f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 13 Jan 2026 20:15:41 +0800 Subject: [PATCH 09/11] Coding standards --- .../PluginSettingsToolsImporterAweberCest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php index e385e9cb4..27bdfca60 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterAweberCest.php @@ -136,14 +136,14 @@ public function testAWeberImportWhenNoKitForms(EndToEndTester $I) * * @since 3.1.5 * - * @param EndToEndTester $I Tester. * @return array */ - private function _createAWeberForms(EndToEndTester $I) + private function _createAWeberForms() { // AWeber doesn't cache Forms or store them in the database, so we mock the data that would be returned from their API. return [ - '10', '11' + '10', + '11', ]; } From 1c1352a5fadb89bc7169e33a400f8d18eb6d1376 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 15 Jan 2026 10:54:31 +0800 Subject: [PATCH 10/11] Importer: Remove AWeber references This is now a generic set of functions for third party form shortcodes. --- .../class-convertkit-admin-importer.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index be55e6819..c3f437f20 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -42,7 +42,7 @@ abstract class ConvertKit_Admin_Importer { abstract public function get_forms(); /** - * Returns an array of post IDs that contain the AWeber form shortcode. + * Returns an array of post IDs that contain the third partyform shortcode. * * @since 3.1.5 * @@ -52,7 +52,7 @@ public function get_forms_in_posts() { global $wpdb; - // Search post_content for [aweber] shortcode and return array of post IDs. + // Search post_content for the third party form shortcode and return array of post IDs. $results = $wpdb->get_col( $wpdb->prepare( " @@ -174,7 +174,7 @@ public function replace_shortcodes_in_content( $content, $third_party_form_id, $ */ public function get_form_ids_in_posts() { - // Get Post IDs that contain the AWeber form shortcode. + // Get Post IDs that contain the third party form shortcode. $post_ids = $this->get_forms_in_posts(); // If no post IDs are found, return an empty array. @@ -182,7 +182,7 @@ public function get_form_ids_in_posts() { return array(); } - // Iterate through Posts, extracting the Form IDs from the AWeber form shortcodes. + // Iterate through Posts, extracting the Form IDs from the third party form shortcodes. $form_ids = array(); foreach ( $post_ids as $post_id ) { $content_form_ids = $this->get_form_ids_from_content( get_post_field( 'post_content', $post_id ) ); @@ -205,13 +205,13 @@ public function get_form_ids_in_posts() { */ public function get_form_ids_from_content( $content ) { - $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 quoted or unquoted value. - . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. + $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 quoted or unquoted value. + . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. preg_match_all( $pattern, $content, $matches ); From 9df7214565d74d79588738ce77c6da41b90fa620 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 15 Jan 2026 11:00:13 +0800 Subject: [PATCH 11/11] Improve and clarify FromContent tests --- tests/Integration/ImporterTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Integration/ImporterTest.php b/tests/Integration/ImporterTest.php index bb8abc890..1fbbe2ca7 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -48,7 +48,8 @@ public function tearDown(): void } /** - * Test that the get_form_ids_from_content() method returns third party form shortcode Form IDs. + * Test that the get_form_ids_from_content() method returns AWeber form shortcode Form IDs + * ignoring any other shortcodes. * * @since 3.1.5 */ @@ -61,7 +62,7 @@ public function testGetAWeberFormIDsFromContent() $this->assertNotInstanceOf(\WP_Error::class, $this->importer); // Define the content to test. - $content = '[aweber formid="10"] some content [aweber formid="11"] some other content'; + $content = '[aweber formid="10"] some content [aweber formid="11"] some other content [mc4wp_form id="12"] different shortcode to ignore'; // Extract form IDs from content. $form_ids = $this->importer->get_form_ids_from_content( $content ); @@ -153,7 +154,8 @@ public function testAWeberReplaceShortcodesInContentIgnoringOtherShortcodes() } /** - * Test that the get_form_ids_from_content() method returns third party form shortcode Form IDs. + * Test that the get_form_ids_from_content() method returns MC4WP form shortcode Form IDs + * ignoring any other shortcodes. * * @since 3.1.5 */ @@ -166,7 +168,7 @@ public function testGetMC4WPFormIDsFromContent() $this->assertNotInstanceOf(\WP_Error::class, $this->importer); // Define the content to test. - $content = '[mc4wp_form id="10"] some content [mc4wp_form id="11"] some other content'; + $content = '[mc4wp_form id="10"] some content [mc4wp_form id="11"] some other content [aweber formid="12"] different shortcode to ignore'; // Extract form IDs from content. $form_ids = $this->importer->get_form_ids_from_content( $content );