broadcasts_settings = new ConvertKit_Settings_Broadcasts();
$this->media_library = new ConvertKit_Media_Library();
$this->settings = new ConvertKit_Settings();
// Create WordPress Posts when the ConvertKit Posts Resource is refreshed.
add_action( 'convertkit_resource_refreshed_posts', array( $this, 'refresh' ) );
}
/**
* When the list of broadcasts is refreshed by the resource class, iterate through
* each Broadcast, to check if a Post exists in WordPress for it.
*
* If not, creates the WordPress Post.
*
* @since 2.2.9
*
* @param array $broadcasts Broadcasts.
*/
public function refresh( $broadcasts ) {
// Bail if Broadcasts to Posts are disabled.
if ( ! $this->broadcasts_settings->enabled() ) {
return;
}
// Bail if no Broadcasts exist.
if ( ! count( $broadcasts ) ) {
return;
}
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 ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . ' already exists as a WordPress Post. Skipping...' );
continue;
}
// Skip if the published_at date is older than the 'Earliest Date' setting.
if ( strtotime( $broadcast['published_at'] ) < strtotime( $this->broadcasts_settings->published_at_min_date() ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . ' published_at date is before ' . $this->broadcasts_settings->published_at_min_date() . '. Skipping...' );
continue;
}
// Import the broadcast.
$this->import_broadcast(
$broadcast_id,
$this->broadcasts_settings->post_status(),
$this->broadcasts_settings->author_id(),
$this->broadcasts_settings->category_id(),
$this->broadcasts_settings->import_thumbnail(),
$this->broadcasts_settings->import_images(),
$this->broadcasts_settings->no_styles()
);
}
}
/**
* Imports the given Kit Broadcast ID to a new WordPress Post.
*
* @since 2.6.4
*
* @param int $broadcast_id Broadcast ID.
* @param string $post_status WordPress Post Status to save Post as (publish,draft etc).
* @param int $author_id WordPress User ID to assign as the author of the Post.
* @param bool|int $category_id WordPress Category to assign to the Post.
* @param bool $import_thumbnail Store Broadcast's thumbnail as the WordPress Post's Featured Image.
* @param bool $import_images Store Broadcast's inline images in the Media Library.
* @param bool $disable_styles Remove CSS styles and layout elements from the Broadcast content.
*
* @return WP_Error|int
*/
public function import_broadcast( $broadcast_id, $post_status = 'publish', $author_id = 1, $category_id = false, $import_thumbnail = false, $import_images = false, $disable_styles = false ) {
// Bail if the Plugin Access Token has not been configured.
if ( ! $this->settings->has_access_and_refresh_token() ) {
return new WP_Error(
'convertkit_broadcasts_importer_error',
__( 'No Access Token specified in Plugin Settings', 'convertkit' )
);
}
// Initialize the API.
$api = new ConvertKit_API_V4(
CONVERTKIT_OAUTH_CLIENT_ID,
CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
$this->settings->get_access_token(),
$this->settings->get_refresh_token(),
$this->settings->debug_enabled(),
'broadcasts_importer'
);
// Check that we're using the ConvertKit WordPress Libraries 1.3.8 or higher.
// If another ConvertKit Plugin is active and out of date, its libraries might
// be loaded that don't have this method.
if ( ! method_exists( $api, 'get_post' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
return new WP_Error(
'convertkit_broadcasts_importer_error',
__( 'Kit WordPress Libraries 1.3.7 or older detected, missing the `get_post` method.', 'convertkit' )
);
}
// Fetch Broadcast's content.
// We need to query wordpress/posts/{id} to fetch the full Broadcast information and content.
$broadcast = $api->get_post( $broadcast_id );
// Unset API class.
unset( $api );
// Bail if an error occurred fetching the Broadcast.
if ( is_wp_error( $broadcast ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error fetching from API: ' . $broadcast->get_error_message() );
return $broadcast;
}
// 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,
$author_id,
$category_id
),
true
);
// Bail if an error occurred.
if ( is_wp_error( $post_id ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on wp_insert_post(): ' . $post_id->get_error_message() );
return $post_id;
}
// 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(
$post_id,
$broadcast['content'],
$broadcast['title'],
$import_images,
$disable_styles
),
),
true
);
// Bail if an error occurred updating the Post.
if ( is_wp_error( $post_id ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on wp_update_post() when adding Broadcast content: ' . $post_id->get_error_message() );
return $post_id;
}
// If a Product is specified, apply it as the Restrict Content setting.
if ( $broadcast['is_paid'] && $broadcast['product_id'] ) {
// Fetch Post's settings.
$convertkit_post = new ConvertKit_Post( $post_id );
$meta = $convertkit_post->get();
// Define Restrict Content setting.
$meta['restrict_content'] = 'product_' . $broadcast['product_id'];
// Save Post's settings.
$convertkit_post->save( $meta );
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Set Restrict Content = ' . $broadcast['product_id'] );
}
// If the Import Thumbnail setting is enabled, and the Broadcast has an image, save it to the Media Library and link it to the Post.
if ( $import_thumbnail ) {
$result = $this->add_broadcast_image_to_post( $broadcast, $post_id );
if ( is_wp_error( $result ) ) {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Error on add_broadcast_image_to_post(): ' . $result->get_error_message() );
}
} else {
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Skipping thumbnail.' );
}
// Transition the Post to the defined Post Status in the settings, now that the image has been added to it.
$post_id = wp_update_post(
array(
'ID' => $post_id,
'post_status' => $post_status,
),
true
);
// Maybe log if an error occurred updating the Post to the publish status.
if ( is_wp_error( $post_id ) ) {
$this->maybe_log( '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() );
return $post_id;
}
// Import successful.
$this->maybe_log( 'ConvertKit_Broadcasts_Importer::refresh(): Broadcast #' . $broadcast_id . '. Added as Post ID #' . $post_id );
return $post_id;
}
/**
* Logs the given message if debug logging is enabled
* in the Plugin's settings.
*
* @since 2.6.4
*
* @param string $message Log message.
*/
private function maybe_log( $message ) {
// Don't log if debugging is not enabled.
if ( ! $this->settings->debug_enabled() ) {
return;
}
// Initialize logging class, if not yet initialized.
if ( ! $this->log ) {
$this->log = new ConvertKit_Log( CONVERTKIT_PLUGIN_PATH );
}
$this->log->add( $message );
}
/**
* Helper method to determine if the given ConvertKit Broadcast already exists
* as a WordPress Post.
*
* @since 2.2.7
*
* @param int $broadcast_id ConvertKit Broadcast ID.
* @return bool Broadcast exists as a WordPress Post
*/
private function broadcast_exists_as_post( $broadcast_id ) {
$posts = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'any',
'meta_query' => array(
array(
'key' => '_convertkit_broadcast_id',
'value' => $broadcast_id,
),
),
'fields' => 'ids',
'update_post_cache' => false,
)
);
if ( ! $posts->post_count ) {
return false;
}
return true;
}
/**
* Defines the wp_insert_post() compatible arguments for importing the given ConvertKit
* Broadcast to a new WordPress Post.
*
* @since 2.2.9
*
* @param array $broadcast Broadcast.
* @param int $author_id WordPress User to assign as the author of the Post.
* @param bool|int $category_id Category ID.
* @return array wp_insert_post() compatible arguments.
*/
private function build_post_args( $broadcast, $author_id, $category_id = false ) {
// Define array for the wp_insert_post() compatible arguments.
$post_args = array(
'post_type' => 'post',
'post_title' => $broadcast['title'],
'post_excerpt' => ( ! is_null( $broadcast['description'] ) ? $broadcast['description'] : '' ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( $broadcast['published_at'] ) ),
'post_author' => $author_id,
'post_name' => $this->generate_permalink( $broadcast['title'] ),
);
// If a Category was supplied, assign the Post to the given Category ID when created.
if ( $category_id ) {
$post_args['post_category'] = array( $category_id );
}
/**
* Define the wp_insert_post() compatible arguments for importing a ConvertKit Broadcast
* to a new WordPress Post.
*
* @since 2.2.9
*
* @param array $post_args Post arguments.
* @param array $broadcast Broadcast.
*/
$post_args = apply_filters( 'convertkit_broadcasts_build_post_args', $post_args, $broadcast );
// Deliberate: force the Post Status = draft. This will be changed to scheduled or publish after
// any image is imported to the Post's Featured Image. So many plugins wrongly publish a Post
// and then import the Featured Image, which breaks a lot of social media sharing Plugins.
$post_args['post_status'] = 'draft';
// Deliberate: ensure the Broadcast ID is always defined.
if ( ! array_key_exists( 'meta_input', $post_args ) ) {
$post_args['meta_input'] = array();
}
$post_args['meta_input']['_convertkit_broadcast_id'] = $broadcast['id'];
return $post_args;
}
/**
* Removes emojis from the given string.
*
* @since 2.8.2
*
* @param string $title Broadcast Title.
* @return string
*/
public function generate_permalink( $title ) {
// Remove emojis.
$title = preg_replace( '/[^\p{L}\p{N}\p{P}\s]+/u', '', $title );
// Return the Permalink.
return sanitize_title( $title );
}
/**
* 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 int $post_id WordPress Post ID.
* @param string $broadcast_content Broadcast Content.
* @param string $broadcast_title Broadcast Title.
* @param bool $import_images Import images to Media Library.
* @param bool $disable_styles Disable CSS styles in content.
* @return string Parsed Content.
*/
private function parse_broadcast_content( $post_id, $broadcast_content, $broadcast_title = '', $import_images = false, $disable_styles = false ) {
$content = $broadcast_content;
// Load the content into the parser.
$parser = new ConvertKit_HTML_Parser( $content );
// Remove certain elements and their contents, as we never want these to be included in the WordPress Post.
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Remove open tracking.
foreach ( $parser->xpath->query( '//img[@src="https://preview.convertkit-mail2.com/open"]' ) as $node ) {
$node->parentNode->removeChild( $node );
}
// Remove blank contenteditable table cells.
foreach ( $parser->xpath->query( '//td[@contenteditable="false"]' ) as $node ) {
$node->parentNode->removeChild( $node );
}
// Remove