diff --git a/admin/importers/class-convertkit-admin-importer-newsletter.php b/admin/importers/class-convertkit-admin-importer-newsletter.php new file mode 100644 index 000000000..a99d34e9e --- /dev/null +++ b/admin/importers/class-convertkit-admin-importer-newsletter.php @@ -0,0 +1,49 @@ +get_form_ids_in_posts(); + + } + +} diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index 17e86d60c..f9abfc724 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -28,9 +28,9 @@ abstract class ConvertKit_Admin_Importer { * * @since 3.1.0 * - * @var string + * @var bool|string */ - public $shortcode_id_attribute = ''; + public $shortcode_id_attribute = false; /** * Holds the block name for the third party Form plugin. @@ -46,9 +46,9 @@ abstract class ConvertKit_Admin_Importer { * * @since 3.1.6 * - * @var string + * @var bool|string */ - public $block_id_attribute = ''; + public $block_id_attribute = false; /** * Returns an array of third party form IDs and titles. @@ -171,13 +171,20 @@ public function replace_shortcodes_in_posts( $third_party_form_id, $form_id ) { */ public function replace_shortcodes_in_content( $content, $third_party_form_id, $form_id ) { - $pattern = '/\[' // Start regex with an opening square bracket. + // If there's no shortcode ID attribute, match shortcodes with or without any attribute. + if ( ! $this->shortcode_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. - . '\s*=\s*' // Match optional whitespace around an equals sign. - . '(?:"' . preg_quote( (string) $third_party_form_id, '/' ) . '"|' . preg_quote( (string) $third_party_form_id, '/' ) . ')' // Match the form ID, quoted or unquoted. . '[^\]]*?\]/i'; // Match any other characters (non-greedy) up to the closing square bracket, case-insensitive. + } else { + $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. + . '\s*=\s*' // Match optional whitespace around an equals sign. + . '(?:"' . preg_quote( (string) $third_party_form_id, '/' ) . '"|' . preg_quote( (string) $third_party_form_id, '/' ) . ')' // Match the form ID, quoted or unquoted. + . '[^\]]*?\]/i'; // Match any other characters (non-greedy) up to the closing square bracket, case-insensitive. + } return preg_replace( $pattern, @@ -204,6 +211,14 @@ public function get_form_ids_in_posts() { return array(); } + // If the shortcode or block ID attribute is not set, the third party Plugin doesn't use IDs + // and only has one form. + if ( ! $this->shortcode_id_attribute && ! $this->block_id_attribute ) { + return array( + __( 'Default Form', 'convertkit' ), + ); + } + // Iterate through Posts, extracting the Form IDs from the third party form shortcodes. $form_ids = array(); foreach ( $post_ids as $post_id ) { @@ -227,6 +242,24 @@ public function get_form_ids_in_posts() { */ public function get_form_ids_from_content( $content ) { + // If there's no shortcode ID attribute, match shortcodes with or without any attribute and treat any match as a single "form". + if ( ! $this->shortcode_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. + . '[^\]]*?\]/i'; // Match up to closing bracket, case-insensitive. + + preg_match_all( $pattern, $content, $matches ); + + // If we matched at least one occurrence, just return an array with a single 0 (default/non-ID form). + if ( ! empty( $matches[0] ) ) { + return array( 0 ); + } + + 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. @@ -365,14 +398,18 @@ private function recursively_convert_blocks( $blocks, $third_party_form_id, $for continue; } - // 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; - } - - // 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 ) { - continue; + // 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; + } + + // 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 ) { + continue; + } } // Replace third party form block with Kit form block. diff --git a/admin/section/class-convertkit-admin-section-tools.php b/admin/section/class-convertkit-admin-section-tools.php index a37447258..b2d6f78e5 100644 --- a/admin/section/class-convertkit-admin-section-tools.php +++ b/admin/section/class-convertkit-admin-section-tools.php @@ -53,13 +53,14 @@ public function register_notices( $notices ) { return array_merge( $notices, array( - 'import_configuration_upload_error' => __( 'An error occured uploading the configuration file.', 'convertkit' ), - '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' ), - 'migrate_mailpoet_configuration_success' => __( 'MailPoet forms migrated successfully.', 'convertkit' ), + 'import_configuration_upload_error' => __( 'An error occured uploading the configuration file.', 'convertkit' ), + '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' ), + 'migrate_mailpoet_configuration_success' => __( 'MailPoet forms migrated successfully.', 'convertkit' ), + 'migrate_newsletter_configuration_success' => __( 'Newsletter forms migrated successfully.', 'convertkit' ), ) ); @@ -81,6 +82,7 @@ private function maybe_perform_actions() { $this->maybe_migrate_aweber_configuration(); $this->maybe_migrate_mc4wp_configuration(); $this->maybe_migrate_mailpoet_configuration(); + $this->maybe_migrate_newsletter_configuration(); } @@ -428,6 +430,42 @@ private function maybe_migrate_mailpoet_configuration() { } + /** + * Replaces Newsletter Form Blocks and Shortcodes with Kit Form Blocks and Shortcodes, if the user submitted the + * Newsletter Migrate Configuration section. + * + * @since 3.1.6 + */ + private function maybe_migrate_newsletter_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 Newsletter Form IDs were submitted. + if ( ! isset( $_REQUEST['_wp_convertkit_integration_newsletter_settings'] ) ) { + return; + } + + // Initialise the importer. + $newsletter = new ConvertKit_Admin_Importer_Newsletter(); + + // Iterate through the Newsletter Form IDs and replace the blocks and shortcodes with the Kit Form blocks and shortcodes. + foreach ( array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['_wp_convertkit_integration_newsletter_settings'] ) ) as $newsletter_form_id => $kit_form_id ) { + $newsletter->replace_blocks_in_posts( (int) $newsletter_form_id, (int) $kit_form_id ); + $newsletter->replace_shortcodes_in_posts( (int) $newsletter_form_id, (int) $kit_form_id ); + } + + // Redirect to Tools screen. + $this->redirect_with_success_notice( 'migrate_newsletter_configuration_success' ); + + } + /** * Outputs the Debug Log and System Info view. * @@ -450,9 +488,10 @@ public function render() { $forms = new ConvertKit_Resource_Forms(); // Get Importers. - $aweber = new ConvertKit_Admin_Importer_AWeber(); - $mc4wp = new ConvertKit_Admin_Importer_MC4WP(); - $mailpoet = new ConvertKit_Admin_Importer_Mailpoet(); + $aweber = new ConvertKit_Admin_Importer_AWeber(); + $mc4wp = new ConvertKit_Admin_Importer_MC4WP(); + $mailpoet = new ConvertKit_Admin_Importer_Mailpoet(); + $newsletter = new ConvertKit_Admin_Importer_Newsletter(); // Output view. require_once CONVERTKIT_PLUGIN_PATH . '/views/backend/settings/tools.php'; diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterNewsletterCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterNewsletterCest.php new file mode 100644 index 000000000..43c25a6e0 --- /dev/null +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsToolsImporterNewsletterCest.php @@ -0,0 +1,208 @@ + Kit > Tools > Import sections for the Newsletter third party form plugin. + * + * @since 3.1.6 + */ +class PluginSettingsToolsImporterNewsletterCest +{ + /** + * Run common actions before running the test functions in this class. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + */ + public function _before(EndToEndTester $I) + { + // Activate Plugins. + $I->activateKitPlugin($I); + } + + /** + * Test that Newsletter Form Shortcodes are replaced with Kit Form Shortcodes when the Tools > Newsletter: Migrate Configuration is configured. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + */ + public function testNewsletterImportWithShortcodes(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Insert Newsletter Form Shortcode into Page. + $pageID = $this->_createPageWithNewsletterFormShortcodes($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Select the Kit Form to replace the Newsletter Forms. + $I->selectOption('_wp_convertkit_integration_newsletter_settings[0]', $_ENV['CONVERTKIT_API_FORM_ID']); + + // Click the Migrate button. + $I->click('Migrate'); + + // Confirm success message displays. + $I->waitForElementVisible('.notice-success'); + $I->see('Newsletter forms migrated successfully.'); + + // View the Page, to confirm Kit Forms now display. + $I->amOnPage('?p=' . $pageID); + $I->seeElementInDOM('form[data-sv-form]'); + } + + /** + * Test that Newsletter Blocks are replaced with Kit Blocks when the Tools > Newsletter: Migrate Configuration is configured. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + */ + public function testNewsletterImportWithBlocks(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Insert Newsletter Block into Page. + $pageID = $this->_createPageWithNewsletterBlock($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Select the Kit Form to replace the Newsletter Form. + $I->selectOption('_wp_convertkit_integration_newsletter_settings[0]', $_ENV['CONVERTKIT_API_FORM_ID']); + + // Click the Migrate button. + $I->click('Migrate'); + + // Confirm success message displays. + $I->waitForElementVisible('.notice-success'); + $I->see('Newsletter forms migrated successfully.'); + + // View the Page, to confirm Kit Form block now displays. + $I->amOnPage('?p=' . $pageID); + $I->seeElementInDOM('form[data-sv-form]'); + } + + /** + * Test that the Newsletter: Migrate Configuration section is not displayed when Newsletter Forms exist, + * but no Pages, Posts or Custom Posts contain Newsletter Form Shortcodes. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + */ + public function testNewsletterImportWhenNoNewsletterShortcodesInContent(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no Newsletter: Migrate Configuration section is displayed, as there are no + // Newsletter Form Shortcodes in the content. + $I->dontSeeElementInDOM('#import-newsletter'); + } + + /** + * Test that the Newsletter: Migrate Configuration section is not displayed when no Kit Forms exist. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + */ + public function testNewsletterImportWhenNoKitForms(EndToEndTester $I) + { + // Setup Plugin. + $I->setupKitPluginCredentialsNoData($I); + $I->setupKitPluginResourcesNoData($I); + + // Navigate to the Tools screen. + $I->loadKitSettingsToolsScreen($I); + + // Confirm no Newsletter: Migrate Configuration section is displayed, as there are no + // Newsletter Form Shortcodes in the content. + $I->dontSeeElementInDOM('#import-newsletter'); + } + + /** + * Create Page with Newsletter Form Shortcodes. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + * @return int + */ + private function _createPageWithNewsletterFormShortcodes(EndToEndTester $I) + { + return $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with Newsletter Form', + 'post_content' => '[newsletter_form]', + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ); + } + + /** + * Create Page with Newsletter Block. + * + * @since 3.1.6 + * + * @param EndToEndTester $I Tester. + * @return int + */ + private function _createPageWithNewsletterBlock(EndToEndTester $I) + { + return $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with Newsletter Block', + 'post_content' => '', + + // Configure Kit Plugin to not display a default Form, so we test against the Kit Form in the content. + '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.1.6 + * + * @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 f6e1a36f5..580d6dca3 100644 --- a/tests/Integration/ImporterTest.php +++ b/tests/Integration/ImporterTest.php @@ -543,4 +543,157 @@ public function testMailPoetReplaceBlocksInContent() $this->importer->replace_blocks_in_content( parse_blocks( $content ), 4410, $_ENV['CONVERTKIT_API_FORM_ID'] ) ); } + + /** + * Test that the get_form_ids_from_content() method returns a single Newsletter form shortcode, + * ignoring any other shortcodes. + * + * @since 3.1.6 + */ + public function testGetNewsletterFormFromContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_Newsletter(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the content to test. + $content = '[newsletter_form] some content [newsletter_form] some other content [aweber formid="12"] different shortcode to ignore'; + + // Extract forms from content. + // Only one form should be returned as the Newsletter Plugin does not use IDs. + $form_ids = $this->importer->get_form_ids_from_content( $content ); + + // Assert the correct number of form IDs are returned. + $this->assertEquals( 1, count( $form_ids ) ); + $this->assertEquals( 0, $form_ids[0] ); + } + + /** + * Test that the replace_shortcodes_in_content() method replaces the Newsletter form shortcode with the Kit form shortcode. + * + * @since 3.1.6 + */ + public function testNewsletterReplaceShortcodesInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_Newsletter(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Define the shortcodes to test. + $shortcodes = [ + '[newsletter_form]', + ]; + + // 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, 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.', + $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'] . '"]', + $this->importer->replace_shortcodes_in_content( $content, 0, $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } + } + + /** + * Test that the replace_shortcodes_in_content() method ignores non-MailPoet shortcodes. + * + * @since 3.1.6 + */ + public function testNewsletterReplaceShortcodesInContentIgnoringOtherShortcodes() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_Newsletter(); + + // 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, 0, $_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.1.6 + */ + public function testNewsletterReplaceBlocksInPost() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_Newsletter(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->importer); + + // Create a Post with a MailPoet 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' => 'Newsletter: Replace Blocks in Post', + 'post_content' => str_replace( '\\', '\\\\', '' . $this->html_block ), + ] + ); + + // Replace the blocks in the post. + $this->importer->replace_blocks_in_post( $postID, 0, $_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.1.6 + */ + public function testNewsletterReplaceBlocksInContent() + { + // Initialize the class we want to test. + $this->importer = new \ConvertKit_Admin_Importer_Newsletter(); + + // 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 ), 0, $_ENV['CONVERTKIT_API_FORM_ID'] ) + ); + } } diff --git a/views/backend/settings/tools.php b/views/backend/settings/tools.php index 53f2876cf..a6e05be82 100644 --- a/views/backend/settings/tools.php +++ b/views/backend/settings/tools.php @@ -212,8 +212,6 @@ has_forms_in_posts() && $mailpoet->has_forms() && $forms->exist() ) { ?> @@ -269,6 +267,61 @@ has_forms_in_posts() && $newsletter->has_forms() && $forms->exist() ) { + ?> +
+

+ +

+
+

+ + + + + + + + + + get_forms() as $newsletter_form_id => $newsletter_form_title ) { + ?> + + + + + + +
+ +
+ +

+ +

+
+ diff --git a/wp-convertkit.php b/wp-convertkit.php index 6ef1ab423..d412bf834 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -120,6 +120,7 @@ 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/importers/class-convertkit-admin-importer-mailpoet.php'; +require_once CONVERTKIT_PLUGIN_PATH . '/admin/importers/class-convertkit-admin-importer-newsletter.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'; require_once CONVERTKIT_PLUGIN_PATH . '/admin/section/class-convertkit-admin-section-form-entries.php';