settings = $settings; $this->api_client = $api_client; $this->debug_history = $debug_history; } /** * Imports remote posts and updates the stored sync cursor. * * @param string $source Source that triggered the sync. * @return array|\WP_Error */ public function sync( $source = 'manual' ) { // Record the run time immediately so it's always set, even if an API // error causes an early return or the queue is empty. $this->settings->update_last_run( time() ); $since = $this->get_since_timestamp(); $seen_ids = array(); // guard against infinite loops if mark-published fails $max_rounds = 100; // safety cap — more than enough for any realistic queue $round = 0; $result = array( 'fetched' => 0, 'created' => 0, 'skipped' => 0, 'failed' => 0, 'errors' => array(), 'last_sync' => $since, ); // Loop until the API queue is empty or the safety cap is hit. // The API returns pending posts one batch at a time; each successful // mark-published removes a post from the queue so the next call returns // the next one(s). do { $posts = $this->api_client->get_posts( $since ); if ( is_wp_error( $posts ) ) { $this->debug_history->add_entry( $source, 'error', $posts->get_error_message(), array( 'requested_since' => $since, 'error_code' => $posts->get_error_code(), 'error_data' => $posts->get_error_data(), ) ); return $posts; } if ( empty( $posts ) ) { break; // Queue is empty — all done. } $result['fetched'] += count( $posts ); $new_this_round = 0; foreach ( $posts as $post_stub ) { if ( ! is_array( $post_stub ) ) { ++$result['failed']; $result['errors'][] = __( 'A post item returned by the API had an unexpected format.', 'ai-blog-poster' ); continue; } $blog_id = $this->extract_blog_id( $post_stub ); if ( empty( $blog_id ) ) { ++$result['failed']; $result['errors'][] = __( 'A post item was missing blog_id.', 'ai-blog-poster' ); continue; } // Already processed in a previous round of this sync session. if ( isset( $seen_ids[ $blog_id ] ) ) { ++$result['skipped']; continue; } if ( $this->find_existing_post_id( $blog_id ) ) { ++$result['skipped']; $seen_ids[ $blog_id ] = true; $this->track_duplicate_sync_cursor( $result, $blog_id, $post_stub ); continue; } $details = $this->api_client->get_post_details( $blog_id ); if ( is_wp_error( $details ) ) { ++$result['failed']; $result['errors'][] = sprintf( /* translators: %s: remote blog ID */ __( 'Failed to load details for blog ID %s.', 'ai-blog-poster' ), $blog_id ); continue; } if ( ! is_array( $details ) || empty( $details ) ) { ++$result['failed']; $result['errors'][] = sprintf( /* translators: %s: remote blog ID */ __( 'Post details for blog ID %s had an unexpected format.', 'ai-blog-poster' ), $blog_id ); continue; } $details_blog_id = $this->extract_blog_id( $details ); if ( ! empty( $details_blog_id ) ) { $blog_id = $details_blog_id; } if ( $this->find_existing_post_id( $blog_id ) ) { ++$result['skipped']; $seen_ids[ $blog_id ] = true; $this->maybe_track_latest_sync( $result, $details ); continue; } $post_id = $this->create_post_from_details( $blog_id, $details ); if ( is_wp_error( $post_id ) ) { ++$result['failed']; $result['errors'][] = $post_id->get_error_message(); continue; } if ( 'publish' === get_post_status( $post_id ) ) { $mark_response = $this->api_client->mark_published( $blog_id, $post_id ); if ( is_wp_error( $mark_response ) ) { $result['errors'][] = sprintf( /* translators: %s: remote blog ID */ __( 'Post created for blog ID %s, but mark-published failed.', 'ai-blog-poster' ), $blog_id ); } } ++$result['created']; ++$new_this_round; $seen_ids[ $blog_id ] = true; $this->maybe_track_latest_sync( $result, $details ); } ++$round; // If nothing new was created or queued in this round, the remaining // posts are all already-imported — stop to avoid an infinite loop. if ( 0 === $new_this_round ) { break; } } while ( $round < $max_rounds ); if ( $result['last_sync'] > 0 ) { $this->settings->update_last_sync( $result['last_sync'] ); } // Always record the wall-clock time of this sync attempt so the admin // UI shows when sync last ran, regardless of whether any posts were imported. $this->settings->update_last_run( time() ); $this->debug_history->add_entry( $source, $result['failed'] > 0 ? 'warning' : 'success', sprintf( /* translators: 1: returned count, 2: created count, 3: duplicate count, 4: failed count */ __( 'API Returned: %1$d, Created: %2$d, Already Imported: %3$d, Failed: %4$d.', 'ai-blog-poster' ), (int) $result['fetched'], (int) $result['created'], (int) $result['skipped'], (int) $result['failed'] ), array( 'requested_since' => $since, 'fetched' => $result['fetched'], 'created' => $result['created'], 'skipped' => $result['skipped'], 'failed' => $result['failed'], 'stored_last_sync' => $result['last_sync'], 'errors' => $result['errors'], ) ); return $result; } private function get_since_timestamp() { $last_sync = absint( $this->settings->get( 'last_sync', 0 ) ); if ( $last_sync > 0 ) { return $last_sync + 1; } return time() - self::INITIAL_SYNC_LOOKBACK; } /** * Advances the sync cursor for duplicate posts using detail timestamps when possible. * * @param array $result Running sync result. * @param string $blog_id Remote blog ID. * @param array $post_stub API list item. */ private function track_duplicate_sync_cursor( array &$result, $blog_id, array $post_stub ) { $details = $this->api_client->get_post_details( $blog_id ); if ( is_array( $details ) && ! empty( $details ) ) { $this->maybe_track_latest_sync( $result, $details ); return; } $this->maybe_track_latest_sync( $result, $post_stub ); } private function extract_blog_id( array $payload ) { if ( isset( $payload['blog_id'] ) && is_numeric( $payload['blog_id'] ) ) { return (string) absint( $payload['blog_id'] ); } if ( isset( $payload['id'] ) && is_numeric( $payload['id'] ) ) { return (string) absint( $payload['id'] ); } return ''; } private function find_existing_post_id( $blog_id ) { $remote_id = absint( $blog_id ); if ( $remote_id <= 0 ) { return 0; } $posts = get_posts( array( 'post_type' => 'post', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => self::REMOTE_ID_META_KEY, 'meta_value' => (string) $remote_id, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); return empty( $posts ) ? 0 : (int) $posts[0]; } private function create_post_from_details( $blog_id, array $details ) { $postarr = array( 'post_type' => 'post', 'post_status' => $this->map_post_status( $details ), 'post_title' => $this->get_detail_value( $details, array( 'title', 'blog_title', 'topic' ) ), 'post_content' => $this->build_post_content( $details ), 'post_excerpt' => $this->get_detail_value( $details, array( 'excerpt' ) ), 'post_name' => sanitize_title( $this->get_detail_value( $details, array( 'slug', 'blog_title', 'topic' ) ) ), ); $default_author_id = absint( $this->settings->get( 'default_author_id', 0 ) ); if ( $default_author_id > 0 ) { $postarr['post_author'] = $default_author_id; } $post_date = $this->get_detail_value( $details, array( 'published_at', 'scheduled_at', 'date', 'created_at' ) ); if ( ! empty( $post_date ) ) { $timestamp = strtotime( $post_date ); if ( false !== $timestamp ) { $postarr['post_date'] = gmdate( 'Y-m-d H:i:s', $timestamp ); } } if ( empty( $postarr['post_title'] ) ) { return new \WP_Error( 'aibp_missing_title', __( 'Remote post details are missing a title.', 'ai-blog-poster' ) ); } $post_id = wp_insert_post( wp_slash( $postarr ), true ); if ( is_wp_error( $post_id ) ) { return $post_id; } update_post_meta( $post_id, self::REMOTE_ID_META_KEY, absint( $blog_id ) ); $this->assign_terms( $post_id, $details ); $this->assign_featured_image( $post_id, $details ); $this->apply_seo_meta( $post_id, $details ); return $post_id; } private function build_post_content( array $details ) { $sections = array(); $anchors = array(); $raw_content = $this->get_detail_value( $details, array( 'content', 'body' ) ); // Decode HTML entities in case the API returns entity-encoded HTML. $decoded = html_entity_decode( $raw_content, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); if ( $this->is_html_content( $decoded ) ) { $decoded = $this->ensure_heading_anchors( $decoded, $anchors ); $body = wp_kses_post( $decoded ); } else { $body = $this->format_body_content( $raw_content, $anchors ); } $toc_html = $this->build_table_of_contents_html( isset( $details['toc'] ) ? $details['toc'] : array(), $anchors ); if ( '' !== $toc_html ) { $sections[] = $toc_html; } if ( '' !== $body ) { $sections[] = $body; } $conclusion_html = $this->build_conclusion_html( $this->get_detail_value( $details, array( 'conclusion' ) ) ); if ( '' !== $conclusion_html ) { $sections[] = $conclusion_html; } $faq_html = $this->build_faq_html( isset( $details['faqs'] ) ? $details['faqs'] : array() ); if ( '' !== $faq_html ) { $sections[] = $faq_html; } return implode( "\n\n", array_filter( $sections ) ); } private function format_body_content( $body, array &$anchors ) { $body = trim( str_replace( array( "\r\n", "\r" ), "\n", $body ) ); if ( '' === $body ) { return ''; } $lines = explode( "\n", $body ); $html_parts = array(); $list_type = ''; $paragraph = array(); $used_ids = array(); foreach ( $lines as $line ) { $trimmed = trim( $line ); if ( '' === $trimmed ) { $this->flush_paragraph_buffer( $paragraph, $html_parts ); $this->close_list_buffer( $list_type, $html_parts ); continue; } if ( preg_match( '/^(#{1,6})\s+(.+)$/', $trimmed, $matches ) ) { $this->flush_paragraph_buffer( $paragraph, $html_parts ); $this->close_list_buffer( $list_type, $html_parts ); $level = min( 6, strlen( $matches[1] ) ); $heading_text = trim( $matches[2] ); $anchor_id = $this->register_anchor( $heading_text, $anchors, $used_ids ); $html_parts[] = sprintf( '%s', $level, esc_attr( $anchor_id ), $this->format_inline_text( $heading_text ), $level ); continue; } if ( preg_match( '/^\*\*(.+?)\*\*:?\s*$/', $trimmed, $matches ) ) { $this->flush_paragraph_buffer( $paragraph, $html_parts ); $this->close_list_buffer( $list_type, $html_parts ); $heading_text = trim( $matches[1] ); $anchor_id = $this->register_anchor( $heading_text, $anchors, $used_ids ); $html_parts[] = sprintf( '

%s

', esc_attr( $anchor_id ), $this->format_inline_text( $heading_text ) ); continue; } if ( preg_match( '/^[-*]\s+(.+)$/', $trimmed, $matches ) ) { $this->flush_paragraph_buffer( $paragraph, $html_parts ); if ( 'ul' !== $list_type ) { $this->close_list_buffer( $list_type, $html_parts ); $html_parts[] = '
    '; $list_type = 'ul'; } $html_parts[] = '
  • ' . $this->format_inline_text( $matches[1] ) . '
  • '; continue; } if ( preg_match( '/^\d+\.\s+(.+)$/', $trimmed, $matches ) ) { $this->flush_paragraph_buffer( $paragraph, $html_parts ); if ( 'ol' !== $list_type ) { $this->close_list_buffer( $list_type, $html_parts ); $html_parts[] = '
      '; $list_type = 'ol'; } $html_parts[] = '
    1. ' . $this->format_inline_text( $matches[1] ) . '
    2. '; continue; } $this->close_list_buffer( $list_type, $html_parts ); $paragraph[] = $trimmed; } $this->flush_paragraph_buffer( $paragraph, $html_parts ); $this->close_list_buffer( $list_type, $html_parts ); return implode( "\n", $html_parts ); } /** * Returns true when the content string contains HTML tags. * * @param string $content Raw content from the API. * @return bool */ private function is_html_content( $content ) { return $content !== wp_strip_all_tags( $content ); } /** * Ensures every heading in an HTML string has an id attribute, adding one * where missing, and populates the anchor map so the TOC can link to them. * * @param string $html HTML content. * @param array $anchors Anchor map to populate. * @return string HTML with id attributes added to headings as needed. */ private function ensure_heading_anchors( $html, array &$anchors ) { $used_ids = array(); $result = preg_replace_callback( '/]*)>(.*?)<\/h[1-6]>/is', function ( $matches ) use ( &$anchors, &$used_ids ) { $level = $matches[1]; $attrs = $matches[2]; $inner = $matches[3]; $text = wp_strip_all_tags( $inner ); if ( preg_match( '/\bid=["\']([^"\']+)["\']/i', $attrs, $id_match ) ) { $anchor_id = $id_match[1]; $used_ids[] = $anchor_id; } else { $anchor_id = $this->generate_anchor_id( $text, $used_ids ); $attrs .= ' id="' . esc_attr( $anchor_id ) . '"'; } $keys = array( $text, rtrim( $text, ':' ), $this->normalize_toc_lookup_key( $text ), $this->normalize_toc_lookup_key( rtrim( $text, ':' ) ), ); foreach ( array_unique( array_filter( $keys ) ) as $key ) { $anchors[ $key ] = esc_attr( $anchor_id ); } return '' . $inner . ''; }, $html ); return null === $result ? $html : $result; } /** * Registers a heading anchor and lookup aliases for TOC matching. * * @param string $heading_text Heading text. * @param array $anchors Registered anchor map. * @param array $used_ids Used anchor IDs. * @return string */ private function register_anchor( $heading_text, array &$anchors, array &$used_ids ) { $anchor_id = $this->generate_anchor_id( $heading_text, $used_ids ); $keys = array( $heading_text, rtrim( $heading_text, ':' ), $this->normalize_toc_lookup_key( $heading_text ), $this->normalize_toc_lookup_key( rtrim( $heading_text, ':' ) ), ); foreach ( array_unique( array_filter( $keys ) ) as $key ) { $anchors[ $key ] = $anchor_id; } return $anchor_id; } private function flush_paragraph_buffer( array &$paragraph, array &$html_parts ) { if ( empty( $paragraph ) ) { return; } $html_parts[] = '

      ' . $this->format_inline_text( implode( ' ', $paragraph ) ) . '

      '; $paragraph = array(); } private function close_list_buffer( &$list_type, array &$html_parts ) { if ( '' === $list_type ) { return; } $html_parts[] = sprintf( '', $list_type ); $list_type = ''; } private function generate_anchor_id( $text, array &$used_ids ) { $base = sanitize_title( $text ); if ( '' === $base ) { $base = 'section'; } $id = $base; $counter = 2; while ( in_array( $id, $used_ids, true ) ) { $id = $base . '-' . $counter; ++$counter; } $used_ids[] = $id; return $id; } private function format_inline_text( $text ) { $text = esc_html( trim( $text ) ); $text = preg_replace( '/\*\*(.+?)\*\*/', '$1', $text ); $text = preg_replace( '/`([^`]+)`/', '$1', $text ); return nl2br( $text, false ); } private function build_table_of_contents_html( $items, array $anchors ) { if ( ! is_array( $items ) || empty( $items ) ) { return ''; } $list_items = array(); foreach ( $items as $item ) { if ( ! is_scalar( $item ) ) { continue; } $text = sanitize_text_field( (string) $item ); if ( '' === $text ) { continue; } $anchor = $this->find_matching_anchor( $text, $anchors ); if ( '' === $anchor ) { $anchor = sanitize_title( $text ); } $list_items[] = '
    3. ' . esc_html( $text ) . '
    4. '; } if ( empty( $list_items ) ) { return ''; } return '

      ' . esc_html__( 'Table of Contents', 'ai-blog-poster' ) . '

        ' . implode( '', $list_items ) . '
      '; } /** * Matches a TOC label to a registered heading anchor. * * @param string $text TOC item text. * @param array $anchors Registered anchors. * @return string */ private function find_matching_anchor( $text, array $anchors ) { $candidates = array( $text, rtrim( $text, ':' ), $this->normalize_toc_lookup_key( $text ), $this->normalize_toc_lookup_key( rtrim( $text, ':' ) ), ); foreach ( array_unique( array_filter( $candidates ) ) as $candidate ) { if ( isset( $anchors[ $candidate ] ) ) { return $anchors[ $candidate ]; } } return ''; } /** * Normalizes heading text for more reliable TOC lookup. * * @param string $text Heading or TOC text. * @return string */ private function normalize_toc_lookup_key( $text ) { $text = strtolower( trim( wp_strip_all_tags( (string) $text ) ) ); $text = preg_replace( '/[^\p{L}\p{N}\s]/u', ' ', $text ); $text = preg_replace( '/\s+/', ' ', $text ); return trim( $text ); } private function build_conclusion_html( $conclusion ) { $conclusion = wp_kses_post( $conclusion ); if ( '' === trim( wp_strip_all_tags( $conclusion ) ) ) { return ''; } return '

      ' . esc_html__( 'Conclusion', 'ai-blog-poster' ) . '

      ' . $conclusion . '

      '; } private function build_faq_html( $items ) { if ( ! is_array( $items ) || empty( $items ) ) { return ''; } $faq_items = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $question = isset( $item['question'] ) ? sanitize_text_field( (string) $item['question'] ) : ''; $answer = isset( $item['answer'] ) ? wp_kses_post( (string) $item['answer'] ) : ''; if ( '' === $question || '' === trim( wp_strip_all_tags( $answer ) ) ) { continue; } $faq_items[] = '

      ' . esc_html( $question ) . '

      ' . $answer . '

      '; } if ( empty( $faq_items ) ) { return ''; } return '

      ' . esc_html__( 'FAQs', 'ai-blog-poster' ) . '

      ' . implode( '', $faq_items ); } private function map_post_status( array $details ) { $status = sanitize_key( $this->settings->get( 'default_post_status', 'publish' ) ); if ( in_array( $status, array( 'publish', 'draft', 'pending', 'private' ), true ) ) { return $status; } return 'publish'; } private function assign_terms( $post_id, array $details ) { $category_ids = $this->upsert_terms( $this->normalize_terms( isset( $details['categories'] ) ? $details['categories'] : array() ), 'category' ); $tag_ids = $this->upsert_terms( $this->normalize_terms( isset( $details['tags'] ) ? $details['tags'] : array() ), 'post_tag' ); $default_category_id = absint( $this->settings->get( 'default_category_id', 0 ) ); if ( $default_category_id > 0 ) { $category_ids[] = $default_category_id; $category_ids = array_values( array_unique( array_filter( $category_ids ) ) ); } if ( ! empty( $category_ids ) ) { wp_set_post_terms( $post_id, $category_ids, 'category', false ); } if ( ! empty( $tag_ids ) ) { wp_set_post_terms( $post_id, $tag_ids, 'post_tag', false ); } } private function normalize_terms( $items ) { if ( is_string( $items ) ) { $items = array_map( 'trim', explode( ',', $items ) ); } return is_array( $items ) ? $items : array(); } private function upsert_terms( $items, $taxonomy ) { if ( ! is_array( $items ) ) { return array(); } $term_ids = array(); foreach ( $items as $item ) { $name = ''; if ( is_string( $item ) ) { $name = $item; } elseif ( is_array( $item ) && ! empty( $item['name'] ) ) { $name = $item['name']; } $name = sanitize_text_field( $name ); if ( '' === $name ) { continue; } $term = term_exists( $name, $taxonomy ); if ( ! $term ) { $term = wp_insert_term( $name, $taxonomy ); } if ( is_wp_error( $term ) ) { continue; } $term_ids[] = (int) ( is_array( $term ) ? $term['term_id'] : $term ); } return array_values( array_unique( array_filter( $term_ids ) ) ); } private function assign_featured_image( $post_id, array $details ) { $image_url = ''; if ( ! empty( $details['featured_image'] ) && is_string( $details['featured_image'] ) ) { $image_url = $details['featured_image']; } elseif ( ! empty( $details['featured_image_url'] ) && is_string( $details['featured_image_url'] ) ) { $image_url = $details['featured_image_url']; } if ( empty( $image_url ) ) { return; } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $attachment_id = media_sideload_image( esc_url_raw( $image_url ), $post_id, null, 'id' ); if ( ! is_wp_error( $attachment_id ) ) { set_post_thumbnail( $post_id, (int) $attachment_id ); } } private function apply_seo_meta( $post_id, array $details ) { $title = sanitize_text_field( $this->get_detail_value( $details, array( 'meta_title' ) ) ); $description = sanitize_textarea_field( $this->get_detail_value( $details, array( 'meta_description' ) ) ); $focus = $this->extract_focus_keyword( $details ); if ( ! empty( $details['seo'] ) && is_array( $details['seo'] ) ) { if ( empty( $title ) && ! empty( $details['seo']['title'] ) ) { $title = sanitize_text_field( $details['seo']['title'] ); } if ( empty( $description ) && ! empty( $details['seo']['description'] ) ) { $description = sanitize_textarea_field( $details['seo']['description'] ); } if ( empty( $focus ) ) { $focus = $this->extract_focus_keyword( $details['seo'] ); } } if ( ! empty( $title ) ) { update_post_meta( $post_id, 'rank_math_title', $title ); update_post_meta( $post_id, '_yoast_wpseo_title', $title ); update_post_meta( $post_id, '_seopress_titles_title', $title ); } if ( ! empty( $description ) ) { update_post_meta( $post_id, 'rank_math_description', $description ); update_post_meta( $post_id, '_yoast_wpseo_metadesc', $description ); update_post_meta( $post_id, '_seopress_titles_desc', $description ); } if ( ! empty( $focus ) ) { update_post_meta( $post_id, 'rank_math_focus_keyword', $focus ); update_post_meta( $post_id, '_yoast_wpseo_focuskw', $focus ); update_post_meta( $post_id, '_seopress_analysis_target_kw', $focus ); } // All in One SEO v4+ stores data in its own table, not post meta. $this->update_aioseo_data( $post_id, $title, $description, $focus ); } /** * Writes title, description, and focus keyphrase to the AIOSEO v4+ custom * table (wp_aioseo_posts). Falls back gracefully if the table does not exist * (i.e. AIOSEO is not installed). * * @param int $post_id Post ID. * @param string $title SEO title. * @param string $description Meta description. * @param string $focus Focus keyword. */ private function update_aioseo_data( $post_id, $title, $description, $focus ) { global $wpdb; $table = $wpdb->prefix . 'aioseo_posts'; // Bail if AIOSEO is not installed. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table}'" ) !== $table ) { return; } $now = current_time( 'mysql' ); // Detect whether the table uses 'created'/'updated' (AIOSEO v4) // or 'created_at'/'updated_at' (older schema), so we don't insert // columns that don't exist and silently fail. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $columns_raw = $wpdb->get_results( "SHOW COLUMNS FROM `{$table}`", ARRAY_A ); $col_names = array(); if ( is_array( $columns_raw ) ) { foreach ( $columns_raw as $col ) { if ( isset( $col['Field'] ) ) { $col_names[] = $col['Field']; } } } $ts_updated = in_array( 'updated', $col_names, true ) ? 'updated' : 'updated_at'; $ts_created = in_array( 'created', $col_names, true ) ? 'created' : 'created_at'; // Build keyphrases JSON, preserving any existing analysis data. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $existing_keyphrases = $wpdb->get_var( $wpdb->prepare( "SELECT keyphrases FROM `{$table}` WHERE post_id = %d", $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); $keyphrases = array( 'focus' => array( 'keyphrase' => '' ), 'additional' => array() ); if ( ! empty( $existing_keyphrases ) ) { $decoded = json_decode( $existing_keyphrases, true ); if ( is_array( $decoded ) ) { $keyphrases = $decoded; } } if ( ! empty( $focus ) ) { if ( empty( $keyphrases['focus'] ) || ! is_array( $keyphrases['focus'] ) ) { $keyphrases['focus'] = array(); } $keyphrases['focus']['keyphrase'] = $focus; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $row_exists = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM `{$table}` WHERE post_id = %d", $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); $data = array( 'keyphrases' => wp_json_encode( $keyphrases ), $ts_updated => $now, ); if ( ! empty( $title ) ) { $data['title'] = $title; } if ( ! empty( $description ) ) { $data['description'] = $description; } if ( $row_exists ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, $data, array( 'post_id' => $post_id ) ); } else { $data['post_id'] = $post_id; $data[ $ts_created ] = $now; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $table, $data ); } } /** * Extracts a focus keyword from several possible API field names. * * @param array $details API detail payload. * @return string */ private function extract_focus_keyword( array $details ) { $value = $this->get_detail_value( $details, array( 'focus_keyword', 'focus_keywords', 'focus_keyphrase', 'keyword', 'keywords', 'meta_keyword', 'meta_keywords', 'seo_keyword', ) ); if ( '' === $value && isset( $details['keywords'] ) && is_array( $details['keywords'] ) ) { $keywords = array(); foreach ( $details['keywords'] as $keyword ) { if ( is_scalar( $keyword ) ) { $keywords[] = sanitize_text_field( (string) $keyword ); } } $value = implode( ', ', array_filter( $keywords ) ); } return sanitize_text_field( $value ); } private function get_detail_value( array $details, array $keys ) { foreach ( $keys as $key ) { if ( isset( $details[ $key ] ) && is_scalar( $details[ $key ] ) ) { return (string) $details[ $key ]; } } return ''; } private function maybe_track_latest_sync( array &$result, array $payload ) { $candidates = array(); foreach ( array( 'timestamp', 'published_at', 'updated_at', 'scheduled_at', 'created_at', 'date' ) as $key ) { if ( empty( $payload[ $key ] ) ) { continue; } if ( is_numeric( $payload[ $key ] ) ) { $candidates[] = (int) $payload[ $key ]; continue; } $parsed = strtotime( $payload[ $key ] ); if ( false !== $parsed ) { $candidates[] = $parsed; } } if ( ! empty( $candidates ) ) { $result['last_sync'] = max( $result['last_sync'], max( $candidates ) ); } } }