| @@ -204,9 +204,16 @@ | ||
| 204 | 204 | */ |
| 205 | 205 | public function delete_conversation( $conversation_id ) { |
| 206 | 206 | $userid = WPF()->current_userid; |
| 207 | 207 | |
| 208 | - // Delete messages first | |
| 208 | + // SECURITY: Verify ownership before deleting anything | |
| 209 | + // get_conversation() returns null if conversation doesn't belong to current user | |
| 210 | + $conversation = $this->get_conversation( $conversation_id ); | |
| 211 | + if ( ! $conversation ) { | |
| 212 | + return false; | |
| 213 | + } | |
| 214 | + | |
| 215 | + // Delete messages first (now safe - ownership verified above) | |
| 209 | 216 | $messages_table = WPF()->tables->ai_chat_messages; |
| 210 | 217 | WPF()->db->delete( |
| 211 | 218 | $messages_table, |
| 212 | 219 | [ 'conversation_id' => $conversation_id ], |
| @@ -381,9 +388,9 @@ | ||
| 381 | 388 | $updates = [ |
| 382 | 389 | 'message_count' => $conversation['message_count'] + 1, |
| 383 | 390 | 'total_tokens' => $conversation['total_tokens'] + $tokens, |
| 384 | 391 | 'total_credits' => $conversation['total_credits'] + $credits, |
| 385 | - 'last_message_at' => current_time( 'mysql' ), | |
| 392 | + 'last_message_at' => current_time( 'mysql', true ), // UTC for timezone conversion | |
| 386 | 393 | ]; |
| 387 | 394 | $formats = [ '%d', '%d', '%d', '%s' ]; |
| 388 | 395 | |
| 389 | 396 | // Set title from first user message if empty (25 chars max) |
| @@ -455,8 +462,16 @@ | ||
| 455 | 462 | if ( $min_score_setting > 0 ) { |
| 456 | 463 | $request_data['settings']['min_score'] = $min_score_setting / 100; |
| 457 | 464 | } |
| 458 | 465 | |
| 466 | + // Add custom knowledge parameters (Business+ cloud mode only) | |
| 467 | + if ( WPF()->ai_client->is_custom_knowledge_enabled() ) { | |
| 468 | + $request_data['settings']['include_custom_knowledge'] = true; | |
| 469 | + $request_data['settings']['knowledge_priority'] = [ | |
| 470 | + 'chat_priority' => WPF()->ai_client->get_knowledge_priorities( 'chat' ), | |
| 471 | + ]; | |
| 472 | + } | |
| 473 | + | |
| 459 | 474 | // Add local context if enabled |
| 460 | 475 | if ( $use_local && ! empty( $local_context ) ) { |
| 461 | 476 | $request_data['local_context'] = $local_context; |
| 462 | 477 | } |
| @@ -513,10 +528,15 @@ | ||
| 513 | 528 | |
| 514 | 529 | // Convert post_id to url in sources (API sends post_id, PHP generates URL) |
| 515 | 530 | $sources = $has_no_content ? [] : ( $response['sources'] ?? [] ); |
| 516 | 531 | foreach ( $sources as &$source ) { |
| 517 | - if ( ! empty( $source['post_id'] ) && empty( $source['url'] ) ) { | |
| 518 | - $content_source = $source['content_source'] ?? 'wpforo'; | |
| 532 | + $content_source = $source['content_source'] ?? 'wpforo'; | |
| 533 | + | |
| 534 | + if ( $content_source === 'custom_knowledge' ) { | |
| 535 | + // Custom knowledge - no URL, just ensure proper labeling | |
| 536 | + $source['url'] = ''; | |
| 537 | + $source['content_type_label'] = __( 'Knowledge Base', 'wpforo' ); | |
| 538 | + } elseif ( ! empty( $source['post_id'] ) && empty( $source['url'] ) ) { | |
| 519 | 539 | if ( $content_source === 'wordpress' ) { |
| 520 | 540 | // WordPress content - use get_permalink() or stored permalink |
| 521 | 541 | if ( ! empty( $source['permalink'] ) ) { |
| 522 | 542 | $source['url'] = $source['permalink']; |
| @@ -600,11 +620,23 @@ | ||
| 600 | 620 | // Only match if it looks like a post ID (not part of markdown link) |
| 601 | 621 | $text = preg_replace( '/(?<!\[)\[#?(\d{2,})\](?!\])(?!\()/', '[[#$1]]', $text ); |
| 602 | 622 | |
| 603 | 623 | // Early exit if no citations to process |
| 604 | - if ( strpos( $text, '[[#' ) === false ) { | |
| 624 | + // Check for [[# (standard), [[wp_ (WordPress), [[kb_ (knowledge base) | |
| 625 | + if ( strpos( $text, '[[#' ) === false && strpos( $text, '[[wp_' ) === false && strpos( $text, '[[kb_' ) === false ) { | |
| 605 | 626 | return $text; |
| 606 | 627 | } |
| 628 | + | |
| 629 | + // Replace [[#kb_FILE_ID_CHUNK]] format - Knowledge Base content (no URL, show badge) | |
| 630 | + // Matches patterns like [[#kb_59a02953-e6ca-40b6-9d0f-5f482f340b10_383]] | |
| 631 | + $text = preg_replace_callback( | |
| 632 | + '/\[\[#?kb_[a-f0-9\-]+_\d+(?::[^\]]+)?\]\]/', | |
| 633 | + function ( $matches ) { | |
| 634 | + // Show a simple knowledge base indicator (no link since KB has no URL) | |
| 635 | + return '<sup class="wpf-ai-chat-reference wpf-ai-chat-kb-ref" title="' . esc_attr__( 'Source: Knowledge Base', 'wpforo' ) . '"><span>[<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>KB]</span></sup>'; | |
| 636 | + }, | |
| 637 | + $text | |
| 638 | + ); | |
| 607 | 639 | |
| 608 | 640 | // Replace [[#wp_POST_ID:Title]] format - WordPress content with title (title ignored) |
| 609 | 641 | $text = preg_replace_callback( |
| 610 | 642 | '/\[\[#wp_(\d+):([^\]]+)\]\]/', |