tag with specific attributes. $allowed_html = array( 'img' => array( 'src' => array(), 'alt' => array(), 'class' => array(), ), ); // Construct the tag. $img_tag = ''; return wp_kses( $img_tag, $allowed_html ); } /** * Convert OpenAI Markdown to HTML. * * @param string $content The OpenAI Markdown content. * @param string $title The title of the content. * * @since 1.0.0 * @package AIContentWriter * @return array The HTML content and title. */ function aicw_convert_openai_markdown_to_html( $content, $title = '' ) { $content = (string) $content; // Some models (notably Gemini) open with a standalone bold line instead of // an `# ` heading. When no H1 exists, treat that line as the title. if ( ! preg_match( '/^# .+/m', $content ) && preg_match( '/^\s*\*\*([^*\n]+)\*\*[ \t]*\r?\n/', $content, $bold_title ) ) { $title = trim( $bold_title[1] ); $content = substr( $content, strlen( $bold_title[0] ) ); } // Replace headers. $content = preg_replace( '/###### (.+)/', '
$1
', $content ); $content = preg_replace( '/##### (.+)/', '
$1
', $content ); $content = preg_replace( '/#### (.+)/', '

$1

', $content ); $content = preg_replace( '/### (.+)/', '

$1

', $content ); $content = preg_replace( '/## (.+)/', '

$1

', $content ); // Replace bold text. $content = preg_replace( '/\*\*(.+?)\*\*/', '$1', $content ); // Convert unordered list blocks (consecutive lines starting with * or -). $content = preg_replace_callback( '/(?:^|\n)((?:[ \t]*[*\-][ \t]+.+(?:\n|$))+)/', function ( $m ) { $items = preg_split( '/\r?\n/', trim( $m[1] ) ); $html = ''; return "\n" . $html . "\n"; }, $content ); // Convert ordered list blocks (consecutive lines starting with "1." style). $content = preg_replace_callback( '/(?:^|\n)((?:[ \t]*\d+\.[ \t]+.+(?:\n|$))+)/', function ( $m ) { $items = preg_split( '/\r?\n/', trim( $m[1] ) ); $html = '
    '; foreach ( $items as $item ) { $item = preg_replace( '/^[ \t]*\d+\.[ \t]+/', '', $item ); $html .= '
  1. ' . trim( $item ) . '
  2. '; } $html .= '
'; return "\n" . $html . "\n"; }, $content ); // Extract title if present. preg_match( '/^# (.+)/m', $content, $matches ); $title = $matches[1] ?? $title; // Remove title from content. $content = preg_replace( '/^# .+\n*/', '', $content ); return array( 'title' => $title, 'content' => $content, ); } /** * Get the JSON response instructions appended to article prompts. * * Every AI article response is requested as one JSON document so post data * (title, body, excerpt, taxonomy suggestions, SEO fields) can be handled * deterministically instead of scraped out of loose markdown. * * @since 2.3.0 * @return string The JSON contract instructions. */ function aicw_get_json_prompt_instructions() { $instructions = 'Respond ONLY with a single valid JSON object using exactly this schema, with no extra commentary and no markdown code fences: { "title": "Post title", "content": "Markdown body with ## headings, lists, bold, links", "excerpt": "Short 1-2 sentence summary of the article", "categories": ["Suggested category name"], "tags": ["tag1", "tag2", "tag3"], "seo": { "meta_title": "SEO title, maximum 60 characters", "meta_description": "SEO meta description, maximum 160 characters", "focus_keyword": "primary keyword phrase", "slug": "url-friendly-slug" } } The "content" value must contain the full article body in markdown (do not repeat the title as a heading inside it).'; /** * Filter the JSON response instructions appended to article prompts. * * Pro/add-on plugins can extend the schema with additional keys (e.g. an * `faq` array) and pick them up via the `aicw_parsed_generated_post` filter. * * @param string $instructions The JSON contract instructions. * * @since 2.4.0 */ return apply_filters( 'aicw_json_prompt_instructions', $instructions ); } /** * Parse and validate a structured JSON article response from an AI provider. * * @param string $raw The raw model output (JSON, possibly fenced). * @param string $fallback_title Title to fall back to when the response lacks one. * * @since 2.3.0 * @return array|null Normalized post data (title, content [HTML], excerpt, * categories, tags, seo{meta_title, meta_description, * focus_keyword, slug}), or null when the response is not * valid JSON — callers should fall back to markdown parsing. */ function aicw_parse_generated_post( $raw, $fallback_title = '' ) { $raw = trim( (string) $raw ); // Strip a wrapping code fence some models still add around JSON. if ( preg_match( '/^```[a-zA-Z0-9_-]*\s*\n(.*)\n```$/s', $raw, $fence ) ) { $raw = trim( $fence[1] ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) || empty( $data['content'] ) || ! is_string( $data['content'] ) ) { return null; } // The body arrives as markdown — convert through the shared converter. $converted = aicw_convert_openai_markdown_to_html( $data['content'], $fallback_title ); $title = ! empty( $data['title'] ) && is_string( $data['title'] ) ? $data['title'] : $converted['title']; if ( empty( $title ) ) { $title = $fallback_title; } $sanitize_list = static function ( $value ) { if ( ! is_array( $value ) ) { return array(); } return array_values( array_filter( array_map( 'sanitize_text_field', array_filter( $value, 'is_string' ) ) ) ); }; $seo = isset( $data['seo'] ) && is_array( $data['seo'] ) ? $data['seo'] : array(); $get_seo = static function ( $key ) use ( $seo ) { return isset( $seo[ $key ] ) && is_string( $seo[ $key ] ) ? sanitize_text_field( $seo[ $key ] ) : ''; }; $parsed = array( 'title' => sanitize_text_field( $title ), 'content' => $converted['content'], 'excerpt' => isset( $data['excerpt'] ) && is_string( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : '', 'categories' => $sanitize_list( isset( $data['categories'] ) ? $data['categories'] : null ), 'tags' => $sanitize_list( isset( $data['tags'] ) ? $data['tags'] : null ), 'seo' => array( 'meta_title' => mb_substr( $get_seo( 'meta_title' ), 0, 60 ), 'meta_description' => mb_substr( $get_seo( 'meta_description' ), 0, 160 ), 'focus_keyword' => $get_seo( 'focus_keyword' ), 'slug' => sanitize_title( $get_seo( 'slug' ) ), ), ); /** * Filter the normalized post data parsed from an AI JSON response. * * Receives the raw decoded response too, so extensions can pull additional * schema keys they added via `aicw_json_prompt_instructions`. * * @param array $parsed The normalized post data. * @param array $data The raw decoded JSON response. * * @since 2.4.0 */ return apply_filters( 'aicw_parsed_generated_post', $parsed, $data ); } /** * Get campaign. * * @param mixed $data The data. * * @since 1.4.0 * @return WP_Post|false The campaign object, or false if not found. */ function aicw_get_campaign( $data ) { if ( is_numeric( $data ) ) { $data = get_post( $data ); } if ( $data instanceof WP_Post && 'aicw_campaign' === $data->post_type ) { return $data; } return false; } /** * Get campaigns. * * @param array $args The args. * @param bool $count Whether to return a count. * * @since 1.4.0 * @return array|int The campaigns. */ function aicw_get_campaigns( $args = array(), $count = false ) { $defaults = array( 'post_type' => 'aicw_campaign', 'posts_per_page' => - 1, 'orderby' => 'date', 'order' => 'ASC', ); $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); if ( $count ) { return $query->found_posts; } return array_map( 'aicw_get_campaign', $query->posts ); } /** * Get the registered AI providers. * * @since 2.3.0 * @return array Provider slug => class name (or instance) map. */ function aicw_get_ai_providers(): array { $providers = array( 'gemini' => \AIContentWriter\Services\Gemini::class, 'chatgpt' => \AIContentWriter\Services\OpenAI::class, ); /** * Filter the registered AI providers. * * Each entry maps a provider slug to a class name (or instance) implementing * \AIContentWriter\Services\ProviderInterface. Pro/add-on plugins hook here * to register additional providers. * * @param array $providers Provider slug => class/instance map. * * @since 2.3.0 */ return apply_filters( 'aicw_ai_providers', $providers ); } /** * Resolve an AI provider instance by slug. * * @param string $slug Provider slug. `openai` is accepted as a legacy alias of `chatgpt`. * * @since 2.3.0 * @return \AIContentWriter\Services\ProviderInterface|null The provider or null when unknown. */ function aicw_get_ai_provider( $slug ) { $slug = 'openai' === $slug ? 'chatgpt' : (string) $slug; $providers = aicw_get_ai_providers(); if ( empty( $providers[ $slug ] ) ) { return null; } $provider = $providers[ $slug ]; if ( is_string( $provider ) && class_exists( $provider ) ) { $provider = new $provider(); } return $provider instanceof \AIContentWriter\Services\ProviderInterface ? $provider : null; } /** * Get the registered SEO plugin adapters. * * @since 2.4.0 * @return array Adapter slug => class name (or instance) map. */ function aicw_get_seo_adapters(): array { $adapters = array( 'yoast' => \AIContentWriter\Services\SEO\Yoast::class, 'rank-math' => \AIContentWriter\Services\SEO\RankMath::class, 'aioseo' => \AIContentWriter\Services\SEO\AIOSEO::class, 'seopress' => \AIContentWriter\Services\SEO\SEOPress::class, ); /** * Filter the registered SEO plugin adapters. * * Each entry maps a slug to a class name (or instance) implementing * \AIContentWriter\Services\SEO\AdapterInterface. * * @param array $adapters Adapter slug => class/instance map. * * @since 2.4.0 */ return apply_filters( 'aicw_seo_adapters', $adapters ); } /** * Get the adapter for the SEO plugin active on this site, if any. * * When multiple SEO plugins are active (rare), the first registered one wins. * * @since 2.4.0 * @return \AIContentWriter\Services\SEO\AdapterInterface|null */ function aicw_get_active_seo_adapter() { static $resolved = false; static $active = null; if ( $resolved ) { return $active; } $resolved = true; foreach ( aicw_get_seo_adapters() as $adapter ) { if ( is_string( $adapter ) && class_exists( $adapter ) ) { $adapter = new $adapter(); } if ( $adapter instanceof \AIContentWriter\Services\SEO\AdapterInterface && $adapter->is_active() ) { $active = $adapter; break; } } return $active; } /** * Write the stored AI-generated SEO fields of a post into the active SEO plugin. * * No-op when the post has no `_aicw_seo` data or no supported SEO plugin is * active (the data stays in `_aicw_seo` and the frontend fallback covers the * meta description). * * @param int $post_id The post ID. * * @since 2.4.0 * @return bool Whether SEO data was written to an SEO plugin. */ function aicw_apply_seo_meta( $post_id ) { $seo = get_post_meta( $post_id, '_aicw_seo', true ); if ( empty( $seo ) || ! is_array( $seo ) ) { return false; } $adapter = aicw_get_active_seo_adapter(); if ( ! $adapter ) { return false; } $adapter->apply( (int) $post_id, $seo ); return true; } /** * Get the available featured-image sources. * * @since 2.4.0 * @return array Source slug => label map. */ function aicw_get_image_sources(): array { $sources = array( 'pexels' => __( 'Pexels', 'ai-content-writer' ), 'unsplash' => __( 'Unsplash', 'ai-content-writer' ), 'pixabay' => __( 'Pixabay', 'ai-content-writer' ), ); /** * Filter the available featured-image sources. * * Add-ons register additional sources here and serve them through the * `aicw_pre_fetch_images` filter. * * @param array $sources Source slug => label map. * * @since 2.4.0 */ return apply_filters( 'aicw_image_sources', $sources ); } /** * Mask an API key for display (e.g. ••••••••1234). * * @param string $key The API key. * * @since 2.3.0 * @return string The masked key, or an empty string when no key is stored. */ function aicw_mask_api_key( $key ) { $key = (string) $key; if ( '' === $key ) { return ''; } if ( strlen( $key ) <= 4 ) { return str_repeat( '•', strlen( $key ) ); } return str_repeat( '•', 8 ) . substr( $key, -4 ); } /** * Get the supported content languages. * * Keys are the English language names passed to the AI prompt; values are the * translated labels shown in the UI. * * @since 2.3.0 * @return array Language => label map. */ function aicw_get_languages(): array { $languages = array( 'English' => __( 'English', 'ai-content-writer' ), 'Spanish' => __( 'Spanish', 'ai-content-writer' ), 'French' => __( 'French', 'ai-content-writer' ), 'German' => __( 'German', 'ai-content-writer' ), 'Italian' => __( 'Italian', 'ai-content-writer' ), 'Portuguese' => __( 'Portuguese', 'ai-content-writer' ), 'Dutch' => __( 'Dutch', 'ai-content-writer' ), 'Polish' => __( 'Polish', 'ai-content-writer' ), 'Swedish' => __( 'Swedish', 'ai-content-writer' ), 'Greek' => __( 'Greek', 'ai-content-writer' ), 'Russian' => __( 'Russian', 'ai-content-writer' ), 'Ukrainian' => __( 'Ukrainian', 'ai-content-writer' ), 'Turkish' => __( 'Turkish', 'ai-content-writer' ), 'Arabic' => __( 'Arabic', 'ai-content-writer' ), 'Hebrew' => __( 'Hebrew', 'ai-content-writer' ), 'Hindi' => __( 'Hindi', 'ai-content-writer' ), 'Bengali' => __( 'Bengali', 'ai-content-writer' ), 'Indonesian' => __( 'Indonesian', 'ai-content-writer' ), 'Vietnamese' => __( 'Vietnamese', 'ai-content-writer' ), 'Thai' => __( 'Thai', 'ai-content-writer' ), 'Japanese' => __( 'Japanese', 'ai-content-writer' ), 'Korean' => __( 'Korean', 'ai-content-writer' ), 'Chinese' => __( 'Chinese (Simplified)', 'ai-content-writer' ), ); /** * Filter the supported content languages. * * @param array $languages Language => label map. * * @since 2.3.0 */ return apply_filters( 'aicw_languages', $languages ); } /** * Get the writing-tone presets. * * Keys are the tone names passed to the AI prompt; values are the translated * labels shown in the UI. An empty key means "no specific tone". * * @since 2.3.0 * @return array Tone => label map. */ function aicw_get_content_tones(): array { $tones = array( '' => __( 'Default', 'ai-content-writer' ), 'professional' => __( 'Professional', 'ai-content-writer' ), 'conversational' => __( 'Conversational', 'ai-content-writer' ), 'friendly' => __( 'Friendly', 'ai-content-writer' ), 'formal' => __( 'Formal', 'ai-content-writer' ), 'casual' => __( 'Casual', 'ai-content-writer' ), 'informative' => __( 'Informative', 'ai-content-writer' ), 'persuasive' => __( 'Persuasive', 'ai-content-writer' ), 'authoritative' => __( 'Authoritative', 'ai-content-writer' ), 'humorous' => __( 'Humorous', 'ai-content-writer' ), 'technical' => __( 'Technical', 'ai-content-writer' ), ); /** * Filter the writing-tone presets. * * @param array $tones Tone => label map. * * @since 2.3.0 */ return apply_filters( 'aicw_content_tones', $tones ); } /** * Get the content language configured for a campaign. * * @param int $campaign_id The campaign ID. * * @since 2.3.0 * @return string The language name (defaults to English). */ function aicw_get_campaign_language( $campaign_id ) { $language = get_post_meta( $campaign_id, '_aicw_language', true ); if ( empty( $language ) || ! array_key_exists( $language, aicw_get_languages() ) ) { $language = 'English'; } return $language; } /** * Get the writing tone configured for a campaign. * * @param int $campaign_id The campaign ID. * * @since 2.3.0 * @return string The tone key, or an empty string for the default tone. */ function aicw_get_campaign_tone( $campaign_id ) { $tone = get_post_meta( $campaign_id, '_aicw_tone', true ); return array_key_exists( (string) $tone, aicw_get_content_tones() ) ? (string) $tone : ''; } /** * Get campaign titles. * This will generate the titles for the campaign using OpenAI or any other AI service. * * @param string $type The type of the campaign. * @param string $keywords The campaign keywords. * @param int $needed_count The number of titles to generate. * @param array $args Optional. Extra arguments: `language`. * * @since 1.4.0 * @return array The generated titles. */ function aicw_generate_titles( $type, $keywords, $needed_count = 20, $args = array() ) { if ( empty( $type ) || empty( $keywords ) ) { return array(); } // Initialize titles needed count. $needed_count = empty( $needed_count ) ? 20 : ( absint( $needed_count ) + 20 ); $provider = aicw_get_ai_provider( $type ); if ( ! $provider || ! $provider->is_configured() ) { return array(); } $language = ! empty( $args['language'] ) ? (string) $args['language'] : 'English'; $prompt = sprintf( 'You are experienced in writing about %1$s. Generate %2$d SEO friendly article titles in %3$s for these keywords. Return them as a plain numbered list ("1. Title"), one title per line, with no extra commentary.', $keywords, $needed_count, $language ); $text = $provider->generate( $prompt, array( 'max_tokens' => 1000 ) ); if ( is_wp_error( $text ) || empty( $text ) ) { return array(); } return explode( "\n", trim( $text ) ); } /** * Generate content for the campaign. * This will generate the content for the campaign using OpenAI or any other AI service. * * @param int $post_id The ID of the post for which content is being generated. * @param string $title The title of the content. * @param string $keywords The campaign keywords. * * @since 1.4.0 * @return string|mixed The generated content or null. */ function aicw_generate_content( $post_id, $title, $keywords = '' ) { if ( empty( $post_id ) || empty( $title ) ) { return ''; } // Get the associated campaign ID. $campaign_id = get_post_meta( $post_id, '_aicw_campaign_id', true ); $campaign_type = get_post_meta( $campaign_id, '_aicw_campaign_type', true ); $api_model = empty( $campaign_type ) ? get_option( 'aicw_api_model' ) : $campaign_type; // TODO: We will remove this option in the future. It's already remove from the settings page. if ( empty( $api_model ) ) { return null; } $provider = aicw_get_ai_provider( $api_model ); if ( ! $provider || ! $provider->is_configured() ) { return null; } $language = aicw_get_campaign_language( $campaign_id ); $tone = aicw_get_campaign_tone( $campaign_id ); $system_tone = esc_html__( 'You are an expert SEO content writer. Generate factually accurate, engaging, and well-structured articles optimized for readability and search engines.', 'ai-content-writer' ); if ( ! empty( $tone ) ) { $system_tone .= ' ' . sprintf( /* translators: %s: Writing tone. */ esc_html__( 'Write in a %s tone.', 'ai-content-writer' ), $tone ); } $prompt = sprintf( /* translators: 1: Prompt Content, 2: Writing tone, 3: SEO/keyword instructions, 4: Language */ esc_html__( 'Write a well-structured article on %1$s with a %2$s tone. %3$s Ensure the content is engaging, easy to read, and formatted with proper headings (H2, H3), bullet points, and paragraphs. Use %4$s as the language for this article. The article should include: - A catchy introduction that hooks the reader - Informative body sections with subheadings - A compelling conclusion with key takeaways Avoid fluff, keep sentences concise, and ensure factual accuracy. Where applicable, use industry insights, statistics, and real-world examples. If necessary, cite reliable sources to back up claims. Ensure no AI or robotic tone—the content should feel human-written.', 'ai-content-writer' ), '"' . $title . '"', empty( $tone ) ? 'professional' : $tone, empty( $keywords ) ? esc_html__( 'The article should be optimized for SEO', 'ai-content-writer' ) : esc_html__( 'The article should be optimized for SEO, including relevant keywords: ', 'ai-content-writer' ) . '"' . $keywords . '".', $language ); /** * Filter the article-generation prompt for a campaign post. * * Add-ons can replace the default prompt with custom templates (using the * context values for variable substitution). * * @param string $prompt The generated prompt. * @param int $campaign_id The campaign ID. * @param array $context Context: `title`, `keywords`, `language`, `tone`. * * @since 2.4.0 */ $prompt = apply_filters( 'aicw_content_prompt', $prompt, $campaign_id, array( 'title' => $title, 'keywords' => $keywords, 'language' => $language, 'tone' => $tone, ) ); // Request the structured JSON contract so excerpt, taxonomy suggestions, // and SEO fields arrive alongside the article body. $prompt .= "\n\n" . aicw_get_json_prompt_instructions(); $raw = $provider->generate( $prompt, array( 'system' => $system_tone, 'max_tokens' => 5000, 'temperature' => 0.7, 'json' => true, ) ); if ( is_wp_error( $raw ) || empty( $raw ) ) { return null; } $parsed = aicw_parse_generated_post( $raw, $title ); // Fallback: treat the response as plain markdown when it is not valid JSON. if ( null === $parsed ) { $converted = aicw_convert_openai_markdown_to_html( $raw, $title ); return isset( $converted['content'] ) ? $converted['content'] : null; } // Persist the structured extras on the temp post; the caller saves the content. update_post_meta( $post_id, '_aicw_seo', $parsed['seo'] ); update_post_meta( $post_id, '_aicw_suggested_categories', $parsed['categories'] ); update_post_meta( $post_id, '_aicw_suggested_tags', $parsed['tags'] ); if ( ! empty( $parsed['excerpt'] ) ) { wp_update_post( array( 'ID' => $post_id, 'post_excerpt' => $parsed['excerpt'], ) ); } /** * Fires after a structured AI response has been parsed and persisted on a * campaign temp post. Extensions can store their own schema keys here. * * @param int $post_id The temp post ID. * @param array $parsed The normalized post data. * * @since 2.4.0 */ do_action( 'aicw_generated_post_parsed', $post_id, $parsed ); return $parsed['content']; } /** * Get the post-ID by the exact title using a direct $wpdb query. * * @param string $post_title The post-title. * * @since 1.4.0 * @return int|null The post-ID if found, otherwise null. */ function aicw_get_post_by_title( $post_title ) { global $wpdb; // Encode to match how WordPress stores titles in the DB. $post_title = htmlspecialchars( wp_unslash( $post_title ), ENT_QUOTES, 'UTF-8' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $query = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type != 'revision' AND post_status IN ('publish', 'pending', 'draft', 'future', 'private') LIMIT 1", $post_title ) ); if ( $query ) { return (int) $query; } // Duplicate guard: also match by normalized slug so case/punctuation // variants of the same title ("AI Tips!" vs "ai tips") are caught. $slug = sanitize_title( wp_unslash( $post_title ) ); if ( ! empty( $slug ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $query = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type != 'revision' AND post_status IN ('publish', 'pending', 'draft', 'future', 'private') LIMIT 1", $slug ) ); } return $query ? (int) $query : null; } /** * Get all post-IDs by the exact title using a direct $wpdb query. * * @param string $post_title The post-title. * * @since 1.4.0 * @return int[] Array of post-IDs if found, otherwise an empty array. */ function aicw_get_posts_by_title( $post_title ) { global $wpdb; // Encode to match how WordPress stores titles in the DB. $post_title = htmlspecialchars( wp_unslash( $post_title ), ENT_QUOTES, 'UTF-8' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $query = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type != 'revision' AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')", $post_title ) ); return array_map( 'intval', $query ); } /** * Campaign needs to update. * This function checks if the campaign needs to be updated. * * @param int $campaign_id The campaign ID. * * @since 1.4.0 * @return array The array of necessary elements. */ function aicw_campaign_needs_update( $campaign_id ) { if ( empty( $campaign_id ) ) { return array( 'needs_update' => false, 'error' => __( 'The campaign is not found.', 'ai-content-writer' ), ); } // Check is the campaign target reached. $target = absint( get_post_meta( $campaign_id, '_aicw_campaign_target', true ) ); // Get total pending posts for this campaign. $pending_posts = get_posts( array( 'post_type' => 'aicw_post', 'posts_per_page' => -1, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_aicw_campaign_id', 'value' => $campaign_id, ), ), 'post_status' => 'any', 'fields' => 'ids', ) ); // Get total found posts for this campaign that already published. $published_posts = get_posts( array( 'post_type' => get_post_meta( $campaign_id, '_aicw_post_type', true ) ?? 'post', 'posts_per_page' => -1, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_aicw_campaign_id', 'value' => $campaign_id, ), ), 'post_status' => 'any', 'fields' => 'ids', ), ); $total_posts = count( $pending_posts ); $published_posts_count = count( $published_posts ); // Merge the total posts and total published posts. if ( $published_posts_count > 0 ) { $total_posts = $total_posts + $published_posts_count; } // Check if the target is reached. if ( $total_posts >= $target ) { return array( 'needs_update' => false, 'posts_needed' => 0, 'pending_posts' => $pending_posts, 'error' => __( 'The campaign target is reached.', 'ai-content-writer' ), ); } // Check how many posts are needed to reach the target. $posts_needed = absint( $target - $total_posts ); // Set the posts needed to max 20. Forcefully set to 20 to avoid overloading. if ( $posts_needed >= 20 ) { $posts_needed = 20; } return array( 'needs_update' => true, 'posts_needed' => $posts_needed, 'pending_posts' => $pending_posts, 'error' => '', ); } /** * Campaign temporary post needs update. * This function checks if the campaign temporary post needs to be updated. * * @param \WP_Post $post The temporary post. * @param int $campaign_id The campaign ID. * * @since 1.4.0 * @return array The array of necessary elements. */ function aicw_temp_post_needs_update( $post, $campaign_id ) { // Check if the temporary post and campaign are empty or not found. if ( empty( $post ) || empty( $campaign_id ) ) { return array(); } $needs_update = array(); // Check if the temporary post-title is empty or not found. if ( empty( $post->post_title ) ) { $needs_update['title'] = true; } // Check if the temporary post-content is empty or not found. if ( empty( $post->post_content ) ) { $needs_update['content'] = true; } // Check if the campaign needs to generate a thumbnail. $generate_thumb = get_post_meta( $campaign_id, '_aicw_generate_thumbnail', true ); // Check if the temporary post-thumbnail is empty or not found. if ( empty( get_post_thumbnail_id( $post->ID ) ) && $generate_thumb ) { $needs_update['thumbnail'] = true; } return $needs_update; } /** * Update last run time. * This function updates the last run time for the campaigns or temporary posts. * * @param int $post_id The post ID. * * @since 1.4.0 * @return void */ function aicw_update_last_run_time( $post_id ) { // Update the post and indicate that the post has been updated. wp_update_post( array( 'ID' => $post_id, 'post_modified' => current_time( 'mysql' ), 'post_modified_gmt' => current_time( 'mysql', 1 ), ) ); } /** * Get the camping target max limit. * This function gets the campaign target maximum limit. * * @since 1.4.0 * @return int The campaign target limit. */ function aicw_get_campaign_target_max_limit() { $target_limit = apply_filters( 'aicw_campaign_target_max_limit', 20 ); return absint( $target_limit ); } /** * Get the camping frequencies. * This function gets the campaign frequencies. * * @since 2.2.5 * @return array The campaign frequencies. */ function aicw_get_campaign_frequencies(): array { return apply_filters( 'aicw_campaign_frequencies', array( 'hourly' => __( 'Hourly', 'ai-content-writer' ), 'twicedaily' => __( 'Twice Daily', 'ai-content-writer' ), 'daily' => __( 'Daily', 'ai-content-writer' ), 'weekly' => __( 'Weekly', 'ai-content-writer' ), 'monthly' => __( 'Monthly', 'ai-content-writer' ), ) ); } /** * Get user agents. * This function gets the user agents for the campaigns. * * @since 1.7.0 * @return array The user agents. */ function aicw_user_agents(): array { return array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', ); } /** * Render search and replace fields. * * @param array $field Field arguments. * * @since 2.0.2 */ function aicw_search_replace_fields( $field ) { $key = wp_rand( 1, 100000 ); ?> 0 ) { $u .= str_repeat( '=', 4 - $padding ); } // Decode the Base64 string. The base64_decode function is safe here as we are validating the input. $decoded_url = base64_decode( $u, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( false !== $decoded_url ) { return esc_url_raw( $decoded_url ); } } return false; } /** * Render an inline SVG icon from the curated icon set. * * Icons are stroke based (24x24, currentColor) so they inherit the surrounding * text color and stay crisp at any size. * * @param string $name Icon name. * * @since 2.2.7 * @return string SVG markup, or an empty string when the icon is unknown. */ function aicw_get_icon( $name ) { $icons = array( 'check' => '', 'settings' => '', 'arrow-right' => '', 'sparkles' => '', 'rocket' => '', 'help' => '', 'book' => '', 'layers' => '', ); if ( ! isset( $icons[ $name ] ) ) { return ''; } return ''; } /** * Allowed HTML for rendering inline SVG icons with wp_kses(). * * @since 2.2.7 * @return array Allowed HTML tags and attributes for SVG icons. */ function aicw_get_svg_allowed_html() { $attrs = array( 'class' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'aria-hidden' => true, 'focusable' => true, ); return array( 'svg' => $attrs, 'g' => array( 'fill' => true, 'clip-path' => true ), 'path' => array( 'd' => true, 'fill' => true ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ), 'polyline' => array( 'points' => true ), 'polygon' => array( 'points' => true ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true ), ); }