From 6f1f1342fea8cdde694125633edac68a2d608fa5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 25 Oct 2024 19:44:09 +0800 Subject: [PATCH 1/8] Started work on importing broadcast images to Media Library --- ...s-convertkit-admin-settings-broadcasts.php | 44 +++++++++ .../class-convertkit-broadcasts-importer.php | 92 ++++++++++++++++--- .../class-convertkit-settings-broadcasts.php | 14 +++ 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/admin/section/class-convertkit-admin-settings-broadcasts.php b/admin/section/class-convertkit-admin-settings-broadcasts.php index 939e0a70..80f74933 100644 --- a/admin/section/class-convertkit-admin-settings-broadcasts.php +++ b/admin/section/class-convertkit-admin-settings-broadcasts.php @@ -273,6 +273,20 @@ public function register_fields() { ) ); + add_settings_field( + 'import_images', + __( 'Import Images', 'convertkit' ), + array( $this, 'import_images_callback' ), + $this->settings_key, + $this->name, + array( + 'name' => 'import_images', + 'label_for' => 'import_images', + 'label' => __( 'If enabled, the imported Broadcast\'s inline images will be stored in the Media Library, instead of served by Kit.', 'convertkit' ), + 'description' => '', + ) + ); + add_settings_field( 'published_at_min_date', __( 'Earliest Date', 'convertkit' ), @@ -509,6 +523,29 @@ public function import_thumbnail_callback( $args ) { } + /** + * Renders the input for the Import Images setting. + * + * @since 2.6.3 + * + * @param array $args Setting field arguments (name,description). + */ + public function import_images_callback( $args ) { + + // Output field. + echo $this->get_checkbox_field( // phpcs:ignore WordPress.Security.EscapeOutput + $args['name'], + 'on', + $this->settings->import_images(), // phpcs:ignore WordPress.Security.EscapeOutput + $args['label'], // phpcs:ignore WordPress.Security.EscapeOutput + $args['description'], // phpcs:ignore WordPress.Security.EscapeOutput + array( + 'enabled', + ) + ); + + } + /** * Renders the input for the date setting. * @@ -585,6 +622,13 @@ public function sanitize_settings( $settings ) { $settings['import_thumbnail'] = ''; } + // If the 'Include Images' setting isn't checked, it won't be included + // in the array of settings, and the defaults will enable this. + // Therefore, if the setting doesn't exist, set it to blank. + if ( ! array_key_exists( 'import_images', $settings ) ) { + $settings['import_images'] = ''; + } + // Merge settings with defaults. $settings = wp_parse_args( $settings, $this->settings->get_defaults() ); diff --git a/includes/class-convertkit-broadcasts-importer.php b/includes/class-convertkit-broadcasts-importer.php index b0326d95..b3a614d1 100644 --- a/includes/class-convertkit-broadcasts-importer.php +++ b/includes/class-convertkit-broadcasts-importer.php @@ -23,6 +23,15 @@ class ConvertKit_Broadcasts_Importer { */ private $broadcasts_settings = false; + /** + * Holds the Media Library class. + * + * @since 2.6.3 + * + * @var bool|ConvertKit_Media_Library + */ + private $media_library = false; + /** * Constructor. Registers actions and filters to output ConvertKit Forms and Landing Pages * on the frontend web site. @@ -50,6 +59,7 @@ public function refresh( $broadcasts ) { // Initialize required classes. $this->broadcasts_settings = new ConvertKit_Settings_Broadcasts(); + $this->media_library = new ConvertKit_Media_Library(); $settings = new ConvertKit_Settings(); $log = new ConvertKit_Log( CONVERTKIT_PLUGIN_PATH ); @@ -85,7 +95,6 @@ public function refresh( $broadcasts ) { return; } - // Iterate through each Broadcast. foreach ( $broadcasts as $broadcast_id => $broadcast ) { // If a WordPress Post exists for this Broadcast ID, we previously imported it - skip it. if ( $this->broadcast_exists_as_post( $broadcast_id ) ) { @@ -115,7 +124,10 @@ public function refresh( $broadcasts ) { continue; } - // Create Post as a draft. + // Create Post as a draft, without content or a Featured Image. + // This gives us a Post ID we can then use if we need to import + // the Featured Image and/or Broadcast images to the Media Library, + // storing them against the Post ID just created. $post_id = wp_insert_post( $this->build_post_args( $broadcast, @@ -133,6 +145,23 @@ public function refresh( $broadcasts ) { continue; } + // Parse the Broadcast's content, storing it in the Post. + $post_id = wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => $this->parse_broadcast_content( $broadcast['content'], $post_id ), + ), + true + ); + + // Skip if an error occured. + if ( is_wp_error( $post_id ) ) { + if ( $settings->debug_enabled() ) { + $log->add( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on wp_update_post() when adding Broadcast content: ' . $post_id->get_error_message() ); + } + continue; + } + // If a Product is specified, apply it as the Restrict Content setting. if ( $broadcast['is_paid'] && $broadcast['product_id'] ) { // Fetch Post's settings. @@ -173,7 +202,7 @@ public function refresh( $broadcasts ) { // Maybe log if an error occured updating the Post to the publish status. if ( is_wp_error( $post_id ) ) { if ( $settings->debug_enabled() ) { - $log->add( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on wp_update_post(): ' . $post_id->get_error_message() ); + $log->add( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on wp_update_post() when transitioning post status from draft to publish: ' . $post_id->get_error_message() ); } } if ( $settings->debug_enabled() ) { @@ -235,7 +264,6 @@ private function build_post_args( $broadcast, $author_id, $category_id = false ) 'post_type' => 'post', 'post_title' => $broadcast['title'], 'post_excerpt' => ( ! is_null( $broadcast['description'] ) ? $broadcast['description'] : '' ), - 'post_content' => $this->parse_broadcast_content( $broadcast['content'] ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( $broadcast['published_at'] ) ), 'post_author' => $author_id, ); @@ -271,13 +299,18 @@ private function build_post_args( $broadcast, $author_id, $category_id = false ) /** * Parses the given Broadcast's content, removing unnecessary HTML tags and styles. + * + * If 'Import Images' is enabled in the Plugin settings, imports images to the + * Media Library, replacing the `src` with the WordPress Media Library + * Image URL. * * @since 2.2.9 * * @param string $broadcast_content Broadcast Content. - * @return string Parsed Content. + * @param int $post_id WordPress Post ID. + * @return string Parsed Content. */ - private function parse_broadcast_content( $broadcast_content ) { + private function parse_broadcast_content( $broadcast_content, $post_id ) { $content = $broadcast_content; @@ -319,6 +352,45 @@ private function parse_broadcast_content( $broadcast_content ) { $node->parentNode->removeChild( $node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } + // If the Import Images setting is enabled, iterate through all images within the Broadcast, importing them and changing their + // URLs to the WordPress Media Library hosted versions. + if ( $this->broadcasts_settings->import_images() ) { + + + foreach ( $xpath->query( '//img' ) as $node ) { + $image = array( + 'src' => $node->getAttribute( 'src' ), + 'alt' => $node->getAttribute( 'alt' ), + ); + + // Skip if this image isn't served from https://embed.filekitcdn.com, as it isn't + // a user uploaded image to the Broadcast. + if ( strpos( $image['src'], 'https://embed.filekitcdn.com' ) === false ) { + continue; + } + + // Import Image into the Media Library. + $image_id = $this->media_library->import_remote_image( + $image['src'], + $post_id, + $image['alt'] + ); + + // If the image could not be imported, serve the original CDN version. + if ( is_wp_error( $image_id ) ) { + continue; + } + + // Get image URL from Media Library. + $image_url = wp_get_attachment_image( $image_id, 'full', false, array( + 'alt' => $image['alt'], + ) ); + + // Replace this image's `src` attribute with the Media Library Image URL. + $node->setAttribute( 'src', $image_url ); + } + } + // Save HTML to a string. $content = $html->saveHTML(); @@ -455,19 +527,13 @@ private function add_broadcast_image_to_post( $broadcast, $post_id ) { return false; } - // Initialize class. - $media_library = new ConvertKit_Media_Library(); - // Import Image into the Media Library. - $image_id = $media_library->import_remote_image( + $image_id = $this->media_library->import_remote_image( $broadcast['thumbnail_url'], $post_id, $broadcast['thumbnail_alt'] ); - // Destroy class. - unset( $media_library ); - // Bail if an error occured. if ( is_wp_error( $image_id ) ) { return $image_id; diff --git a/includes/class-convertkit-settings-broadcasts.php b/includes/class-convertkit-settings-broadcasts.php index 27490aa5..10fc8f4b 100644 --- a/includes/class-convertkit-settings-broadcasts.php +++ b/includes/class-convertkit-settings-broadcasts.php @@ -137,6 +137,19 @@ public function import_thumbnail() { } + /** + * Returns whether to import the thumbnail to the Featured Image. + * + * @since 2.6.3 + * + * @return bool + */ + public function import_images() { + + return ( $this->settings['import_images'] === 'on' ? true : false ); + + } + /** * Returns the earliest date that Broadcasts should be imported, * based on their published_at date. @@ -220,6 +233,7 @@ public function get_defaults() { 'post_status' => 'publish', 'category_id' => '', 'import_thumbnail' => 'on', + 'import_images' => '', // By default, only import Broadcasts as Posts for the last 30 days. 'published_at_min_date' => gmdate( 'Y-m-d', strtotime( '-30 days' ) ), From a9afc890a1f48f8afa9048ae05fc409a816aa209 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 25 Oct 2024 19:46:19 +0800 Subject: [PATCH 2/8] Coding standards --- .../class-convertkit-broadcasts-importer.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/includes/class-convertkit-broadcasts-importer.php b/includes/class-convertkit-broadcasts-importer.php index b3a614d1..8d875c8f 100644 --- a/includes/class-convertkit-broadcasts-importer.php +++ b/includes/class-convertkit-broadcasts-importer.php @@ -59,7 +59,7 @@ public function refresh( $broadcasts ) { // Initialize required classes. $this->broadcasts_settings = new ConvertKit_Settings_Broadcasts(); - $this->media_library = new ConvertKit_Media_Library(); + $this->media_library = new ConvertKit_Media_Library(); $settings = new ConvertKit_Settings(); $log = new ConvertKit_Log( CONVERTKIT_PLUGIN_PATH ); @@ -299,7 +299,7 @@ private function build_post_args( $broadcast, $author_id, $category_id = false ) /** * Parses the given Broadcast's content, removing unnecessary HTML tags and styles. - * + * * If 'Import Images' is enabled in the Plugin settings, imports images to the * Media Library, replacing the `src` with the WordPress Media Library * Image URL. @@ -307,7 +307,7 @@ private function build_post_args( $broadcast, $author_id, $category_id = false ) * @since 2.2.9 * * @param string $broadcast_content Broadcast Content. - * @param int $post_id WordPress Post ID. + * @param int $post_id WordPress Post ID. * @return string Parsed Content. */ private function parse_broadcast_content( $broadcast_content, $post_id ) { @@ -355,7 +355,6 @@ private function parse_broadcast_content( $broadcast_content, $post_id ) { // If the Import Images setting is enabled, iterate through all images within the Broadcast, importing them and changing their // URLs to the WordPress Media Library hosted versions. if ( $this->broadcasts_settings->import_images() ) { - foreach ( $xpath->query( '//img' ) as $node ) { $image = array( @@ -382,9 +381,14 @@ private function parse_broadcast_content( $broadcast_content, $post_id ) { } // Get image URL from Media Library. - $image_url = wp_get_attachment_image( $image_id, 'full', false, array( - 'alt' => $image['alt'], - ) ); + $image_url = wp_get_attachment_image( + $image_id, + 'full', + false, + array( + 'alt' => $image['alt'], + ) + ); // Replace this image's `src` attribute with the Media Library Image URL. $node->setAttribute( 'src', $image_url ); From 0f5ddbef015467fd7d92bdc3b43d2599b0be0a32 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 28 Oct 2024 11:25:09 +0800 Subject: [PATCH 3/8] WIP --- admin/class-convertkit-admin-settings.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/admin/class-convertkit-admin-settings.php b/admin/class-convertkit-admin-settings.php index 1f2be4a4..f8453ccf 100644 --- a/admin/class-convertkit-admin-settings.php +++ b/admin/class-convertkit-admin-settings.php @@ -108,6 +108,13 @@ public function enqueue_styles( $hook ) { */ public function add_settings_page() { + // Refresh Posts Resource. + /* + $posts = new ConvertKit_Resource_Posts( 'cron' ); + $result = $posts->refresh(); + die('Finished'); + */ + add_options_page( __( 'Kit', 'convertkit' ), __( 'Kit', 'convertkit' ), From e97739d61f17a4e781d372ff3c2a4f271af00fff Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 28 Oct 2024 14:45:59 +0800 Subject: [PATCH 4/8] Set image URL to WordPress hosted version --- includes/class-convertkit-broadcasts-importer.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/includes/class-convertkit-broadcasts-importer.php b/includes/class-convertkit-broadcasts-importer.php index 8d875c8f..f0e4c319 100644 --- a/includes/class-convertkit-broadcasts-importer.php +++ b/includes/class-convertkit-broadcasts-importer.php @@ -381,17 +381,13 @@ private function parse_broadcast_content( $broadcast_content, $post_id ) { } // Get image URL from Media Library. - $image_url = wp_get_attachment_image( + $image_url = wp_get_attachment_image_src( $image_id, - 'full', - false, - array( - 'alt' => $image['alt'], - ) + 'full' ); // Replace this image's `src` attribute with the Media Library Image URL. - $node->setAttribute( 'src', $image_url ); + $node->setAttribute( 'src', $image_url[0] ); } } From b1e7fd9f6c44b6f55eea96f780855b97e407984d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 28 Oct 2024 14:46:34 +0800 Subject: [PATCH 5/8] Remove debugging code --- admin/class-convertkit-admin-settings.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/admin/class-convertkit-admin-settings.php b/admin/class-convertkit-admin-settings.php index f8453ccf..1f2be4a4 100644 --- a/admin/class-convertkit-admin-settings.php +++ b/admin/class-convertkit-admin-settings.php @@ -108,13 +108,6 @@ public function enqueue_styles( $hook ) { */ public function add_settings_page() { - // Refresh Posts Resource. - /* - $posts = new ConvertKit_Resource_Posts( 'cron' ); - $result = $posts->refresh(); - die('Finished'); - */ - add_options_page( __( 'Kit', 'convertkit' ), __( 'Kit', 'convertkit' ), From d6ad47cf713b23c608d19fb4f0fd3ad1f7206f5d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 28 Oct 2024 15:24:29 +0800 Subject: [PATCH 6/8] Added tests --- .../Acceptance/ConvertKitBroadcasts.php | 1 + .../import-export/BroadcastsToPostsCest.php | 60 +++++++++++++++++++ .../BroadcastsToPostsSettingsCest.php | 5 ++ 3 files changed, 66 insertions(+) diff --git a/tests/_support/Helper/Acceptance/ConvertKitBroadcasts.php b/tests/_support/Helper/Acceptance/ConvertKitBroadcasts.php index 4fa404f8..e074cacf 100644 --- a/tests/_support/Helper/Acceptance/ConvertKitBroadcasts.php +++ b/tests/_support/Helper/Acceptance/ConvertKitBroadcasts.php @@ -28,6 +28,7 @@ public function setupConvertKitPluginBroadcasts($I, $settings = false) switch ( $key ) { case 'enabled': case 'import_thumbnail': + case 'import_images': case 'enabled_export': case 'no_styles': if ( $value ) { diff --git a/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php b/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php index 2a91f1dd..13bef150 100644 --- a/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php +++ b/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php @@ -471,6 +471,66 @@ public function testBroadcastsImportWithImportThumbnailDisabled(AcceptanceTester } } + /** + * Tests that Broadcasts import with inline images copied to WordPress when the Import Images + * option is enabled. + * + * @since 2.6.3 + * + * @param AcceptanceTester $I Tester. + */ + public function testBroadcastsImportWithImportImagesEnabled(AcceptanceTester $I) + { + // Enable Broadcasts to Posts. + $I->setupConvertKitPluginBroadcasts( + $I, + [ + 'enabled' => true, + 'import_thumbnail' => false, + 'import_images' => true, + 'published_at_min_date' => '01/01/2020', + ] + ); + + // Run the WordPress Cron event to import Broadcasts to WordPress Posts. + $I->runCronEvent($I, $this->cronEventName); + + // Wait a few seconds for the Cron event to complete importing Broadcasts. + $I->wait(7); + + // Load the Posts screen. + $I->amOnAdminPage('edit.php'); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm expected Broadcasts exist as Posts. + $I->see($_ENV['CONVERTKIT_API_BROADCAST_FIRST_TITLE']); + $I->see($_ENV['CONVERTKIT_API_BROADCAST_SECOND_TITLE']); + $I->see($_ENV['CONVERTKIT_API_BROADCAST_THIRD_TITLE']); + + // Get created Post IDs. + $postIDs = [ + (int) str_replace('post-', '', $I->grabAttributeFrom('tbody#the-list > tr:nth-child(2)', 'id')), + (int) str_replace('post-', '', $I->grabAttributeFrom('tbody#the-list > tr:nth-child(3)', 'id')), + (int) str_replace('post-', '', $I->grabAttributeFrom('tbody#the-list > tr:nth-child(4)', 'id')), + ]; + + // Set cookie with signed subscriber ID, so Member Content broadcasts can be viewed. + $I->setCookie('ck_subscriber_id', $_ENV['CONVERTKIT_API_SIGNED_SUBSCRIBER_ID']); + + // View the first post. + $I->amOnPage('?p=' . $postIDs[0]); + + // Check that no PHP warnings or notices were output. + $I->checkNoWarningsAndNoticesOnScreen($I); + + // Confirm no images are served from Kit's CDN, and they are served from the WordPress Media Library + // (uploads folder). + $I->dontSeeInSource('embed.filekitcdn.com'); + $I->seeInSource($_ENV['TEST_SITE_WP_URL'] . '/wp-content/uploads/2023/08'); + } + /** * Tests that Broadcasts do not import when enabled in the Plugin's settings * and an Earliest Date is specified that is newer than any Broadcasts sent diff --git a/tests/acceptance/broadcasts/import-export/BroadcastsToPostsSettingsCest.php b/tests/acceptance/broadcasts/import-export/BroadcastsToPostsSettingsCest.php index 0704b0aa..65cdba0f 100644 --- a/tests/acceptance/broadcasts/import-export/BroadcastsToPostsSettingsCest.php +++ b/tests/acceptance/broadcasts/import-export/BroadcastsToPostsSettingsCest.php @@ -41,6 +41,7 @@ public function testAccessibility(AcceptanceTester $I) $I->seeInSource('