aistma_log_manager = new AISTMA_Log_Manager();
add_action( 'admin_enqueue_scripts', array( $this, 'aistma_admin_enqueue_scripts' ) );
add_action( 'admin_footer', array( $this, 'aistma_render_wizard_modals' ) );
add_action( 'admin_menu', array( $this, 'aistma_add_admin_menu' ) );
add_action( 'admin_head-edit.php', array( $this, 'aistma_add_posts_page_button' ) );
add_action( 'wp_ajax_aistma_wizard_generate', array( $this, 'aistma_wizard_generate' ) );
add_action( 'wp_ajax_aistma_wizard_save', array( $this, 'aistma_wizard_save' ) );
add_action( 'wp_ajax_aistma_wizard_cancel', array( $this, 'aistma_wizard_cancel' ) );
add_action( 'wp_ajax_aistma_generate_site_prompt', array( $this, 'aistma_generate_site_prompt' ) );
add_action( 'wp_ajax_aistma_wizard_dismiss', array( $this, 'aistma_wizard_dismiss' ) );
add_action( 'wp_ajax_aistma_confirm_weekly', array( $this, 'aistma_confirm_weekly' ) );
add_action( 'wp_ajax_aistma_submit_rating', array( $this, 'aistma_submit_rating' ) );
add_action( 'wp_ajax_aistma_dismiss_rating', array( $this, 'aistma_dismiss_rating' ) );
add_action( 'wp_ajax_aistma_never_show_rating', array( $this, 'aistma_never_show_rating' ) );
add_action( 'wp_ajax_aistma_ensure_startup_credits', array( $this, 'aistma_ensure_startup_credits' ) );
add_action( 'wp_ajax_aistma_log_wizard_escape', array( $this, 'aistma_log_wizard_escape' ) );
add_action( 'wp_ajax_aistma_mark_wizard_shown_today', array( $this, 'aistma_mark_wizard_shown_today' ) );
// Initialize social media bulk actions
$this->init_social_media_bulk_actions();
}
/**
* Enqueue admin scripts and styles.
*
* @param string $hook_suffix The current admin page.
* @return void
*/
public function aistma_admin_enqueue_scripts( $hook_suffix ) {
// Only load wizard and rating scripts on relevant pages
$load_wizard = false;
$load_rating = false;
// Load wizard and rating modal on posts pages
if ( in_array( $hook_suffix, [ 'edit.php', 'post.php', 'post-new.php' ], true ) ) {
$load_wizard = true;
$load_rating = true;
}
// Load wizard on dashboard (for widget button) but not the rating modal
if ( 'index.php' === $hook_suffix ) {
$load_wizard = true;
}
// Load wizard on plugin settings pages
if ( strpos( $hook_suffix, 'aistma' ) !== false ) {
$load_wizard = true;
}
// Always enqueue admin script (needed for social media nonce across all pages)
wp_enqueue_script(
'aistma-admin-js',
AISTMA_URL . 'admin/js/admin.js',
array( 'jquery' ),
filemtime( AISTMA_PATH . 'admin/js/admin.js' ),
true
);
// Localize script with nonce for AJAX requests
wp_localize_script( 'aistma-admin-js', 'aistmaSocialMedia', array(
'nonce' => wp_create_nonce( 'aistma_social_media_nonce' ),
'ajaxurl' => admin_url( 'admin-ajax.php' )
) );
wp_enqueue_style(
'aistma-admin-css',
AISTMA_URL . 'admin/css/admin.css',
array(),
filemtime( AISTMA_PATH . 'admin/css/admin.css' )
);
// Enqueue wizard scripts and styles (only on relevant pages)
if ( $load_wizard ) {
wp_enqueue_script(
'aistma-wizard-js',
AISTMA_URL . 'admin/js/activation-wizard.js',
array( 'jquery' ),
filemtime( AISTMA_PATH . 'admin/js/activation-wizard.js' ),
true
);
// Localize wizard script with nonces and settings
wp_localize_script( 'aistma-wizard-js', 'aistmaWizardL10n', array(
'showWizard' => AISTMA_Activation_Wizard::maybe_show_wizard() ? '1' : '0',
'generateNonce' => wp_create_nonce( 'aistma_wizard_generate_nonce' ),
'saveNonce' => wp_create_nonce( 'aistma_wizard_save_nonce' ),
'dismissNonce' => wp_create_nonce( 'aistma_wizard_dismiss_nonce' ),
'weeklyNonce' => wp_create_nonce( 'aistma_confirm_weekly_nonce' ),
'startupCreditsNonce' => wp_create_nonce( 'aistma_ensure_startup_credits_nonce' ),
'escapeNonce' => wp_create_nonce( 'aistma_log_wizard_escape_nonce' ),
'showTodayNonce' => wp_create_nonce( 'aistma_mark_wizard_shown_today_nonce' ),
'selectPrompt' => __( 'Please select a prompt first.', 'ai-story-maker' ),
'generateError' => __( 'An error occurred while generating the story. Please try again.', 'ai-story-maker' ),
'saveError' => __( 'An error occurred while saving. Please try again.', 'ai-story-maker' ),
'saveSuccess' => __( 'Story saved successfully!', 'ai-story-maker' ),
'unknownError' => __( 'An unknown error occurred.', 'ai-story-maker' ),
'editPostUrl' => admin_url( 'post.php?action=edit' ),
'postsPageUrl' => admin_url( 'edit.php' ),
'draftSavedNotice' => __( 'Your story draft was saved — find it in Posts → All Posts.', 'ai-story-maker' ),
'viewDrafts' => __( 'View Posts', 'ai-story-maker' ),
'siteContext' => esc_js( AISTMA_Activation_Wizard::build_site_context() ),
'sitePromptNonce' => wp_create_nonce( 'aistma_site_prompt_nonce' ),
'enterSiteDescription' => __( 'Please describe your site first.', 'ai-story-maker' ),
'generatingPrompt' => __( 'Building your prompt…', 'ai-story-maker' ),
'promptReady' => __( 'Your custom prompt is ready — click Generate Post Now on the highlighted card.', 'ai-story-maker' ),
'generatePostNow' => __( 'Generate Post Now', 'ai-story-maker' ),
'redirectAfterSave' => true,
) );
// Enqueue wizard CSS
wp_enqueue_style(
'aistma-wizard-css',
AISTMA_URL . 'admin/css/activation-wizard.css',
array(),
filemtime( AISTMA_PATH . 'admin/css/activation-wizard.css' )
);
}
// Enqueue rating modal CSS and JS (only on posts page)
if ( $load_rating ) {
wp_enqueue_style(
'aistma-rating-css',
AISTMA_URL . 'admin/css/rating-modal.css',
array(),
filemtime( AISTMA_PATH . 'admin/css/rating-modal.css' )
);
wp_enqueue_script(
'aistma-rating-js',
AISTMA_URL . 'admin/js/rating-modal.js',
array( 'jquery' ),
filemtime( AISTMA_PATH . 'admin/js/rating-modal.js' ),
true
);
// Localize rating script with nonces (only when script is enqueued)
wp_localize_script( 'aistma-rating-js', 'aistmaRatingL10n', array(
'submitNonce' => wp_create_nonce( 'aistma_submit_rating_nonce' ),
'dismissNonce' => wp_create_nonce( 'aistma_dismiss_rating_nonce' ),
) );
}
}
/**
* Add the top-level admin menu and main tabbed interface.
*
* @return void
*/
public function aistma_add_admin_menu() {
add_menu_page(
__( 'AI Story Maker Settings', 'ai-story-maker' ),
__( 'AI Story Maker', 'ai-story-maker' ),
'manage_options',
'aistma-settings',
array( $this, 'aistma_render_main_page' ),
'dashicons-welcome-widgets-menus',
9
);
}
/**
* Render the plugin's main settings page with tabs.
*
* @return void
*/
public function aistma_render_main_page() {
$allowed_tabs = array(
self::TAB_WELCOME,
self::TAB_AI_WRITER,
self::TAB_SOCIAL_MEDIA,
self::TAB_SETTINGS,
self::TAB_GENERAL,
self::TAB_PROMPTS,
self::TAB_ANALYTICS,
self::TAB_LOG,
self::TAB_SHORTCODES,
self::TAB_FAQ,
);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab selection only affects UI; no action taken
$active_tab = isset( $_GET['tab'] ) && in_array( sanitize_key( $_GET['tab'] ), $allowed_tabs, true ) ? sanitize_key( $_GET['tab'] ) : self::TAB_WELCOME;
?>
';
echo '' . esc_html__( 'Account Setup Required', 'ai-story-maker' ) . '
';
echo '' . sprintf(
/* translators: %s: pricing page URL */
esc_html__( 'Please activate a plan at %s to start generating stories.', 'ai-story-maker' ),
esc_url( AISTMA_PRICING_URL )
) . '
';
echo '';
} elseif ( $notice === 'prompts_required' ) {
echo '';
echo '
' . esc_html__( 'Prompts Setup Required', 'ai-story-maker' ) . '
';
echo '
' . esc_html__( 'Please create and activate at least one prompt before generating stories.', 'ai-story-maker' ) . '
';
echo '
';
}
}
if ( self::TAB_WELCOME === $active_tab ) {
include_once AISTMA_PATH . 'admin/templates/welcome-tab-template.php';
} elseif ( self::TAB_AI_WRITER === $active_tab ) {
$this->aistma_settings_page = AISTMA_Settings_Page::get_instance();
$this->aistma_settings_page->aistma_subscriptions_page_render();
} elseif ( self::TAB_SOCIAL_MEDIA === $active_tab ) {
include_once AISTMA_PATH . 'admin/templates/social-media-template.php';
} elseif ( self::TAB_SETTINGS === $active_tab ) {
$this->aistma_settings_page = AISTMA_Settings_Page::get_instance();
$this->aistma_settings_page->aistma_settings_page_render();
} elseif ( self::TAB_PROMPTS === $active_tab ) {
$this->aistma_prompt_editor = new AISTMA_Prompt_Editor();
$this->aistma_prompt_editor->aistma_prompt_editor_render();
} elseif ( self::TAB_ANALYTICS === $active_tab ) {
include_once AISTMA_PATH . 'admin/templates/analytics-template.php';
} elseif ( self::TAB_SHORTCODES === $active_tab ) {
include_once AISTMA_PATH . 'admin/templates/shortcodes-tab-template.php';
} elseif ( self::TAB_LOG === $active_tab ) {
$this->aistma_log_manager = new AISTMA_Log_Manager();
$this->aistma_log_manager->aistma_log_table_render();
} elseif ( self::TAB_FAQ === $active_tab ) {
include_once AISTMA_PATH . 'admin/templates/faq-template.php';
}
// Include generation controls on all tabs
include_once AISTMA_PATH . 'admin/templates/generation-controls-template.php';
}
/**
* Add Generate Stories button to the WordPress Posts page.
*
* @return void
*/
public function aistma_add_posts_page_button() {
global $typenow;
// Only add button on the posts list page
if ( $typenow !== 'post' ) {
return;
}
// Only show to users who can edit posts
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
?>
wp_create_nonce( 'generate_story_nonce' ),
'validateNonce' => wp_create_nonce( 'generate_story_nonce' ),
'buttonDisabled' => $button_disabled,
'buttonText' => $button_text,
)
);
?>
array() ) );
if ( empty( $social_media_accounts['accounts'] ) ) {
return $actions;
}
// Add separator
$actions['aistma_separator'] = '--- ' . __( 'Publish to Social Media', 'ai-story-maker' ) . ' ---';
// Add action for each enabled account
foreach ( $social_media_accounts['accounts'] as $account ) {
if ( $account['enabled'] ) {
$action_key = 'aistma_publish_to_' . $account['id'];
$platform_name = ucfirst( $account['platform'] );
$actions[ $action_key ] = sprintf(
/* translators: %1$s: Platform name (e.g., Facebook), %2$s: Account name */
__( 'Publish to %1$s (%2$s)', 'ai-story-maker' ),
$platform_name,
$account['name']
);
}
}
return $actions;
}
/**
* Handle social media bulk actions.
*/
public function handle_social_media_bulk_actions( $redirect_to, $doaction, $post_ids ) {
// Check if this is a social media bulk action
if ( strpos( $doaction, 'aistma_publish_to_' ) !== 0 ) {
return $redirect_to;
}
// Extract account ID from action
$account_id = str_replace( 'aistma_publish_to_', '', $doaction );
// Get the account details
$account = $this->get_social_media_account( $account_id );
if ( ! $account ) {
$this->aistma_log_manager->log( 'error', 'Social media bulk action failed: Account not found (ID: ' . $account_id . ')' );
$redirect_to = add_query_arg( 'aistma_bulk_error', 'account_not_found', $redirect_to );
return $redirect_to;
}
// Validate user permissions
if ( ! current_user_can( 'edit_posts' ) ) {
$this->aistma_log_manager->log( 'error', 'Social media bulk action failed: Insufficient permissions for user ' . get_current_user_id() );
$redirect_to = add_query_arg( 'aistma_bulk_error', 'insufficient_permissions', $redirect_to );
return $redirect_to;
}
$published_count = 0;
$failed_count = 0;
$errors = array();
// Process each selected post
foreach ( $post_ids as $post_id ) {
$post = get_post( $post_id );
if ( ! $post || $post->post_status !== 'publish' ) {
$failed_count++;
/* translators: %d: Post ID number */
$error_msg = sprintf( __( 'Post ID %d is not published', 'ai-story-maker' ), $post_id );
$errors[] = $error_msg;
$this->aistma_log_manager->log( 'error', 'Social media publish failed: ' . $error_msg . ' (Account: ' . $account['name'] . ')' );
continue;
}
// Publish to social media
$result = $this->publish_post_to_social_media( $post, $account );
if ( $result['success'] ) {
$published_count++;
$this->aistma_log_manager->log(
'info',
sprintf(
'Post "%s" (ID: %d) successfully published to %s account "%s"',
$post->post_title,
$post_id,
$account['platform'],
$account['name']
)
);
} else {
$failed_count++;
$error_msg = sprintf(
/* translators: %1$s: Post title, %2$s: Error message */
__( 'Failed to publish post "%1$s": %2$s', 'ai-story-maker' ),
$post->post_title,
$result['message']
);
$errors[] = $error_msg;
$this->aistma_log_manager->log(
'error',
sprintf(
'Failed to publish post "%s" (ID: %d) to %s account "%s": %s',
$post->post_title,
$post_id,
$account['platform'],
$account['name'],
$result['message']
)
);
}
}
// Add results to redirect URL
$redirect_to = add_query_arg( array(
'aistma_bulk_published' => $published_count,
'aistma_bulk_failed' => $failed_count,
'aistma_account_name' => urlencode( $account['name'] ),
'aistma_platform' => $account['platform']
), $redirect_to );
return $redirect_to;
}
/**
* Show bulk action result notices.
*/
public function show_bulk_action_notices() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displaying admin notices only, no actions taken
if ( isset( $_GET['aistma_bulk_published'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displaying admin notices only, no actions taken
$published = intval( $_GET['aistma_bulk_published'] );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displaying admin notices only, no actions taken
$failed = isset( $_GET['aistma_bulk_failed'] ) ? intval( $_GET['aistma_bulk_failed'] ) : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displaying admin notices only, no actions taken
$account_name = isset( $_GET['aistma_account_name'] ) ? urldecode( sanitize_text_field( wp_unslash( $_GET['aistma_account_name'] ) ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displaying admin notices only, no actions taken
$platform = isset( $_GET['aistma_platform'] ) ? sanitize_text_field( wp_unslash( $_GET['aistma_platform'] ) ) : '';
if ( $published > 0 ) {
$message = sprintf(
/* translators: %1$d: Number of posts, %2$s: Platform name (e.g., Facebook), %3$s: Account name */
_n(
'Successfully published %1$d post to %2$s (%3$s).',
'Successfully published %1$d posts to %2$s (%3$s).',
$published,
'ai-story-maker'
),
$published,
ucfirst( $platform ),
$account_name
);
echo '' . esc_html( $message ) . '
';
}
if ( $failed > 0 ) {
$message = sprintf(
/* translators: %d: Number of posts that failed to publish */
_n(
'Failed to publish %d post to social media.',
'Failed to publish %d posts to social media.',
$failed,
'ai-story-maker'
),
$failed
);
echo '' . esc_html( $message ) . '
';
}
}
}
/**
* Get social media account by ID.
*/
private function get_social_media_account( $account_id ) {
$social_media_accounts = get_option( 'aistma_social_media_accounts', array( 'accounts' => array() ) );
foreach ( $social_media_accounts['accounts'] as $account ) {
if ( $account['id'] === $account_id && $account['enabled'] ) {
return $account;
}
}
return null;
}
/**
* Publish a post to a social media account.
*/
private function publish_post_to_social_media( $post, $account ) {
if ( $account['platform'] === 'facebook' ) {
return $this->publish_to_facebook( $post, $account );
}
/* translators: %s: Social media platform name (e.g., Twitter, Instagram) */
$error_msg = sprintf( __( 'Platform %s not yet implemented', 'ai-story-maker' ), $account['platform'] );
$this->aistma_log_manager->log(
'error',
sprintf(
'Unsupported platform for post "%s" (ID: %d): %s (Account: %s)',
$post->post_title,
$post->ID,
$account['platform'],
$account['name']
)
);
return array(
'success' => false,
'message' => $error_msg
);
}
/**
* Publish post to Facebook.
*/
private function publish_to_facebook( $post, $account ) {
if ( empty( $account['credentials']['access_token'] ) || empty( $account['credentials']['page_id'] ) ) {
$error_msg = __( 'Missing Facebook credentials.', 'ai-story-maker' );
$this->aistma_log_manager->log(
'error',
sprintf(
'Facebook publish failed for post "%s" (ID: %d): %s (Account: %s)',
$post->post_title,
$post->ID,
$error_msg,
$account['name']
)
);
return array(
'success' => false,
'message' => $error_msg
);
}
// Prepare post content
$message = $post->post_title;
if ( ! empty( $post->post_excerpt ) ) {
$message .= "\n\n" . $post->post_excerpt;
}
$post_url = get_permalink( $post );
// Add "Read More.." with post link
$message .= "\n\nRead More.. " . $post_url;
// Add hashtags if enabled
$hashtags = $this->get_social_media_hashtags( $post );
if ( ! empty( $hashtags ) ) {
$message .= "\n\n" . $hashtags;
}
// Facebook Graph API endpoint
$api_url = 'https://graph.facebook.com/v18.0/' . $account['credentials']['page_id'] . '/feed';
$post_data = array(
'message' => $message,
'link' => $post_url,
'access_token' => $account['credentials']['access_token']
);
// Log the Facebook API request details
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Facebook API request - URL: %s, Page ID: %s, Message length: %d',
$api_url,
$account['credentials']['page_id'],
strlen( $message )
) );
}
$response = wp_remote_post( $api_url, array(
'body' => $post_data,
'timeout' => 60, // Increased timeout to 60 seconds
'headers' => array(
'User-Agent' => 'AI Story Maker WordPress Plugin'
)
) );
if ( is_wp_error( $response ) ) {
$error_msg = __( 'Network error: ', 'ai-story-maker' ) . $response->get_error_message();
$this->aistma_log_manager->log(
'error',
sprintf(
'Facebook API network error for post "%s" (ID: %d): %s (Account: %s) with hashtags: %s',
$post->post_title,
$post->ID,
$response->get_error_message(),
$account['name'],
$hashtags
)
);
return array(
'success' => false,
'message' => $error_msg
);
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
// Log the Facebook API response details
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Facebook API response - Code: %d, Body length: %d, Body: %s',
$response_code,
strlen( $response_body ),
substr( $response_body, 0, 500 ) // Log first 500 chars
) );
}
$data = json_decode( $response_body, true );
if ( $response_code === 200 && isset( $data['id'] ) ) {
// Store the social media post ID for future reference
add_post_meta( $post->ID, '_aistma_facebook_post_id', $data['id'], true );
$this->aistma_log_manager->log(
'info',
sprintf(
'Post "%s" (ID: %d) successfully published to Facebook account "%s" (Facebook Post ID: %s) with hashtags: %s',
$post->post_title,
$post->ID,
$account['name'],
$data['id'],
$hashtags
)
);
return array(
'success' => true,
/* translators: %s: Facebook account name */
'message' => sprintf( __( 'Successfully published to Facebook: %s', 'ai-story-maker' ), $account['name'] )
);
} else {
$error_message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown Facebook API error', 'ai-story-maker' );
$full_error = __( 'Facebook API error: ', 'ai-story-maker' ) . $error_message;
$this->aistma_log_manager->log(
'error',
sprintf(
'Facebook API error for post "%s" (ID: %d): %s (HTTP %d) (Account: %s) (Response: %s) with hashtags: %s',
$post->post_title,
$post->ID,
$error_message,
$response_code,
$account['name'],
$response_body,
$hashtags
)
);
return array(
'success' => false,
'message' => $full_error
);
}
}
/**
* Get hashtags for social media posting based on settings and post tags.
*
* @param WP_Post $post The post object.
* @return string Formatted hashtags string.
*/
private function get_social_media_hashtags( $post ) {
$social_media_accounts = get_option( 'aistma_social_media_accounts', array( 'global_settings' => array() ) );
$global_settings = $social_media_accounts['global_settings'] ?? array();
$hashtags = array();
// Add default hashtags if set
if ( ! empty( $global_settings['default_hashtags'] ) ) {
$default_hashtags = trim( $global_settings['default_hashtags'] );
if ( ! empty( $default_hashtags ) ) {
// Split by spaces and clean up
$default_tags = array_filter( array_map( 'trim', explode( ' ', $default_hashtags ) ) );
foreach ( $default_tags as $tag ) {
// Ensure hashtag starts with #
if ( ! empty( $tag ) && $tag[0] !== '#' ) {
$tag = '#' . $tag;
}
$hashtags[] = $tag;
}
}
}
// Always add post tags as hashtags
$post_tags = get_the_tags( $post->ID );
if ( $post_tags && ! is_wp_error( $post_tags ) ) {
foreach ( $post_tags as $tag ) {
// Convert tag name to hashtag format
$hashtag = '#' . str_replace( ' ', '', $tag->name );
$hashtags[] = $hashtag;
}
}
// Remove duplicates and return
$hashtags = array_unique( $hashtags );
return ! empty( $hashtags ) ? implode( ' ', $hashtags ) : '';
}
/**
* Add social media actions to individual post rows.
*
* @param array $actions Post row actions.
* @param WP_Post $post Post object.
* @return array Modified post row actions.
*/
public function add_social_media_row_actions( $actions, $post ) {
// Only add actions to published posts
if ( $post->post_status !== 'publish' ) {
return $actions;
}
// Get saved social media accounts
$social_media_accounts = get_option( 'aistma_social_media_accounts', array( 'accounts' => array() ) );
if ( empty( $social_media_accounts['accounts'] ) ) {
return $actions;
}
// Count enabled accounts
$enabled_accounts = array_filter( $social_media_accounts['accounts'], function( $account ) {
return $account['enabled'];
});
if ( empty( $enabled_accounts ) ) {
return $actions;
}
// Add social media publish action
if ( count( $enabled_accounts ) === 1 ) {
// Single account - direct action
$account = reset( $enabled_accounts );
$actions['aistma_publish'] = sprintf(
'%s',
$post->ID,
esc_attr( $account['id'] ),
esc_attr( $account['name'] ),
esc_attr( $account['platform'] ),
/* translators: %1$s: Post title, %2$s: Platform name (e.g., Facebook) */
esc_attr( sprintf( __( 'Publish "%1$s" to %2$s', 'ai-story-maker' ), $post->post_title, ucfirst( $account['platform'] ) ) ),
/* translators: %s: Platform name (e.g., Facebook) */
sprintf( __( 'Publish to %s', 'ai-story-maker' ), ucfirst( $account['platform'] ) )
);
} else {
// Multiple accounts - show submenu
$actions['aistma_publish'] = sprintf(
'',
$post->ID,
/* translators: %s: Post title */
esc_attr( sprintf( __( 'Publish "%s" to social media', 'ai-story-maker' ), $post->post_title ) ),
__( 'Publish to Social Media', 'ai-story-maker' )
);
}
return $actions;
}
/**
* Handle AJAX request to validate complete setup for story generation.
*/
public function ajax_validate_accounts() {
// Verify nonce for security
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'generate_story_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Security check failed' ) );
}
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Validate complete setup (accounts + prompts)
$validation = $this->validate_complete_setup_for_generation();
if ( $validation['valid'] ) {
wp_send_json_success( array(
'message' => $validation['message'],
'type' => $validation['type']
) );
} else {
wp_send_json_error( array(
'message' => $validation['message'],
'tab' => $validation['tab'],
'notice' => $validation['notice']
) );
}
}
/**
* Handle AJAX request to publish post to social media.
*/
public function ajax_publish_to_social_media() {
// Set longer execution time for social media API calls
// This is necessary because social media APIs can be slow and may timeout
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required for social media API calls
set_time_limit( 120 ); // 2 minutes
// Log the start of the request
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Social media publish request started - Post ID: %s, Account ID: %s',
sanitize_text_field( wp_unslash( $_POST['post_id'] ?? 'unknown' ) ),
sanitize_text_field( wp_unslash( $_POST['account_id'] ?? 'unknown' ) )
) );
}
// Verify nonce for security
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ?? '' ) ), 'aistma_social_media_nonce' ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( 'Social media publish failed: Security check failed' );
}
wp_send_json_error( array( 'message' => 'Security check failed' ) );
}
// Check user capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( 'Social media publish failed: Insufficient permissions' );
}
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
$post_id = intval( wp_unslash( $_POST['post_id'] ?? 0 ) );
$account_id = sanitize_text_field( wp_unslash( $_POST['account_id'] ?? '' ) );
if ( ! $post_id || ! $account_id ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Social media publish failed: Missing parameters - Post ID: %s, Account ID: %s', $post_id, $account_id ) );
}
wp_send_json_error( array( 'message' => 'Missing required parameters' ) );
}
// Get the post
$post = get_post( $post_id );
if ( ! $post || $post->post_status !== 'publish' ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Social media publish failed: Post not found or not published - Post ID: %s, Status: %s', $post_id, $post->post_status ?? 'unknown' ) );
}
wp_send_json_error( array( 'message' => 'Post not found or not published' ) );
}
// Get the social media account
$account = $this->get_social_media_account( $account_id );
if ( ! $account || ! $account['enabled'] ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Social media publish failed: Account not found or disabled - Account ID: %s', $account_id ) );
}
wp_send_json_error( array( 'message' => 'Social media account not found or disabled' ) );
}
// Log the attempt
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Attempting to publish post "%s" (ID: %d) to %s account "%s"',
$post->post_title,
$post->ID,
$account['platform'],
$account['name']
) );
}
// Attempt to publish
$result = $this->publish_post_to_social_media( $post, $account );
// Log the result
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Conditional debug logging
error_log( sprintf( 'Social media publish result - Success: %s, Message: %s',
$result['success'] ? 'true' : 'false',
$result['message'] ?? 'No message'
) );
}
if ( $result['success'] ) {
wp_send_json_success( array(
'message' => $result['message'],
'platform' => $account['platform'],
'account_name' => $account['name']
) );
} else {
wp_send_json_error( array(
'message' => $result['message']
) );
}
}
/**
* Auto-publish posts to social media when they transition to 'publish' status.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
*/
public function auto_publish_to_social_media( $new_status, $old_status, $post ) {
// Only process when post transitions to 'publish' status
if ( $new_status !== 'publish' || $old_status === 'publish' ) {
return;
}
// Only process standard posts (not pages, attachments, etc.)
if ( $post->post_type !== 'post' ) {
return;
}
// Check if auto-publish is enabled globally
$social_media_accounts = get_option( 'aistma_social_media_accounts', array( 'accounts' => array() ) );
$auto_publish_enabled = isset( $social_media_accounts['global_settings']['auto_publish'] ) &&
$social_media_accounts['global_settings']['auto_publish'];
if ( ! $auto_publish_enabled ) {
return;
}
// Get enabled social media accounts
$enabled_accounts = array();
if ( ! empty( $social_media_accounts['accounts'] ) ) {
foreach ( $social_media_accounts['accounts'] as $account ) {
if ( $account['enabled'] ) {
$enabled_accounts[] = $account;
}
}
}
if ( empty( $enabled_accounts ) ) {
return;
}
// Get hashtags for logging
$hashtags = $this->get_social_media_hashtags( $post );
// Log the auto-publish attempt
$this->aistma_log_manager->log(
'info',
sprintf(
'Auto-publishing post "%s" (ID: %d) to %d social media accounts with hashtags: %s',
$post->post_title,
$post->ID,
count( $enabled_accounts ),
$hashtags
)
);
// Publish to each enabled account
foreach ( $enabled_accounts as $account ) {
$result = $this->publish_post_to_social_media( $post, $account );
if ( $result['success'] ) {
$this->aistma_log_manager->log(
'info',
sprintf(
'Auto-published post "%s" (ID: %d) to %s account "%s" with hashtags: %s',
$post->post_title,
$post->ID,
$account['platform'],
$account['name'],
$hashtags
)
);
} else {
$this->aistma_log_manager->log(
'error',
sprintf(
'Auto-publish failed for post "%s" (ID: %d) to %s account "%s": %s with hashtags: %s',
$post->post_title,
$post->ID,
$account['platform'],
$account['name'],
$result['message'],
$hashtags
)
);
}
}
}
/**
* Check if user has valid subscription or API keys for story generation.
*
* @return array Validation result with 'valid' boolean and 'message' string.
*/
public function validate_accounts_for_generation() {
// Check subscription status first
try {
$subscription_status = $this->aistma_get_subscription_status();
if ( $subscription_status['valid'] ) {
return array(
'valid' => true,
'message' => __( 'Valid subscription found', 'ai-story-maker' ),
'type' => 'subscription'
);
}
// If subscription check failed but we have an error message, log it for debugging
if ( isset( $subscription_status['error'] ) ) {
$this->aistma_log_manager->log( 'warning', 'Subscription validation failed: ' . $subscription_status['error'] );
// Additional debug logging only in debug mode
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'wp_debug_log' ) ) {
wp_debug_log( 'Subscription validation failed: ' . $subscription_status['error'] );
}
}
// Try fallback: check if UI shows active subscription (packages-summary endpoint)
$fallback_check = $this->check_subscription_via_packages_api();
if ( $fallback_check['valid'] ) {
$this->aistma_log_manager->log( 'info', 'Subscription validated via packages API fallback' );
return array(
'valid' => true,
'message' => __( 'Valid subscription found (via packages API)', 'ai-story-maker' ),
'type' => 'subscription'
);
}
} catch ( \Exception $e ) {
$this->aistma_log_manager->log( 'warning', 'Subscription check exception: ' . $e->getMessage() );
}
// No active gateway plan found — direct user to purchase a plan.
return array(
'valid' => false,
/* translators: %s: pricing page URL */
'message' => sprintf( __( 'No active plan found. Please visit %s to get started.', 'ai-story-maker' ), AISTMA_PRICING_URL ),
'type' => 'none'
);
}
/**
* Check if user has saved prompts for story generation.
*
* @return array Validation result with 'valid' boolean and 'message' string.
*/
public function validate_prompts_for_generation() {
$raw_settings = get_option( 'aistma_prompts', '' );
$settings = json_decode( $raw_settings, true );
// Check if the settings are valid JSON and have prompts
if ( JSON_ERROR_NONE !== json_last_error() || empty( $settings['prompts'] ) ) {
return array(
'valid' => false,
'message' => __( 'No prompts found. Please create and save prompts before generating stories.', 'ai-story-maker' ),
'type' => 'no_prompts'
);
}
// Check if there are any active prompts
$active_prompts = 0;
foreach ( $settings['prompts'] as $prompt ) {
if ( isset( $prompt['active'] ) && $prompt['active'] && ! empty( $prompt['text'] ) ) {
$active_prompts++;
}
}
if ( $active_prompts === 0 ) {
return array(
'valid' => false,
'message' => __( 'No active prompts found. Please activate at least one prompt before generating stories.', 'ai-story-maker' ),
'type' => 'no_active_prompts'
);
}
return array(
'valid' => true,
// translators: %d is the number of active prompts
'message' => sprintf( __( 'Found %d active prompts', 'ai-story-maker' ), $active_prompts ),
'type' => 'prompts_ok',
'count' => $active_prompts
);
}
/**
* Complete validation for story generation (accounts + prompts).
*
* @return array Validation result with 'valid' boolean and 'message' string.
*/
public function validate_complete_setup_for_generation() {
// First check accounts
$account_validation = $this->validate_accounts_for_generation();
if ( ! $account_validation['valid'] ) {
return array(
'valid' => false,
'message' => $account_validation['message'],
'type' => 'accounts_required',
'tab' => self::TAB_AI_WRITER,
'notice' => 'accounts_required'
);
}
// Then check prompts
$prompt_validation = $this->validate_prompts_for_generation();
if ( ! $prompt_validation['valid'] ) {
return array(
'valid' => false,
'message' => $prompt_validation['message'],
'type' => 'prompts_required',
'tab' => self::TAB_PROMPTS,
'notice' => 'prompts_required'
);
}
// Both validations passed
return array(
'valid' => true,
'message' => $account_validation['message'] . '. ' . $prompt_validation['message'],
'type' => 'complete_setup'
);
}
/**
* Fallback method to check subscription via packages API (same as UI).
*
* @return array Validation result.
*/
private function check_subscription_via_packages_api() {
try {
$settings_page = AISTMA_Settings_Page::get_instance();
$response_body = $settings_page->aistma_get_available_packages();
// Parse the response similar to how the UI does it
$decoded_wrapper = json_decode( $response_body, true );
if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded_wrapper ) && isset( $decoded_wrapper['body'] ) ) {
$packages_json = is_string( $decoded_wrapper['body'] ) ? $decoded_wrapper['body'] : json_encode( $decoded_wrapper['body'] );
} else {
$packages_json = $response_body;
}
$packages = json_decode( $packages_json, true );
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $packages ) ) {
return array( 'valid' => false, 'error' => 'Invalid packages response' );
}
// Check if any package shows an active subscription
foreach ( $packages as $package ) {
if ( is_array( $package ) && isset( $package['subscription_status'] ) && isset( $package['subscription_info'] ) ) {
$subscription_info = $package['subscription_info'];
// Check if subscription is active and has credits
if ( isset( $subscription_info['credits_total'] ) && isset( $subscription_info['credits_used'] ) ) {
$credits_remaining = $subscription_info['credits_total'] - $subscription_info['credits_used'];
if ( $credits_remaining > 0 ) {
$this->aistma_log_manager->log( 'info', 'Active subscription found via packages API: ' . $credits_remaining . ' credits remaining' );
return array( 'valid' => true, 'credits_remaining' => $credits_remaining );
}
}
}
}
return array( 'valid' => false, 'error' => 'No active subscription found in packages API' );
} catch ( \Exception $e ) {
$this->aistma_log_manager->log( 'error', 'Error checking subscription via packages API: ' . $e->getMessage() );
return array( 'valid' => false, 'error' => 'Packages API check failed: ' . $e->getMessage() );
}
}
/**
* Get subscription status (helper method).
*
* @return array Subscription status information.
*/
private function aistma_get_subscription_status() {
// Use the story generator's subscription status method
$story_generator = new AISTMA_Story_Generator();
return $story_generator->aistma_get_subscription_status();
}
/**
* Render wizard and preview modals in admin footer.
*
* @return void
*/
public function aistma_render_wizard_modals() {
// Only render in admin pages where posts can be edited
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
// Render modals using the wizard class
echo AISTMA_Activation_Wizard::get_wizard_modal_html();
echo AISTMA_Activation_Wizard::get_preview_modal_html();
// Render weekly confirmation modal template
include AISTMA_PATH . 'admin/templates/weekly-confirmation-modal-template.php';
// Rating modal only on post-editing pages, not on the dashboard
$screen = get_current_screen();
if ( ! $screen || 'dashboard' !== $screen->id ) {
include AISTMA_PATH . 'admin/templates/rating-modal-template.php';
$user_id = get_current_user_id();
if ( AISTMA_Rating_Request::should_show_rating( $user_id ) ) {
?>
__( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
$prompt_id = isset( $_POST['prompt_id'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt_id'] ) ) : '';
if ( empty( $prompt_id ) ) {
wp_send_json_error( array( 'message' => __( 'No prompt selected.', 'ai-story-maker' ) ) );
}
// Get the selected prompt — first check user's custom site-topic prompt
// (stored as a transient by aistma_generate_site_prompt), then fall back
// to the built-in wizard prompt list.
$selected_prompt = null;
if ( 'custom-site-prompt' === $prompt_id ) {
$selected_prompt = get_transient( 'aistma_custom_prompt_' . $user_id );
}
if ( ! $selected_prompt ) {
$prompts = AISTMA_Activation_Wizard::get_default_prompts();
foreach ( $prompts as $prompt ) {
if ( $prompt['id'] === $prompt_id ) {
$selected_prompt = $prompt;
break;
}
}
}
if ( ! $selected_prompt ) {
wp_send_json_error( array( 'message' => __( 'Invalid prompt selected.', 'ai-story-maker' ) ) );
}
// The gateway is the single source of truth for credits. If the domain
// has no active plan, auto-enroll the free tier (5 credits) and retry.
// We never fall back to a local OpenAI key — users should purchase a plan.
$story_generator = new AISTMA_Story_Generator();
if ( ! $story_generator->gateway_can_generate() ) {
$this->auto_enroll_free_package( get_option( 'admin_email' ) );
$story_generator->clear_cached_subscription_status();
if ( ! $story_generator->gateway_can_generate() ) {
wp_send_json_error( array(
'message' => __( 'Your free plan could not be activated automatically. Please choose a plan to start generating stories.', 'ai-story-maker' ),
'redirect_url' => AISTMA_PRICING_URL,
) );
return;
}
}
// Get default settings from plugin options
$raw_settings = get_option( 'aistma_prompts', '' );
$settings = json_decode( $raw_settings, true );
$default_settings = isset( $settings['default_settings'] ) ? $settings['default_settings'] : array();
// Format the selected prompt to match system prompt structure
$prompt_for_generation = array(
'prompt_id' => $prompt_id,
'text' => $selected_prompt['description'],
'category' => $selected_prompt['category'],
'active' => 1,
'photos' => isset( $selected_prompt['photos'] ) ? absint( $selected_prompt['photos'] ) : 1,
);
// Save the selected prompt to user prompts
$this->save_prompt_from_wizard( $prompt_id, $prompt_for_generation );
// Get master instructions
$aistma_master_instructions = $story_generator->aistma_get_master_instructions();
// Call the existing story generation function
try {
$post_id = $story_generator->generate_ai_story( $prompt_id, $prompt_for_generation, $default_settings, null, $aistma_master_instructions );
$this->aistma_log_manager->log( 'debug', sprintf( 'Wizard generation returned post_id: %s (type: %s)', var_export( $post_id, true ), gettype( $post_id ) ) );
if ( is_wp_error( $post_id ) || ! $post_id ) {
$this->aistma_log_manager->log( 'error', sprintf( 'Invalid post_id returned from generate_ai_story: %s', var_export( $post_id, true ) ) );
wp_send_json_error( array( 'message' => __( 'Failed to generate story.', 'ai-story-maker' ) ) );
}
// Log the prompt selection event
AISTMA_Gateway_Logger::log_prompt_selected( $user_id, $prompt_id, array( 'post_id' => $post_id ) );
// Prepare response with generated post data. Re-query the gateway
// so the count reflects the credit just consumed server-side.
$story_generator->clear_cached_subscription_status();
$credits_remaining = $story_generator->gateway_credits_remaining();
wp_send_json_success( array(
'post_id' => $post_id,
'prompt_id' => $prompt_id,
'title' => wp_specialchars_decode( get_the_title( $post_id ) ),
'excerpt' => wp_trim_words( get_the_content( '', false, $post_id ), 30 ),
'featured_image_url' => get_the_post_thumbnail_url( $post_id ) ?: '',
'credits_remaining' => $credits_remaining,
) );
} catch ( \RuntimeException $e ) {
wp_send_json_error( array( 'message' => $e->getMessage() ) );
}
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Wizard generation error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred during generation.', 'ai-story-maker' ) ) );
}
}
/**
* Save a wizard prompt to the user's saved prompts.
*
* Adds the selected wizard prompt to the aistma_prompts option if not already present.
*
* @param string $prompt_id The prompt ID.
* @param array $prompt_data The prompt data including text, category, photos.
* @return void
*/
private function save_prompt_from_wizard( $prompt_id, $prompt_data ) {
// Get existing prompts
$raw_json = get_option( 'aistma_prompts', '{}' );
$settings = json_decode( $raw_json, true );
if ( ! is_array( $settings ) ) {
$settings = array();
}
$prompts = isset( $settings['prompts'] ) ? $settings['prompts'] : array();
if ( ! is_array( $prompts ) ) {
$prompts = array();
}
// Check if prompt already exists
$exists = false;
foreach ( $prompts as $p ) {
if ( isset( $p['prompt_id'] ) && $p['prompt_id'] === $prompt_id ) {
$exists = true;
break;
}
}
// If not exists, add it
if ( ! $exists ) {
$prompts[] = array(
'prompt_id' => $prompt_id,
'text' => $prompt_data['text'],
'category' => $prompt_data['category'],
'photos' => isset( $prompt_data['photos'] ) ? absint( $prompt_data['photos'] ) : 1,
'active' => true,
'auto_publish' => false,
);
// Save back
$settings['prompts'] = $prompts;
update_option( 'aistma_prompts', wp_json_encode( $settings ) );
$this->aistma_log_manager->log( 'info', sprintf( 'Prompt %s saved from wizard selection.', $prompt_id ) );
}
}
/**
* AJAX handler for saving wizard-generated story.
*
* Saves the draft post and deducts credits.
*
* @return void
*/
public function aistma_wizard_save() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_wizard_save_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
if ( ! $post_id || intval( get_post_field( 'post_author', $post_id ) ) !== $user_id ) {
wp_send_json_error( array( 'message' => __( 'Invalid post.', 'ai-story-maker' ) ) );
}
// Note: Credits are already deducted during story generation via the gateway endpoint
// No need to deduct again here
// Publish the post
wp_update_post( array(
'ID' => $post_id,
'post_status' => 'publish',
) );
// Get the prompt ID from post meta if available
$prompt_id = get_post_meta( $post_id, '_aistma_prompt_id', true ) ?: 'wizard';
// Get the actual prompt post ID for weekly scheduling
$wizard_prompt_id = $prompt_id;
if ( 'wizard' !== $wizard_prompt_id ) {
// Try to find the actual prompt post by matching the ID
$prompt_post = get_post( absint( $wizard_prompt_id ) );
if ( $prompt_post && 'aistma_prompt' === $prompt_post->post_type ) {
// Store the wizard prompt ID so confirmWeekly can access it
update_post_meta( $post_id, '_aistma_wizard_prompt_id', absint( $wizard_prompt_id ) );
}
}
// Increment generation count for rating request logic
AISTMA_Rating_Request::increment_generation_count( $user_id );
// Log story generation event
AISTMA_Gateway_Logger::log_story_generated( $user_id, $post_id, $prompt_id, 1 );
// Check if rating should be shown
$show_rating = AISTMA_Rating_Request::should_show_rating( $user_id );
$credits_remaining = ( new AISTMA_Story_Generator() )->gateway_credits_remaining();
wp_send_json_success( array(
'post_id' => $post_id,
'credits_remaining' => $credits_remaining,
'edit_url' => get_edit_post_link( $post_id, 'raw' ),
'show_rating' => $show_rating,
) );
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Wizard save error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred while saving.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler for canceling the wizard (deletes draft post).
*
* @return void
*/
public function aistma_wizard_cancel() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_wizard_cancel_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
// The draft is intentionally preserved so the user does not lose their credit.
// The JS layer shows a notice directing the user to Posts > All Posts.
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$this->aistma_log_manager->log( 'info', 'Wizard closed by user ' . get_current_user_id() . ' — draft ' . $post_id . ' preserved.' );
wp_send_json_success( array( 'message' => __( 'Draft preserved.', 'ai-story-maker' ) ) );
}
/**
* AJAX handler: generate a custom wizard prompt from the site description.
*
* Receives a free-text site description typed by the user in the wizard and
* returns a ready-to-use prompt object without requiring an AI API call —
* the prompt text is built server-side from a template so it works for every
* user regardless of their subscription or API key status.
*
* The prompt is also stashed in a 30-minute transient so aistma_wizard_generate
* can look it up by the 'custom-site-prompt' prompt_id.
*
* @return void
*/
public function aistma_generate_site_prompt() {
if ( ! check_ajax_referer( 'aistma_site_prompt_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ai-story-maker' ) ) );
}
$raw_description = isset( $_POST['site_description'] ) ? sanitize_text_field( wp_unslash( $_POST['site_description'] ) ) : '';
if ( empty( $raw_description ) ) {
wp_send_json_error( array( 'message' => __( 'Please enter what your site is about.', 'ai-story-maker' ) ) );
}
// Build a prompt from the site description without any external API call.
$prompt_text = sprintf(
/* translators: %s: site description entered by the user */
__( 'Write an engaging, informative article for a website about: %s. Include practical tips, real-world examples, and a compelling introduction that hooks readers immediately. Structure the article with clear sections and a strong conclusion.', 'ai-story-maker' ),
$raw_description
);
$short_name = wp_trim_words( $raw_description, 6, '...' );
$prompt_data = array(
'id' => 'custom-site-prompt',
'name' => $short_name,
'description' => __( 'Custom prompt generated from your site description.', 'ai-story-maker' ),
'category' => 'General',
'example' => $raw_description,
'photos' => 1,
'text' => $prompt_text,
);
// Stash the prompt so aistma_wizard_generate can retrieve it by prompt_id.
set_transient( 'aistma_custom_prompt_' . get_current_user_id(), $prompt_data, 30 * MINUTE_IN_SECONDS );
wp_send_json_success( $prompt_data );
}
/**
* AJAX handler for dismissing the wizard.
*
* @return void
*/
public function aistma_wizard_dismiss() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_wizard_dismiss_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
AISTMA_Activation_Wizard::dismiss_wizard();
wp_send_json_success();
}
/**
* AJAX handler for confirming weekly auto-generation.
*
* @return void
*/
public function aistma_confirm_weekly() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_confirm_weekly_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
$prompt_id = isset( $_POST['prompt_id'] ) ? absint( $_POST['prompt_id'] ) : 0;
if ( ! $prompt_id ) {
wp_send_json_error( array( 'message' => __( 'Invalid prompt ID.', 'ai-story-maker' ) ) );
}
// Load weekly scheduler
if ( ! class_exists( __NAMESPACE__ . '\\AISTMA_Weekly_Scheduler' ) ) {
AISTMA_Plugin::aistma_load_dependencies( array( 'includes/class-aistma-weekly-scheduler.php' ) );
}
// Enable weekly generation
$enabled = AISTMA_Weekly_Scheduler::enable_weekly( $user_id, $prompt_id );
if ( ! $enabled ) {
wp_send_json_error( array( 'message' => __( 'Could not enable weekly generation.', 'ai-story-maker' ) ) );
}
// Log the event
if ( class_exists( __NAMESPACE__ . '\\AISTMA_Gateway_Logger' ) ) {
AISTMA_Gateway_Logger::log_event( array(
'event_type' => 'aistma_weekly_schedule_enabled',
'user_id' => $user_id,
'prompt_id' => $prompt_id,
) );
}
wp_send_json_success( array(
'message' => __( 'Weekly auto-generation enabled!', 'ai-story-maker' ),
) );
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Weekly confirm error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred while enabling weekly generation.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler for rating submission.
*
* @return void
*/
public function aistma_submit_rating() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_submit_rating_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
$rating = isset( $_POST['rating'] ) ? absint( $_POST['rating'] ) : 0;
$never_ask = isset( $_POST['never_ask'] ) ? (bool) absint( $_POST['never_ask'] ) : false;
// Validate rating value
if ( $rating < 1 || $rating > 5 ) {
wp_send_json_error( array( 'message' => __( 'Invalid rating value.', 'ai-story-maker' ) ) );
}
// Mark rating as shown
AISTMA_Rating_Request::mark_rating_shown( $user_id );
// Mark never ask if checked
if ( $never_ask ) {
AISTMA_Rating_Request::mark_never_show( $user_id );
}
// Log the rating submission event
AISTMA_Gateway_Logger::log_rating_submitted( $user_id, 0, $rating );
// Log local event
$this->aistma_log_manager->log(
'info',
sprintf(
'User %d submitted rating: %d stars',
$user_id,
$rating
)
);
wp_send_json_success( array(
'message' => __( 'Thank you for your rating!', 'ai-story-maker' ),
) );
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Rating submission error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred while submitting your rating.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler for dismissing rating modal.
*
* @return void
*/
public function aistma_dismiss_rating() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_dismiss_rating_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
// Mark rating as shown (starts cooldown)
AISTMA_Rating_Request::mark_rating_shown( $user_id );
// Log dismiss event
AISTMA_Gateway_Logger::log_event( array(
'event_type' => 'aistma_rating_dismissed',
'user_id' => $user_id,
'timestamp' => current_time( 'mysql' ),
) );
$this->aistma_log_manager->log(
'info',
sprintf(
'User %d dismissed rating modal (remind in 7 days)',
$user_id
)
);
wp_send_json_success();
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Rating dismiss error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler for never showing rating again.
*
* @return void
*/
public function aistma_never_show_rating() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_dismiss_rating_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
// Mark never show
AISTMA_Rating_Request::mark_never_show( $user_id );
// Log event
AISTMA_Gateway_Logger::log_event( array(
'event_type' => 'aistma_rating_never',
'user_id' => $user_id,
'timestamp' => current_time( 'mysql' ),
) );
$this->aistma_log_manager->log(
'info',
sprintf(
'User %d selected "never ask again" for rating',
$user_id
)
);
wp_send_json_success();
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Rating never show error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler to ensure startup credits are granted.
*
* @return void
*/
public function aistma_ensure_startup_credits() {
// DEPRECATED: Startup credits are now granted via auto_enroll_free_package
// This handler is kept for backward compatibility but no longer used
wp_send_json_success( array(
'message' => __( 'Credits ensured.', 'ai-story-maker' ),
) );
}
/**
* Auto-enroll user in free package when they commit to generating a story.
* Replaces the separate startup credits process.
*
* @param string $admin_email The admin email address.
* @return bool True if enrolled, false otherwise.
*/
private function auto_enroll_free_package( $admin_email ) {
$domain = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? wp_parse_url( home_url(), PHP_URL_HOST ) ) );
$gateway_url = defined( 'AISTMA_MASTER_API' ) ? AISTMA_MASTER_API : 'https://www.storymakerplugin.com';
$endpoint = trailingslashit( $gateway_url ) . 'wp-json/exaig/v1/wizard-enroll-free';
$body = array(
'domain' => $domain,
'admin_email' => sanitize_email( $admin_email ),
);
$args = array(
'method' => 'POST',
'body' => wp_json_encode( $body ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 10,
);
$response = wp_remote_post( $endpoint, $args );
if ( is_wp_error( $response ) ) {
$this->aistma_log_manager->log( 'error', 'Failed to auto-enroll in free package: ' . $response->get_error_message() );
return false;
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 === $response_code || 201 === $response_code ) {
$this->aistma_log_manager->log( 'info', 'User auto-enrolled in free package for domain: ' . $domain );
// Store the gateway API key returned by enrollment so story generation can authenticate.
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $body['api_key'] ) && empty( get_option( 'aistma_gateway_api_key' ) ) ) {
update_option( 'aistma_gateway_api_key', sanitize_text_field( $body['api_key'] ) );
$this->aistma_log_manager->log( 'info', 'Gateway API key stored for domain: ' . $domain );
}
return true;
} else {
$this->aistma_log_manager->log( 'warning', 'Gateway returned status ' . $response_code . ' for free package auto-enrollment.' );
return false;
}
}
/**
* Create startup credits account in gateway with admin email.
*
* @param string $admin_email The admin email address.
* @return void
*/
private function create_startup_credits_account( $admin_email ) {
if ( empty( $admin_email ) ) {
$this->aistma_log_manager->log( 'warning', 'Admin email not available for startup credits account creation.' );
return;
}
$domain = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? wp_parse_url( home_url(), PHP_URL_HOST ) ) );
$gateway_url = defined( 'AISTMA_MASTER_API' ) ? AISTMA_MASTER_API : 'https://www.storymakerplugin.com';
$endpoint = trailingslashit( $gateway_url ) . 'wp-json/exaig/v1/ensure-startup-credits';
$body = array(
'domain' => $domain,
'admin_email' => sanitize_email( $admin_email ),
'startup_credits' => absint( get_option( 'aistma_startup_credit_amount', 5 ) ),
);
$args = array(
'method' => 'POST',
'body' => wp_json_encode( $body ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 10,
);
$response = wp_remote_post( $endpoint, $args );
if ( is_wp_error( $response ) ) {
$this->aistma_log_manager->log( 'error', 'Failed to create startup credits account in gateway: ' . $response->get_error_message() );
} else {
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 === $response_code || 201 === $response_code ) {
$this->aistma_log_manager->log( 'info', 'Startup credits account created in gateway with admin email: ' . $admin_email );
} else {
$this->aistma_log_manager->log( 'warning', 'Gateway returned status ' . $response_code . ' for startup credits account creation.' );
}
}
}
/**
* AJAX handler to log when user closes wizard without generating.
*
* @return void
*/
public function aistma_log_wizard_escape() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_log_wizard_escape_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
$user_id = get_current_user_id();
// Log the escape event
if ( class_exists( __NAMESPACE__ . '\\AISTMA_Gateway_Logger' ) ) {
AISTMA_Gateway_Logger::log_event( array(
'event_type' => 'aistma_wizard_closed_without_generating',
'user_id' => $user_id,
) );
}
// Log locally
$this->aistma_log_manager->log( 'info', sprintf( 'User %d closed wizard without generating a story.', $user_id ) );
wp_send_json_success();
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Log wizard escape error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred.', 'ai-story-maker' ) ) );
}
}
/**
* AJAX handler to mark wizard as shown today (24-hour throttling).
*
* @return void
*/
public function aistma_mark_wizard_shown_today() {
// Check nonce
if ( ! check_ajax_referer( 'aistma_mark_wizard_shown_today_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed.', 'ai-story-maker' ) ) );
}
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'ai-story-maker' ) ) );
}
try {
if ( class_exists( __NAMESPACE__ . '\\AISTMA_Activation_Wizard' ) ) {
AISTMA_Activation_Wizard::mark_wizard_shown_today();
}
wp_send_json_success( array(
'message' => __( 'Wizard marked as shown today.', 'ai-story-maker' ),
) );
} catch ( \Throwable $e ) {
$this->aistma_log_manager->log( 'error', 'Mark wizard shown today error: ' . $e->getMessage() );
wp_send_json_error( array( 'message' => __( 'An error occurred.', 'ai-story-maker' ) ) );
}
}
}
// Instantiate the Admin class.
new AISTMA_Admin();