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 8a5a684d..ede6f41b 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, ); @@ -272,12 +300,17 @@ 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; @@ -324,6 +357,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' ), // @phpstan-ignore-line + 'alt' => $node->getAttribute( 'alt' ), // @phpstan-ignore-line + ); + + // 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_src( + $image_id, + 'full' + ); + + // Replace this image's `src` attribute with the Media Library Image URL. + $node->setAttribute( 'src', $image_url[0] ); // @phpstan-ignore-line + } + } + // Save HTML to a string. $content = $html->saveHTML(); @@ -460,19 +532,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' ) ), 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 8343492a..54fc7358 100644 --- a/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php +++ b/tests/acceptance/broadcasts/import-export/BroadcastsToPostsCest.php @@ -474,6 +474,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('