guidance_repo = new AIT_AIAA_Guidance_Repository(); $this->guidance_renderer = new AIT_AIAA_Guidance_Renderer(); $this->ai_service = new AIT_AIAA_AI_Service(); $this->bookmarks = new AIT_AIAA_Bookmarks(); $this->third_party = new AIT_AIAA_Third_Party(); add_action( 'wp_ajax_ait_aiaa_get_panel_content', array( $this, 'get_panel_content' ) ); add_action( 'wp_ajax_ait_aiaa_ask_ai', array( $this, 'ask_ai' ) ); add_action( 'wp_ajax_ait_aiaa_save_bookmark', array( $this, 'save_bookmark' ) ); add_action( 'wp_ajax_ait_aiaa_get_bookmarks', array( $this, 'get_bookmarks' ) ); add_action( 'wp_ajax_ait_aiaa_delete_bookmark', array( $this, 'delete_bookmark' ) ); } protected function verify() { if ( ! is_admin() ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } if ( ! current_user_can( 'read' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } check_ajax_referer( 'ait_aiaa_nonce', 'nonce' ); } protected function user_has_hidden_role( $setting_key ) { $hide_for = AIT_AIAA_Settings::get( $setting_key ); if ( ! is_array( $hide_for ) ) { $hide_for = array(); } $user = wp_get_current_user(); if ( empty( $user->roles ) ) { return true; } foreach ( $user->roles as $role ) { if ( in_array( $role, $hide_for, true ) ) { return true; } } return false; } protected function get_primary_role() { $user = wp_get_current_user(); if ( ! empty( $user->roles ) ) { return sanitize_text_field( $user->roles[0] ); } return ''; } protected function get_daily_usage_key() { return '_ait_aiaa_ai_usage_' . gmdate( 'Ymd' ); } protected function get_daily_limit_for_role( $role ) { // Pro can provide a per-role limit table, but free can still fall back. $per_role = AIT_AIAA_Settings::get( 'ai_daily_limits_per_role' ); if ( is_array( $per_role ) ) { foreach ( $per_role as $row ) { if ( empty( $row['role'] ) ) { continue; } if ( $row['role'] === $role ) { $limit = isset( $row['limit'] ) ? (int) $row['limit'] : 0; if ( $limit > 0 ) { return $limit; } } } } $global = (int) AIT_AIAA_Settings::get( 'ai_daily_limit' ); return $global > 0 ? $global : 0; } protected function get_daily_usage_for_role( $user_id, $role ) { $data = get_user_meta( $user_id, $this->get_daily_usage_key(), true ); if ( ! is_array( $data ) ) { $data = array(); } return isset( $data[ $role ] ) ? (int) $data[ $role ] : 0; } protected function increment_daily_usage_for_role( $user_id, $role ) { $key = $this->get_daily_usage_key(); $data = get_user_meta( $user_id, $key, true ); if ( ! is_array( $data ) ) { $data = array(); } $data[ $role ] = isset( $data[ $role ] ) ? ( (int) $data[ $role ] + 1 ) : 1; update_user_meta( $user_id, $key, $data ); return (int) $data[ $role ]; } protected function get_guidance_text_for_context( $context ) { $limit = (int) AIT_AIAA_Settings::get( 'guidance_max_items' ); if ( $limit <= 0 ) { $limit = 3; } $posts = $this->guidance_repo->get_guidance_for_context( $context, $limit ); if ( empty( $posts ) ) { return ''; } $chunks = array(); foreach ( $posts as $p ) { $title = isset( $p->post_title ) ? sanitize_text_field( $p->post_title ) : ''; $content = isset( $p->post_content ) ? wp_strip_all_tags( $p->post_content ) : ''; $content = trim( preg_replace( '/\s+/', ' ', $content ) ); if ( '' === $title && '' === $content ) { continue; } $chunks[] = ( $title ? ( $title . ': ' ) : '' ) . $content; } return implode( "\n\n", $chunks ); } protected function sanitize_visible_headings( $data ) { if ( ! is_array( $data ) ) { return array(); } $out = array( 'h1' => '', 'h2' => array(), 'h3' => array(), 'metabox_titles' => array(), ); if ( isset( $data['h1'] ) ) { $out['h1'] = sanitize_text_field( $data['h1'] ); } foreach ( array( 'h2', 'h3', 'metabox_titles' ) as $k ) { if ( empty( $data[ $k ] ) || ! is_array( $data[ $k ] ) ) { continue; } $vals = array(); foreach ( $data[ $k ] as $v ) { $v = sanitize_text_field( $v ); if ( '' === $v ) { continue; } $vals[] = $v; if ( count( $vals ) >= 60 ) { break; } } $out[ $k ] = $vals; } return $out; } protected function sanitize_field_schema( $rows ) { if ( ! is_array( $rows ) ) { return array(); } $out = array(); $allowed_types = array( 'text','email','url','number','search','tel','password','checkbox','radio','select','textarea','date','datetime-local','time','color','range','file' ); foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $label = isset( $row['label'] ) ? sanitize_text_field( $row['label'] ) : ''; $type = isset( $row['type'] ) ? sanitize_text_field( $row['type'] ) : ''; $name = isset( $row['name'] ) ? sanitize_text_field( $row['name'] ) : ''; $type_l = strtolower( $type ); if ( ! in_array( $type_l, $allowed_types, true ) ) { $type_l = 'text'; } $out[] = array( 'label' => $label, 'type' => $type_l, 'name' => $name, ); if ( count( $out ) >= 120 ) { break; } } return $out; } protected function sanitize_conversation_messages( $rows ) { if ( ! is_array( $rows ) ) { return array(); } $out = array(); $allowed_roles = array( 'user', 'assistant' ); foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $role = isset( $row['role'] ) ? strtolower( sanitize_text_field( $row['role'] ) ) : ''; if ( ! in_array( $role, $allowed_roles, true ) ) { continue; } $content = isset( $row['content'] ) ? wp_strip_all_tags( (string) $row['content'] ) : ''; $content = trim( preg_replace( '/\s+/', ' ', $content ) ); if ( '' === $content ) { continue; } // Keep each message bounded. if ( strlen( $content ) > 2500 ) { $content = substr( $content, 0, 2500 ); } $out[] = array( 'role' => $role, 'content' => $content, ); // Max 12 messages (6 turns) if ( count( $out ) >= 12 ) { break; } } return $out; } public function get_panel_content() { $this->verify(); // admin-ajax.php requests don't reliably have get_current_screen(). // Accept explicit context from JS when provided. $context = array( 'screen_id' => isset( $_POST['screen_id'] ) ? sanitize_text_field( wp_unslash( $_POST['screen_id'] ) ) : '', 'post_type' => isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : '', 'taxonomy' => isset( $_POST['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) : '', 'request' => $this->build_request_context(), ); if ( empty( $context['screen_id'] ) && empty( $context['post_type'] ) && empty( $context['taxonomy'] ) ) { $context = $this->guidance_repo->get_current_context(); } // Include all request params for advanced third-party targeting. $context['request'] = $this->build_request_context(); $limit = (int) AIT_AIAA_Settings::get( 'guidance_max_items' ); $guidance_posts = $this->guidance_repo->get_guidance_for_context( $context, $limit ); $guidance_html = $this->guidance_renderer->render_items_html( $guidance_posts ); $user_id = get_current_user_id(); $bookmarks = $this->bookmarks->get_bookmarks( $user_id ); update_option( 'EWD_Debugging', 'Getting context items' ); $third_items = $this->third_party->get_items_for_context( $context ); $third_html = $this->third_party->render_items_html( $third_items ); AIT_AIAA_Logger::log( 'ask_ai_success', array( 'thread_id' => isset( $result['thread_id'] ) ? $result['thread_id'] : 0 ) ); wp_send_json_success( array( 'context' => $context, 'has_guidance' => ! empty( $guidance_posts ), 'guidance' => $guidance_html, 'bookmarks' => $bookmarks, 'third_party' => $third_html, 'third_party_count' => is_array( $third_items ) ? count( $third_items ) : 0, 'has_third_party' => ! empty( $third_items ), 'quick_links' => AIT_AIAA_Settings::get( 'quick_links' ), ) ); } public function ask_ai() { $this->verify(); if ( ! empty( AIT_AIAA_Settings::get( 'ai_disabled' ) ) ) { wp_send_json_error( array( 'message' => __( 'AI is disabled.', 'ait-ai-admin-assistance' ) ), 400 ); } if ( $this->user_has_hidden_role( 'ai_hide_for_roles' ) ) { wp_send_json_error( array( 'message' => __( 'AI is not available for your role.', 'ait-ai-admin-assistance' ) ), 403 ); } $user_id = get_current_user_id(); $role = $this->get_primary_role(); $limit = $this->get_daily_limit_for_role( $role ); if ( $limit > 0 ) { $used = $this->get_daily_usage_for_role( $user_id, $role ); if ( $used >= $limit ) { wp_send_json_error( array( 'message' => __( 'Daily AI limit reached for your role.', 'ait-ai-admin-assistance' ), ), 429 ); } } $question = isset( $_POST['question'] ) ? sanitize_textarea_field( wp_unslash( $_POST['question'] ) ) : ''; $screen_id = isset( $_POST['screen_id'] ) ? sanitize_text_field( wp_unslash( $_POST['screen_id'] ) ) : ''; $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) : ''; // Pro: thread id for follow-up questions (stored chat history). $thread_id = isset( $_POST['thread_id'] ) ? absint( wp_unslash( $_POST['thread_id'] ) ) : 0; $include_extra = ! empty( $_POST['include_extra_context'] ); $admin_path = $include_extra && isset( $_POST['admin_path'] ) ? sanitize_text_field( wp_unslash( $_POST['admin_path'] ) ) : ''; // Extra context is sent as JSON strings (to keep payload small and structured). $visible_headings = array(); if ( $include_extra && isset( $_POST['visible_headings'] ) ) { $raw = (string) wp_unslash( $_POST['visible_headings'] ); $decoded = json_decode( $raw, true ); if ( is_array( $decoded ) ) { $visible_headings = $decoded; } } $field_schema = array(); if ( $include_extra && isset( $_POST['field_schema'] ) ) { $raw = (string) wp_unslash( $_POST['field_schema'] ); $decoded = json_decode( $raw, true ); if ( is_array( $decoded ) ) { $field_schema = $decoded; } } // Sanitize extra context (never include raw HTML or values). $admin_path = (string) $admin_path; if ( '' !== $admin_path ) { // Keep pathname only; strip query string if any slipped through. $admin_path = preg_replace( '/\?.*$/', '', $admin_path ); } $visible_headings = $this->sanitize_visible_headings( $visible_headings ); $field_schema = $this->sanitize_field_schema( $field_schema ); // Pro: optional in-memory "continue conversation" transcript (sent by JS). $continue_conversation = ( defined( 'AIT_AIAA_PRO_ACTIVE' ) && AIT_AIAA_PRO_ACTIVE && ! empty( $_POST['continue_conversation'] ) ); $conversation_messages = array(); if ( $continue_conversation && isset( $_POST['conversation_messages'] ) ) { $raw = (string) wp_unslash( $_POST['conversation_messages'] ); $decoded = json_decode( $raw, true ); if ( is_array( $decoded ) ) { $conversation_messages = $this->sanitize_conversation_messages( $decoded ); } } $context = array( 'screen_id' => $screen_id, 'post_type' => $post_type, 'taxonomy' => $taxonomy, 'response_style' => AIT_AIAA_Settings::get( 'ai_response_style' ), 'user_role' => $role, 'can_manage_options' => current_user_can( 'manage_options' ) ? 1 : 0, 'can_edit_posts' => current_user_can( 'edit_posts' ) ? 1 : 0, 'request' => $this->build_request_context(), ); // Pro: allow the history module to attach this question to an existing thread. if ( $thread_id ) { $context['thread_id'] = $thread_id; } // Designer guidance (screen + role) - always included (when enabled in settings). $context['guidance_text'] = $this->get_guidance_text_for_context( array( 'screen_id' => $screen_id, 'post_type' => $post_type, 'taxonomy' => $taxonomy, 'user_role' => $role, ) ); // Attach extra context only when the user opted in. if ( $include_extra ) { $context['admin_path'] = $admin_path; $context['visible_headings'] = $visible_headings; $context['field_schema'] = $field_schema; } // Pro: include previous conversation messages as AI context when selected. if ( $continue_conversation && ! empty( $conversation_messages ) ) { $context['conversation_messages'] = $conversation_messages; } do_action( 'ait_aiaa_ai_question', $question, $context ); $result = $this->ai_service->answer( $question, $context ); if ( empty( $result['ok'] ) ) { $msg = isset( $result['message'] ) ? $result['message'] : __( 'Unable to generate an answer.', 'ait-ai-admin-assistance' ); wp_send_json_error( array( 'message' => $msg ), 400 ); } // Count usage on success. if ( $limit > 0 ) { $this->increment_daily_usage_for_role( $user_id, $role ); } do_action( 'ait_aiaa_ai_answer', $question, $context, $result ); wp_send_json_success( array( 'answer_html' => isset( $result['answer_html'] ) ? $result['answer_html'] : '', 'meta' => isset( $result['meta'] ) ? $result['meta'] : array(), // Pro: return the thread_id used/created so the next question can continue the conversation. 'thread_id' => $this->get_last_thread_id_safe(), ) ); } /** * Pro safety: avoid fatals if the Pro history module isn't loaded. * * Pro registers a filter that returns the last thread id used. */ protected function get_last_thread_id_safe() { $id = 0; if ( function_exists( 'apply_filters' ) ) { $id = apply_filters( 'ait_aiaa_last_thread_id', 0 ); } return absint( $id ); } public function save_bookmark() { $this->verify(); $user_id = get_current_user_id(); $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : ''; $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : ''; $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : ''; $bookmark = array( 'ts' => time(), 'screen_id' => isset( $_POST['screen_id'] ) ? sanitize_text_field( wp_unslash( $_POST['screen_id'] ) ) : '', 'question' => isset( $_POST['question'] ) ? sanitize_textarea_field( wp_unslash( $_POST['question'] ) ) : '', 'answer' => isset( $_POST['answer_html'] ) ? wp_kses_post( wp_unslash( $_POST['answer_html'] ) ) : '', 'type' => $type, 'title' => $title, 'url' => $url, ); $this->bookmarks->add_bookmark( $user_id, $bookmark ); wp_send_json_success( array( 'ok' => true ) ); } public function get_bookmarks() { $this->verify(); $user_id = get_current_user_id(); wp_send_json_success( array( 'bookmarks' => $this->bookmarks->get_bookmarks( $user_id ) ) ); } public function delete_bookmark() { $this->verify(); $user_id = get_current_user_id(); $index = isset( $_POST['index'] ) ? absint( wp_unslash( $_POST['index'] ) ) : 0; $ok = $this->bookmarks->delete_bookmark( $user_id, $index ); wp_send_json_success( array( 'ok' => $ok ) ); } /** * Build a sanitized request context array for third-party targeting. * * Note: This is intended for internal/admin targeting only. Values are sanitized * and truncated to avoid excessive payloads. * * @return array */ protected function build_request_context() { $raw = isset( $_REQUEST ) ? wp_unslash( $_REQUEST ) : array(); if ( ! is_array( $raw ) ) { return array(); } $out = array(); $max_items = 75; $count = 0; foreach ( $raw as $key => $val ) { if ( $count >= $max_items ) { break; } if ( ! is_string( $key ) ) { continue; } // Avoid massive values (e.g., HTML dumps) if ever present. if ( $key === 'page_source' ) { continue; } $key_s = sanitize_key( $key ); // Scalars if ( is_scalar( $val ) ) { $v = sanitize_text_field( (string) $val ); if ( strlen( $v ) > 500 ) { $v = substr( $v, 0, 500 ); } $out[ $key_s ] = $v; $count++; continue; } // Arrays (one-level deep) if ( is_array( $val ) ) { $arr = array(); $inner_count = 0; foreach ( $val as $k2 => $v2 ) { if ( $inner_count >= 25 ) { break; } if ( is_array( $v2 ) ) { continue; } if ( is_object( $v2 ) ) { continue; } if ( is_scalar( $v2 ) ) { $vv = sanitize_text_field( (string) $v2 ); if ( strlen( $vv ) > 500 ) { $vv = substr( $vv, 0, 500 ); } $arr_key = is_string( $k2 ) ? sanitize_key( $k2 ) : (string) $k2; $arr[ $arr_key ] = $vv; $inner_count++; } } if ( ! empty( $arr ) ) { $out[ $key_s ] = $arr; $count++; } } } return $out; } }