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( '%s', $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();