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 = '';
foreach ( $items as $item ) {
$item = preg_replace( '/^[ \t]*[*\-][ \t]+/', '', $item );
$html .= '- ' . trim( $item ) . '
';
}
$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 .= '- ' . trim( $item ) . '
';
}
$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 ),
);
}