create_log( esc_html__( 'No RSS feed link is configured for this campaign. Edit the campaign and add the feed URL.', 'ai-content-writer' ), 'error', $campaign_id ); return false; } $response = wp_remote_get( $feed_link ); if ( is_wp_error( $response ) ) { ai_content_writer()->create_log( sprintf( /* translators: 1: RSS feed URL. 2: Error message. */ esc_html__( 'Could not fetch the RSS feed %1$s: %2$s', 'ai-content-writer' ), esc_url( $feed_link ), esc_html( $response->get_error_message() ) ), 'error', $campaign_id ); return false; } $response_code = wp_remote_retrieve_response_code( $response ); if ( $response_code < 200 || $response_code >= 300 ) { ai_content_writer()->create_log( sprintf( /* translators: 1: RSS feed URL. 2: HTTP status code. */ esc_html__( 'The RSS feed %1$s returned HTTP status %2$d. Check that the URL is correct and publicly reachable.', 'ai-content-writer' ), esc_url( $feed_link ), absint( $response_code ) ), 'error', $campaign_id ); return false; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { ai_content_writer()->create_log( sprintf( /* translators: %s: RSS feed URL. */ esc_html__( 'The RSS feed %s returned an empty response.', 'ai-content-writer' ), esc_url( $feed_link ) ), 'error', $campaign_id ); return false; } // Suppress warnings and manually handle XML errors. $previous_libxml_state = libxml_use_internal_errors( true ); $rss = simplexml_load_string( $body ); if ( ! $rss ) { // Keep the parser error — it is the only clue the user gets about why the feed failed. $xml_errors = libxml_get_errors(); $xml_message = ! empty( $xml_errors ) ? trim( $xml_errors[0]->message ) : ''; libxml_clear_errors(); libxml_use_internal_errors( $previous_libxml_state ); ai_content_writer()->create_log( sprintf( /* translators: 1: RSS feed URL. 2: XML parser error message. */ esc_html__( 'The RSS feed %1$s is not valid XML (%2$s). Make sure the URL points to the feed itself, for example https://example.com/feed, and not to a regular page.', 'ai-content-writer' ), esc_url( $feed_link ), esc_html( '' !== $xml_message ? $xml_message : __( 'no parser details available', 'ai-content-writer' ) ) ), 'error', $campaign_id ); return false; } libxml_clear_errors(); libxml_use_internal_errors( $previous_libxml_state ); if ( ! isset( $rss->channel->item ) || 0 === count( $rss->channel->item ) ) { ai_content_writer()->create_log( sprintf( /* translators: %s: RSS feed URL. */ esc_html__( 'The RSS feed %s contains no items. Only RSS feeds with entries are supported; Atom feeds are not.', 'ai-content-writer' ), esc_url( $feed_link ) ), 'error', $campaign_id ); return false; } $count = 0; foreach ( $rss->channel->item as $item ) { $title = wp_strip_all_tags( (string) $item->title ); $link = esc_url( (string) $item->link ); if ( empty( $title ) || empty( $link ) ) { continue; } // Check if the title already exists. $exists_post_ids = aicw_get_posts_by_title( $title ); $should_skip = false; foreach ( $exists_post_ids as $post_id ) { if ( get_post_meta( $post_id, '_aicw_content_source_link', true ) === $link ) { $should_skip = true; break; } } if ( $should_skip ) { continue; } // Create a new post. $post_id = wp_insert_post( array( 'post_type' => 'aicw_post', 'post_title' => $title, 'post_content' => '', 'post_status' => 'pending', 'post_author' => get_post_field( 'post_author', $campaign_id ), ) ); if ( is_wp_error( $post_id ) ) { continue; } // Add the post meta. update_post_meta( $post_id, '_aicw_campaign_id', $campaign_id ); update_post_meta( $post_id, '_aicw_content_source_link', $link ); // Update the counter. ++$count; // If the counter is greater than or equal to the target then break the loop. if ( $count >= $posts_needed ) { break; } } // Update the last run time. aicw_update_last_run_time( $campaign_id ); // Logged after the loop so a partial run (fewer feed items than the target) // is reported too, not only a run that hit the target exactly. if ( $count > 0 ) { ai_content_writer()->create_log( sprintf( /* translators: %d: Number of posts generated. */ esc_html__( '%d post titles and source links generated from the RSS feed.', 'ai-content-writer' ), absint( $count ) ), 'success', $campaign_id ); return true; } // The feed parsed fine but produced nothing new — normally every item has // already been imported. Logged so the campaign does not look silently broken. ai_content_writer()->create_log( sprintf( /* translators: %s: RSS feed URL. */ esc_html__( 'No new items found in the RSS feed %s. Every item has already been imported.', 'ai-content-writer' ), esc_url( $feed_link ) ), 'info', $campaign_id ); return false; } /** * Generate content. * This function is responsible for generating content based on the source link. * * @param int $post_id The ID of the post for which content is being generated. * @param string $title The title of the post. * @param string $keyword The keyword that will be used to generate the content. * * @since 1.7.0 * @return bool Whether the content was generated successfully or not. */ public static function generate_content( $post_id, $title = '', $keyword = '' ) { if ( empty( $post_id ) ) { return false; } // Get the source link from the post meta. $source_link = get_post_meta( $post_id, '_aicw_content_source_link', true ); $campaign_id = get_post_meta( $post_id, '_aicw_campaign_id', true ); if ( empty( $source_link ) ) { ai_content_writer()->create_log( sprintf( /* translators: %d: Post ID. */ esc_html__( 'Post ID %d has no source link, so its content cannot be fetched from the feed.', 'ai-content-writer' ), absint( $post_id ) ), 'error', $campaign_id ); return false; } // Get the content from the source link. $dom = new HtmlWeb(); $html = $dom->load( trailingslashit( $source_link ) ); if ( ! $html ) { ai_content_writer()->create_log( sprintf( /* translators: 1: Source article URL. 2: Post ID. */ esc_html__( 'Could not fetch the source article %1$s for post ID %2$d. The page may be unreachable or blocking requests.', 'ai-content-writer' ), esc_url( $source_link ), absint( $post_id ) ), 'error', $campaign_id ); return false; } // SEO Metadata schema. $seo_data = array( 'description' => '', 'keywords' => $keyword, ); $meta_desc = $html->find( 'meta[name=description]', 0 ); if ( ! empty( $meta_desc ) ) { $seo_data['description'] = trim( $meta_desc->content ); } $meta_keywords = $html->find( 'meta[name=keywords]', 0 ); if ( ! empty( $meta_keywords ) ) { $seo_data['keywords'] = trim( $meta_keywords->content ); } // Get the post title, content and error. $result = array( 'title' => '', 'content' => '', ); // Extract title - try different common title selectors. $possible_title_selectors = array( 'h1.entry-title', 'h1.post-title', 'h1.article-title', 'article h1', '.article-header h1', '.post-header h1', 'header h1', 'h1', '.headline', '.title', '#title', ); // Try to find the title using the possible selectors. foreach ( $possible_title_selectors as $selector ) { $title_element = $html->find( $selector, 0 ); if ( $title_element ) { $result['title'] = trim( $title_element->plaintext ); break; } } // If title still not found, use the page title. if ( empty( $result['title'] ) ) { $title_element = $html->find( 'title', 0 ); if ( $title_element ) { $result['title'] = trim( $title_element->plaintext ); } } // Extract content - try different common content containers. $content_elements = null; $possible_content_selectors = array( 'article', '.post-content', '.entry-content', '.article-content', '.content', '#content', '.post-body', '.article-body', '.story-content', '.main-content', 'main', '.post', '#main-content', '.story', '.entry', '.blog-post', '.post-entry', ); // Try to identify the main content container. foreach ( $possible_content_selectors as $selector ) { $element = $html->find( $selector, 0 ); if ( $element ) { $content_elements = $element; break; } } // If no content found with selector approach, use paragraph fallback. if ( empty( (array) $content_elements ) ) { // Find all paragraphs and analyze which ones likely belong to the main content. $all_paragraphs = $html->find( 'p' ); // Filter out small paragraphs that likely aren't article content and paragraphs in common non-content areas like headers, footers, sidebars. $content_paragraphs = array(); foreach ( $all_paragraphs as $p ) { $text = trim( $p->plaintext ); $length = strlen( $text ); // Skip empty paragraphs. if ( $length < 10 ) { continue; } // Skip paragraphs in common non-content areas. $parent = $p->parent(); $parent_tag = $parent ? strtolower( $parent->tag ) : ''; $parent_class = $parent ? strtolower( $parent->class ) : ''; $parent_id = $parent ? strtolower( $parent->id ) : ''; $skip_keywords = array( 'footer', 'header', 'sidebar', 'comment', 'menu', 'nav', 'widget' ); $should_skip = false; foreach ( $skip_keywords as $skip_keyword ) { if ( strpos( $parent_class, $skip_keyword ) !== false || strpos( $parent_id, $skip_keyword ) !== false || strpos( $parent_tag, $skip_keyword ) !== false ) { $should_skip = true; break; } } if ( ! $should_skip ) { $content_paragraphs[] = $text; } } // Join all valid paragraphs. $result['content'] = implode( "\n\n", $content_paragraphs ); } else { // Clean up the content - Remove common non-content elements. foreach ( $content_elements->find( 'aside, .sidebar, .widget, nav, .navigation, .menu, .comment, .comments, .post-meta, .related-posts, .social-share, .share-buttons, .sharing, .author-bio, script, style, iframe, form, .advertisement, .ads, .ad-container' ) as $remove ) { $remove->outertext = ''; $remove->clear(); } /** * Allow cleanup HTML elements from the content. * This will allow users to clean up specific elements using HTML tags, classes, or IDs. * * @param object $content_elements The content elements. * @param int $post_id The post ID. * * @since 2.0.2 * @return object The modified content elements. */ $content_elements = apply_filters( 'aicw_cleanup_content_elements', $content_elements, $post_id ); // Define allowed HTML tags. $allowed_html = array( 'p', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'blockquote' ); // Extract content with HTML structure preserved. $paragraphs = array(); foreach ( $content_elements->find( 'p, h2, h3, h4, h5, h6, ol, ul, blockquote' ) as $node ) { // Get the tag name. $tag_name = strtolower( $node->tag ); // Check if this tag is in our allowed tags list. if ( in_array( $tag_name, $allowed_html, true ) ) { // For list elements, need to preserve structure. if ( 'ol' === $tag_name || 'ul' === $tag_name ) { $list_items = array(); foreach ( $node->find( 'li' ) as $li ) { $li_text = trim( $li->plaintext ); if ( ! empty( $li_text ) ) { $list_items[] = "
  • {$li_text}
  • "; } } if ( ! empty( $list_items ) ) { $list_html = "<{$tag_name}>" . implode( '', $list_items ) . ""; $paragraphs[] = $list_html; } } else { // For other tags like p, h2, etc., just get inner text. $text = trim( $node->plaintext ); if ( ! empty( $text ) ) { $paragraphs[] = "<{$tag_name}>{$text}"; } } } } // Join all elements with proper spacing. $result['content'] = implode( "\n", $paragraphs ); // If no paragraphs found, use the plain text of the content element. if ( empty( $result['content'] ) ) { $result['content'] = trim( $content_elements->plaintext ); } } // Clean the content. $result['content'] = preg_replace( '/\s+/', ' ', $result['content'] ); // Normalize whitespace. $result['content'] = preg_replace( '/\n\s*\n/', "\n\n", $result['content'] ); // Remove extra line breaks. $result['content'] = trim( $result['content'] ); // Clean the title. $result['title'] = preg_replace( '/\s+/', ' ', $result['title'] ); // Normalize whitespace. $result['title'] = trim( $result['title'] ); // Release memory. $html->clear(); unset( $html ); // Check if the content is empty. if ( isset( $result['content'] ) && empty( $result['content'] ) ) { ai_content_writer()->create_log( sprintf( /* translators: 1: Source article URL. 2: Post ID. */ esc_html__( 'No readable article content could be extracted from %1$s for post ID %2$d. The page layout may not be supported.', 'ai-content-writer' ), esc_url( $source_link ), absint( $post_id ) ), 'error', $campaign_id ); return false; } // Update the post content. $updated = wp_update_post( array( 'ID' => $post_id, 'post_title' => wp_strip_all_tags( isset( $result['title'] ) ? $result['title'] : $title ), 'post_content' => wp_kses_post( $result['content'] ), ) ); if ( is_wp_error( $updated ) ) { ai_content_writer()->create_log( sprintf( /* translators: 1: Post ID. 2: Error message. */ esc_html__( 'Could not save the generated content for post ID %1$d: %2$s', 'ai-content-writer' ), absint( $post_id ), esc_html( $updated->get_error_message() ) ), 'error', $campaign_id ); return false; } return true; } }