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('