Changeset 3417967
- Timestamp:
- 12/12/2025 07:25:58 AM (4 months ago)
- Location:
- linkflow-chat/trunk
- Files:
-
- 14 edited
-
assets/admin/js/knowledge.js (modified) (1 diff)
-
includes/admin/class-admin-manager.php (modified) (2 diffs)
-
includes/admin/views/knowledge-list.php (modified) (1 diff)
-
includes/class-database-manager.php (modified) (4 diffs)
-
includes/class-qa-manager.php (modified) (10 diffs)
-
includes/class-version-manager.php (modified) (1 diff)
-
includes/models/class-ai-service-model.php (modified) (10 diffs)
-
includes/models/class-conversation-model.php (modified) (11 diffs)
-
includes/models/class-knowledge-model.php (modified) (11 diffs)
-
includes/models/class-message-model.php (modified) (13 diffs)
-
linkflow-chat.php (modified) (2 diffs)
-
plugin-info.json (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
version.json (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
linkflow-chat/trunk/assets/admin/js/knowledge.js
r3375551 r3417967 1 1 (function($){ 2 2 $(function(){ 3 // Search functionality4 $('#search-submit').on('click', function(){5 var search = $('#knowledge-search').val();6 var serviceId = $('#service-filter').val();7 var url = linkflowKnowledge && linkflowKnowledge.baseUrl ? linkflowKnowledge.baseUrl : '';8 if(!url){ return; }9 url += '&service_id=' + encodeURIComponent(serviceId || '');10 if (search) { url += '&search=' + encodeURIComponent(search); }11 location.href = url;12 });13 $('#knowledge-search').on('keypress', function(e){ if (e.which === 13) { $('#search-submit').click(); } });14 15 // Import/export features removed16 17 3 // Textarea auto-resize 18 4 $('textarea').on('input', function(){ this.style.height='auto'; this.style.height=(this.scrollHeight)+'px'; }); -
linkflow-chat/trunk/includes/admin/class-admin-manager.php
r3375551 r3417967 915 915 $selected_service = null; 916 916 $knowledge_entries = array(); 917 // Read search keyword from query string (used by the view and query)918 $search = $this->has_get('search') ? sanitize_text_field($this->get_get('search')) : '';919 917 920 918 if ($service_id > 0) { 921 919 $selected_service = AI_Service_Model::find($service_id); 922 920 if ($selected_service) { 923 // Pass the search parameter through to filter results 924 $knowledge_entries = Knowledge_Model::get_by_service($service_id, array( 925 'search' => $search, 926 )); 921 $knowledge_entries = Knowledge_Model::get_by_service($service_id); 927 922 } 928 923 } … … 1237 1232 1238 1233 if ($result['success']) { 1239 $redirect_url = admin_url('admin.php?page=' . $this->menu_slug . '-knowledge&service_id=' . $service_id); 1240 // Ensure the message is escaped before embedding in URL 1241 wp_redirect(add_query_arg('linkflow_chat_success', urlencode(esc_html($result['message'])), $redirect_url)); 1234 $redirect_url = add_query_arg( 1235 array( 1236 'page' => $this->menu_slug . '-knowledge', 1237 'service_id' => $service_id, 1238 '_wpnonce' => wp_create_nonce('linkflow_chat_admin_nonce'), 1239 'linkflow_chat_success' => urlencode(esc_html($result['message'])), 1240 ), 1241 admin_url('admin.php') 1242 ); 1243 wp_redirect($redirect_url); 1242 1244 exit; 1243 1245 } else { 1244 $redirect_url = admin_url('admin.php?page=' . $this->menu_slug . '-knowledge&service_id=' . $service_id); 1245 // Ensure the error is escaped before embedding in URL 1246 wp_redirect(add_query_arg('linkflow_chat_error', urlencode(esc_html($result['error'])), $redirect_url)); 1246 $redirect_url = add_query_arg( 1247 array( 1248 'page' => $this->menu_slug . '-knowledge', 1249 'service_id' => $service_id, 1250 '_wpnonce' => wp_create_nonce('linkflow_chat_admin_nonce'), 1251 'linkflow_chat_error' => urlencode(esc_html($result['error'])), 1252 ), 1253 admin_url('admin.php') 1254 ); 1255 wp_redirect($redirect_url); 1247 1256 exit; 1248 1257 } -
linkflow-chat/trunk/includes/admin/views/knowledge-list.php
r3375551 r3417967 48 48 ?> 49 49 </select> 50 51 <?php52 if ( $selected_service ) {53 ?>54 <input type="search" id="knowledge-search" name="search" value="<?php55 echo esc_attr( $search );56 ?>" placeholder="<?php57 esc_attr_e( 'Search knowledge base...', 'linkflow-chat' );58 ?>">59 <button type="button" id="search-submit" class="button"><?php60 esc_html_e( 'Search', 'linkflow-chat' );61 ?></button>62 <?php63 }64 ?>65 50 </div> 66 51 -
linkflow-chat/trunk/includes/class-database-manager.php
r3375551 r3417967 222 222 public function tables_exist() { 223 223 global $wpdb; 224 // Cache the result for a short period to avoid frequent direct DB checks225 $cache_key = 'linkflow_chat_tables_exist';226 $cached = wp_cache_get($cache_key);227 if ($cached !== false) {228 return (bool) $cached;229 }230 224 231 225 $tables = array( … … 237 231 238 232 foreach ($tables as $table) { 239 // PHPCS: using SHOW TABLES is a direct DB check but results are cached. 240 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 241 $result = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table ) ); 233 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 234 $result = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); 242 235 if ($result !== $table) { 243 wp_cache_set($cache_key, false, '', 300);244 236 return false; 245 237 } 246 238 } 247 239 248 wp_cache_set($cache_key, true, '', 300);249 240 return true; 250 241 } … … 344 335 */ 345 336 public function get_table_stats() { 346 // Try to get from cache first 347 $cache_key = 'linkflow_chat_table_stats'; 348 $stats = wp_cache_get($cache_key); 349 350 if ($stats === false) { 351 global $wpdb; 352 353 $stats = array(); 354 $tables = array( 355 'services' => $wpdb->prefix . 'plugin_linkflow_chat_services', 356 'knowledges' => $wpdb->prefix . 'plugin_linkflow_chat_knowledges', 357 'conversations' => $wpdb->prefix . 'plugin_linkflow_chat_conversations', 358 'messages' => $wpdb->prefix . 'plugin_linkflow_chat_messages', 359 'email_queue' => $wpdb->prefix . 'plugin_linkflow_chat_email_queue', 360 'temp_tokens' => $wpdb->prefix . 'plugin_linkflow_chat_temp_tokens', 361 ); 362 363 foreach ($tables as $key => $table) { 364 // Use esc_sql for table identifier and a benign placeholder to route through prepare(). 365 $table_sql = esc_sql( $table ); 366 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 367 $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_sql} WHERE 1 = %d", 1 ) ); 368 $stats[$key] = intval($count); 369 } 370 371 // Cache for 5 minutes 372 wp_cache_set($cache_key, $stats, '', 300); 337 global $wpdb; 338 339 $stats = array(); 340 $tables = array( 341 'services' => $wpdb->prefix . 'plugin_linkflow_chat_services', 342 'knowledges' => $wpdb->prefix . 'plugin_linkflow_chat_knowledges', 343 'conversations' => $wpdb->prefix . 'plugin_linkflow_chat_conversations', 344 'messages' => $wpdb->prefix . 'plugin_linkflow_chat_messages', 345 'email_queue' => $wpdb->prefix . 'plugin_linkflow_chat_email_queue', 346 'temp_tokens' => $wpdb->prefix . 'plugin_linkflow_chat_temp_tokens', 347 ); 348 349 foreach ($tables as $key => $table) { 350 $table_sql = esc_sql($table); 351 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 352 $count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_sql} WHERE 1 = %d", 1)); 353 $stats[$key] = intval($count); 373 354 } 374 355 … … 404 385 405 386 /** 406 * Clear related caches 407 */ 408 public function clear_cache() { 409 wp_cache_delete('linkflow_chat_table_stats'); 410 411 // Clear any other related caches 412 $cache_keys = array( 413 'linkflow_chat_active_services', 414 'linkflow_chat_settings', 387 * Get database size information 388 * 389 * @return array 390 */ 391 public function get_database_size() { 392 global $wpdb; 393 394 $tables = array( 395 $wpdb->prefix . 'plugin_linkflow_chat_services', 396 $wpdb->prefix . 'plugin_linkflow_chat_knowledges', 397 $wpdb->prefix . 'plugin_linkflow_chat_conversations', 398 $wpdb->prefix . 'plugin_linkflow_chat_messages', 399 $wpdb->prefix . 'plugin_linkflow_chat_email_queue', 400 $wpdb->prefix . 'plugin_linkflow_chat_temp_tokens', 415 401 ); 416 402 417 foreach ($cache_keys as $key) { 418 wp_cache_delete($key); 419 } 420 } 421 422 /** 423 * Get database size information 424 * 425 * @return array 426 */ 427 public function get_database_size() { 428 global $wpdb; 429 430 $cache_key = 'linkflow_chat_db_size'; 431 $size_info = wp_cache_get($cache_key); 432 433 if ($size_info === false) { 434 $tables = array( 435 $wpdb->prefix . 'plugin_linkflow_chat_services', 436 $wpdb->prefix . 'plugin_linkflow_chat_knowledges', 437 $wpdb->prefix . 'plugin_linkflow_chat_conversations', 438 $wpdb->prefix . 'plugin_linkflow_chat_messages', 439 $wpdb->prefix . 'plugin_linkflow_chat_email_queue', 440 $wpdb->prefix . 'plugin_linkflow_chat_temp_tokens', 403 $total_size = 0; 404 $size_info = array(); 405 406 foreach ($tables as $table) { 407 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 408 $result = $wpdb->get_row( 409 $wpdb->prepare( 410 "SELECT 411 ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb' 412 FROM information_schema.TABLES 413 WHERE table_schema = %s AND table_name = %s", 414 DB_NAME, 415 $table 416 ) 441 417 ); 442 418 443 $total_size = 0; 444 $size_info = array(); 445 446 foreach ($tables as $table) { 447 // This queries information_schema for table size information. Results are cached 448 // above; silence PHPCS warnings related to direct DB queries and caching. 449 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 450 $result = $wpdb->get_row( 451 $wpdb->prepare( 452 "SELECT 453 ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb' 454 FROM information_schema.TABLES 455 WHERE table_schema = %s AND table_name = %s", 456 DB_NAME, 457 $table 458 ) 459 ); 460 461 $size = $result ? floatval($result->size_mb) : 0; 462 $size_info[$table] = $size; 463 $total_size += $size; 464 } 465 466 $size_info['total'] = $total_size; 467 468 // Cache for 1 hour 469 wp_cache_set($cache_key, $size_info, '', 3600); 470 } 419 $size = $result ? floatval($result->size_mb) : 0; 420 $size_info[$table] = $size; 421 $total_size += $size; 422 } 423 424 $size_info['total'] = $total_size; 471 425 472 426 return $size_info; -
linkflow-chat/trunk/includes/class-qa-manager.php
r3375551 r3417967 60 60 $result = $knowledge->save(); 61 61 if ( $result ) { 62 // Clear related caches for this service and question63 self::clear_service_cache( $service_id );64 self::clear_duplicate_cache( $service_id, $knowledge->question );65 62 return array( 66 63 'success' => true, … … 122 119 $result = $knowledge->save(); 123 120 if ( $result ) { 124 // Clear related caches for this service and question125 self::clear_service_cache( $knowledge->service_id );126 self::clear_duplicate_cache( $knowledge->service_id, $knowledge->question );127 121 return array( 128 122 'success' => true, … … 163 157 $result = $knowledge->delete(); 164 158 if ( $result ) { 165 // Clear related caches for this service and question166 self::clear_service_cache( $knowledge->service_id );167 self::clear_duplicate_cache( $knowledge->service_id, $knowledge->question );168 159 return array( 169 160 'success' => true, … … 230 221 } 231 222 232 // Knowledge import removed233 /**234 * Export QA entries to various formats235 *236 * @param int $service_id237 * @param string $format238 * @return array Result with success status and exported data239 */240 // Knowledge export removed241 223 /** 242 224 * Search QA entries … … 265 247 public static function get_qa_stats( $service_id ) { 266 248 $total_count = Knowledge_Model::get_count_by_service( $service_id ); 267 // Recent activity (last 30 days) - use object cache 268 $cache_key = "linkflow_qa_recent_count_{$service_id}"; 269 $recent_count = wp_cache_get( $cache_key, 'linkflow-chat' ); 270 if ( false === $recent_count ) { 271 global $wpdb; 272 $table_name = Knowledge_Model::get_table_name(); 273 // Escape table name since it cannot be passed as a placeholder to prepare(). 274 $table_name = esc_sql( $table_name ); 275 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 276 $recent_count = $wpdb->get_var( $wpdb->prepare( 277 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 278 "SELECT COUNT(*) FROM {$table_name} \n WHERE service_id = %d \n AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)", 279 $service_id 280 ) ); 281 wp_cache_set( 282 $cache_key, 283 $recent_count, 284 'linkflow-chat', 285 HOUR_IN_SECONDS 286 ); 287 } 249 // Recent activity (last 30 days) 250 global $wpdb; 251 $table_name = Knowledge_Model::get_table_name(); 252 $table_name = esc_sql( $table_name ); 253 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 254 $recent_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)", $service_id ) ); 288 255 return array( 289 256 'total_entries' => $total_count, … … 303 270 */ 304 271 private static function is_duplicate_question( $service_id, $question, $exclude_id = null ) { 305 // Cache duplicate checks per service + question + exclude_id306 $exclude_part = ( $exclude_id ? "_{$exclude_id}" : '' );307 $cache_key = 'linkflow_duplicate_' . md5( $service_id . '|' . $question ) . $exclude_part;308 $cached = wp_cache_get( $cache_key, 'linkflow-chat' );309 if ( false !== $cached ) {310 return (bool) $cached;311 }312 272 global $wpdb; 313 273 $table_name = Knowledge_Model::get_table_name(); 314 // Escape table name since table names cannot be used as placeholders in prepare().315 274 $table_name = esc_sql( $table_name ); 316 275 if ( $exclude_id ) { 317 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 276 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 318 277 $count = $wpdb->get_var( $wpdb->prepare( 319 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared320 278 "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d AND question = %s AND id != %d", 321 279 $service_id, … … 324 282 ) ); 325 283 } else { 326 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 327 $count = $wpdb->get_var( $wpdb->prepare( 328 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 329 "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d AND question = %s", 330 $service_id, 331 $question 332 ) ); 333 } 334 $is_dup = intval( $count ) > 0; 335 wp_cache_set( 336 $cache_key, 337 $is_dup, 338 'linkflow-chat', 339 HOUR_IN_SECONDS 340 ); 341 return $is_dup; 284 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 285 $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d AND question = %s", $service_id, $question ) ); 286 } 287 return intval( $count ) > 0; 342 288 } 343 289 … … 430 376 431 377 /** 432 * Generate CSV data433 *434 * @param array $entries435 * @return string436 */437 // CSV generator removed438 /**439 378 * Get average question length 440 379 * … … 443 382 */ 444 383 private static function get_avg_question_length( $service_id ) { 445 $cache_key = "linkflow_avg_q_len_{$service_id}"; 446 $avg = wp_cache_get( $cache_key, 'linkflow-chat' ); 447 if ( false === $avg ) { 448 global $wpdb; 449 $table_name = Knowledge_Model::get_table_name(); 450 // Escape table name since table names cannot be passed as placeholders to prepare(). 451 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 452 $table_name = esc_sql( $table_name ); 453 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 454 $avg = $wpdb->get_var( $wpdb->prepare( 455 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 456 "SELECT AVG(CHAR_LENGTH(question)) FROM {$table_name} WHERE service_id = %d", 457 $service_id 458 ) ); 459 wp_cache_set( 460 $cache_key, 461 $avg, 462 'linkflow-chat', 463 HOUR_IN_SECONDS 464 ); 465 } 384 global $wpdb; 385 $table_name = Knowledge_Model::get_table_name(); 386 $table_name = esc_sql( $table_name ); 387 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 388 $avg = $wpdb->get_var( $wpdb->prepare( "SELECT AVG(CHAR_LENGTH(question)) FROM {$table_name} WHERE service_id = %d", $service_id ) ); 466 389 return floatval( $avg ); 467 390 } … … 474 397 */ 475 398 private static function get_avg_answer_length( $service_id ) { 476 $cache_key = "linkflow_avg_a_len_{$service_id}"; 477 $avg = wp_cache_get( $cache_key, 'linkflow-chat' ); 478 if ( false === $avg ) { 479 global $wpdb; 480 $table_name = Knowledge_Model::get_table_name(); 481 // Escape table name since table names cannot be passed as placeholders to prepare(). 482 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 483 $table_name = esc_sql( $table_name ); 484 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 485 $avg = $wpdb->get_var( $wpdb->prepare( 486 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 487 "SELECT AVG(CHAR_LENGTH(answer)) FROM {$table_name} WHERE service_id = %d", 488 $service_id 489 ) ); 490 wp_cache_set( 491 $cache_key, 492 $avg, 493 'linkflow-chat', 494 HOUR_IN_SECONDS 495 ); 496 } 399 global $wpdb; 400 $table_name = Knowledge_Model::get_table_name(); 401 $table_name = esc_sql( $table_name ); 402 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 403 $avg = $wpdb->get_var( $wpdb->prepare( "SELECT AVG(CHAR_LENGTH(answer)) FROM {$table_name} WHERE service_id = %d", $service_id ) ); 497 404 return floatval( $avg ); 498 405 } 499 406 500 /**501 * Clear caches related to a service ID502 *503 * @param int $service_id504 * @return void505 */506 private static function clear_service_cache( $service_id ) {507 // Clear known keys; Knowledge_Model may also have its own cache keys508 wp_cache_delete( "linkflow_qa_recent_count_{$service_id}", 'linkflow-chat' );509 wp_cache_delete( "linkflow_avg_q_len_{$service_id}", 'linkflow-chat' );510 wp_cache_delete( "linkflow_avg_a_len_{$service_id}", 'linkflow-chat' );511 // Also clear general list cache if used by Knowledge_Model512 wp_cache_delete( "linkflow_qa_list_{$service_id}", 'linkflow-chat' );513 }514 515 /**516 * Clear duplicate check cache for a specific question517 *518 * @param int $service_id519 * @param string $question520 * @return void521 */522 private static function clear_duplicate_cache( $service_id, $question ) {523 $key = 'linkflow_duplicate_' . md5( $service_id . '|' . $question );524 wp_cache_delete( $key, 'linkflow-chat' );525 // Also delete any keyed variants with exclude id suffixes (best effort)526 // We don't know exclude ids easily here; clear broader cache namespace isn't provided by wp_cache.527 }528 529 407 } -
linkflow-chat/trunk/includes/class-version-manager.php
r3375551 r3417967 398 398 */ 399 399 private function clear_upgrade_caches() { 400 // Clear WordPress caches400 // Clear WordPress object cache 401 401 if (function_exists('wp_cache_flush')) { 402 402 wp_cache_flush(); 403 403 } 404 405 // Clear plugin-specific caches406 wp_cache_delete('linkflow_chat_table_stats');407 wp_cache_delete('linkflow_chat_active_services');408 404 409 405 // Clear transients -
linkflow-chat/trunk/includes/models/class-ai-service-model.php
r3375551 r3417967 18 18 */ 19 19 private static $table_name = 'plugin_linkflow_chat_services'; 20 /**21 * Cache group for object cache22 *23 * @var string24 */25 private static $cache_group = 'linkflow_chat';26 20 27 21 /** … … 142 136 foreach ($fillable as $field) { 143 137 if (isset($data[$field])) { 144 $this->$field = $data[$field]; 138 // Ensure id is always an integer for proper comparison 139 if ($field === 'id') { 140 $this->$field = intval($data[$field]); 141 } else { 142 $this->$field = $data[$field]; 143 } 145 144 } 146 145 } … … 155 154 global $wpdb; 156 155 return $wpdb->prefix . self::$table_name; 157 }158 159 /**160 * Generate cache key for ID161 *162 * @param int $id163 * @return string164 */165 private static function get_cache_key_by_id( $id ) {166 return "ai_service_id_{$id}";167 }168 169 /**170 * Generate cache key for name171 *172 * @param string $name173 * @return string174 */175 private static function get_cache_key_by_name( $name ) {176 return "ai_service_name_{$name}";177 }178 179 /**180 * Generate cache key for args (simple serialization)181 *182 * @param array $args183 * @return string184 */185 private static function get_cache_key_for_args( $args ) {186 return 'ai_service_list_' . md5( serialize( $args ) );187 156 } 188 157 … … 276 245 if ($result !== false) { 277 246 $this->id = $wpdb->insert_id; 278 // Invalidate related caches279 wp_cache_delete( self::get_cache_key_by_id( $this->id ), self::$cache_group );280 wp_cache_delete( self::get_cache_key_by_name( $this->name ), self::$cache_group );281 wp_cache_delete( self::get_cache_key_for_args( array() ), self::$cache_group );282 247 return $this->id; 283 248 } … … 295 260 ); 296 261 297 if ( $result !== false ) { 298 // Invalidate caches for this service 299 wp_cache_delete( self::get_cache_key_by_id( $id ), self::$cache_group ); 300 if ( ! empty( $this->name ) ) { 301 wp_cache_delete( self::get_cache_key_by_name( $this->name ), self::$cache_group ); 302 } 303 wp_cache_delete( self::get_cache_key_for_args( array() ), self::$cache_group ); 262 if ($result !== false) { 304 263 return true; 305 264 } … … 323 282 global $wpdb; 324 283 $table_name = self::get_table_name(); 325 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery284 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 326 285 $result = $wpdb->delete( 327 286 $table_name, … … 330 289 ); 331 290 332 if ( $result !== false ) { 333 // Invalidate caches for this service 334 wp_cache_delete( self::get_cache_key_by_id( $this->id ), self::$cache_group ); 335 if ( ! empty( $this->name ) ) { 336 wp_cache_delete( self::get_cache_key_by_name( $this->name ), self::$cache_group ); 337 } 338 wp_cache_delete( self::get_cache_key_for_args( array() ), self::$cache_group ); 339 return true; 340 } 341 342 return false; 291 return $result !== false; 343 292 } 344 293 … … 375 324 global $wpdb; 376 325 $table_name = self::get_table_name(); 377 $cache_key = self::get_cache_key_by_id( $id ); 378 $cached = wp_cache_get( $cache_key, self::$cache_group ); 379 if ( $cached !== false ) { 380 return new self( $cached ); 381 } 382 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 383 $result = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE id = %d', $id ), ARRAY_A ); 384 385 if ( $result ) { 386 wp_cache_set( $cache_key, $result, self::$cache_group ); 387 return new self( $result ); 388 } 326 327 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 328 $result = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . $table_name . ' WHERE id = %d', $id), ARRAY_A); 389 329 390 330 return $result ? new self($result) : null; … … 400 340 global $wpdb; 401 341 $table_name = self::get_table_name(); 402 $cache_key = self::get_cache_key_by_name( $name ); 403 $cached = wp_cache_get( $cache_key, self::$cache_group ); 404 if ( $cached !== false ) { 405 return new self( $cached ); 406 } 407 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 408 $result = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE name = %s', $name ), ARRAY_A ); 409 410 if ( $result ) { 411 wp_cache_set( $cache_key, $result, self::$cache_group ); 412 return new self( $result ); 413 } 414 415 return null; 342 343 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 344 $result = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . $table_name . ' WHERE name = %s', $name), ARRAY_A); 345 346 return $result ? new self($result) : null; 416 347 } 417 348 … … 434 365 435 366 $args = wp_parse_args($args, $defaults); 436 437 $cache_key = self::get_cache_key_for_args( $args );438 $cached = wp_cache_get( $cache_key, self::$cache_group );439 if ( $cached !== false ) {440 $services = array();441 foreach ( $cached as $result ) {442 $services[] = new self( $result );443 }444 445 return $services;446 }447 367 448 368 // Whitelist ORDER BY column and direction 449 $allowed_orderby = array( 'id', 'name', 'show_name', 'created_at', 'updated_at');450 $order_by = in_array( $args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'created_at';451 $order_direction = strtoupper( $args['order']) === 'ASC' ? 'ASC' : 'DESC';369 $allowed_orderby = array('id', 'name', 'show_name', 'created_at', 'updated_at'); 370 $order_by = in_array($args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'created_at'; 371 $order_direction = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC'; 452 372 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses whitelisted column and static direction 453 373 $order_clause = "ORDER BY {$order_by} {$order_direction}"; 454 374 455 // Build base SQL. Table name and order clause are safe (table name comes from get_table_name(), 456 // order clause is sanitized above). Only LIMIT/OFFSET values come from user args and will be 457 // passed through placeholders using $wpdb->prepare(). 458 $sql = 'SELECT * FROM ' . $table_name . ' ' . $order_clause; 459 460 if ( $args['limit'] > 0 ) { 461 if ( $args['offset'] > 0 ) { 462 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 463 $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . $table_name . ' ' . $order_clause . ' LIMIT %d, %d', $args['offset'], $args['limit'] ), ARRAY_A ); 375 if ($args['limit'] > 0) { 376 if ($args['offset'] > 0) { 377 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 378 $results = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $table_name . ' ' . $order_clause . ' LIMIT %d, %d', $args['offset'], $args['limit']), ARRAY_A); 464 379 } else { 465 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 466 $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . $table_name . ' ' . $order_clause . ' LIMIT %d', $args['limit'] ), ARRAY_A);380 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 381 $results = $wpdb->get_results($wpdb->prepare('SELECT * FROM ' . $table_name . ' ' . $order_clause . ' LIMIT %d', $args['limit']), ARRAY_A); 467 382 } 468 469 383 } else { 470 // Execute via prepare() as well by using a harmless placeholder in WHERE. 471 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 472 $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . $table_name . ' WHERE 1 = %d ' . $order_clause, 1 ), ARRAY_A ); 384 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 385 $results = $wpdb->get_results('SELECT * FROM ' . $table_name . ' ' . $order_clause, ARRAY_A); 473 386 } 474 387 475 388 $services = array(); 476 foreach ($results as $result) { 477 $services[] = new self($result); 478 } 479 480 // Store raw results in cache for later 481 wp_cache_set( $cache_key, $results, self::$cache_group ); 389 if ($results) { 390 foreach ($results as $result) { 391 $services[] = new self($result); 392 } 393 } 482 394 483 395 return $services; -
linkflow-chat/trunk/includes/models/class-conversation-model.php
r3375551 r3417967 176 176 } 177 177 178 $table_name = self::get_table_name();178 $table_name = self::get_table_name(); 179 179 $data = $this->to_array(); 180 180 … … 190 190 if ($result !== false) { 191 191 $this->id = $wpdb->insert_id; 192 // Prime cache for the newly created conversation193 if ( function_exists( 'wp_cache_set' ) ) {194 wp_cache_set( "linkflow_conversation_{$this->id}", $this->to_array(), 'linkflow_chat' );195 // Clear service list and count caches for this service196 wp_cache_delete( "linkflow_conversations_service_{$this->service_id}", 'linkflow_chat' );197 wp_cache_delete( "linkflow_conversations_count_{$this->service_id}", 'linkflow_chat' );198 }199 200 192 return $this->id; 201 193 } … … 213 205 ); 214 206 215 if ( $result !== false ) {216 // Update cached conversation and clear related lists/counts217 if ( function_exists( 'wp_cache_set' ) ) {218 wp_cache_set( "linkflow_conversation_{$id}", $this->to_array(), 'linkflow_chat' );219 wp_cache_delete( "linkflow_conversations_service_{$this->service_id}", 'linkflow_chat' );220 wp_cache_delete( "linkflow_conversations_count_{$this->service_id}", 'linkflow_chat' );221 }222 }223 224 207 return $result !== false; 225 208 } … … 240 223 global $wpdb; 241 224 $table_name = self::get_table_name(); 242 $now = current_time( 'mysql' ); 243 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 225 $now = current_time('mysql'); 226 227 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 244 228 $result = $wpdb->update( 245 229 $table_name, 246 array( 'last_activity' => $now),247 array( 'id' => $this->id),248 array( '%s'),249 array( '%d')250 ); 251 252 if ( $result !== false) {230 array('last_activity' => $now), 231 array('id' => $this->id), 232 array('%s'), 233 array('%d') 234 ); 235 236 if ($result !== false) { 253 237 $this->last_activity = $now; 254 if ( function_exists( 'wp_cache_set' ) ) {255 wp_cache_set( "linkflow_conversation_{$this->id}", $this->to_array(), 'linkflow_chat' );256 wp_cache_delete( "linkflow_conversations_service_{$this->service_id}", 'linkflow_chat' );257 }258 238 return true; 259 239 } … … 276 256 // Delete associated messages first 277 257 $messages_table = $wpdb->prefix . 'plugin_linkflow_chat_messages'; 278 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery258 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 279 259 $wpdb->delete( 280 260 $messages_table, … … 285 265 // Delete conversation 286 266 $table_name = self::get_table_name(); 287 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery267 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 288 268 $result = $wpdb->delete( 289 269 $table_name, … … 292 272 ); 293 273 294 if ( $result !== false ) { 295 if ( function_exists( 'wp_cache_delete' ) ) { 296 wp_cache_delete( "linkflow_conversation_{$this->id}", 'linkflow_chat' ); 297 wp_cache_delete( "linkflow_conversations_service_{$this->service_id}", 'linkflow_chat' ); 298 wp_cache_delete( "linkflow_conversations_count_{$this->service_id}", 'linkflow_chat' ); 299 } 300 return true; 301 } 302 303 return false; 274 return $result !== false; 304 275 } 305 276 … … 332 303 global $wpdb; 333 304 $table_name = self::get_table_name(); 334 $cache_key = "linkflow_conversation_{$id}"; 335 if ( function_exists( 'wp_cache_get' ) ) { 336 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 337 if ( $cached ) { 338 return new self( $cached ); 339 } 340 } 341 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 342 $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ), ARRAY_A ); 343 344 if ( $result ) { 345 if ( function_exists( 'wp_cache_set' ) ) { 346 wp_cache_set( $cache_key, $result, 'linkflow_chat' ); 347 } 348 return new self( $result ); 349 } 350 351 return null; 305 306 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 307 $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $id), ARRAY_A); 308 309 return $result ? new self($result) : null; 352 310 } 353 311 … … 361 319 global $wpdb; 362 320 $table_name = self::get_table_name(); 363 $cache_key = "linkflow_conversation_session_{$session_id}"; 364 if ( function_exists( 'wp_cache_get' ) ) { 365 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 366 if ( $cached ) { 367 return new self( $cached ); 368 } 369 } 370 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 371 $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE session_id = %s", $session_id ), ARRAY_A ); 372 373 if ( $result ) { 374 if ( function_exists( 'wp_cache_set' ) ) { 375 wp_cache_set( $cache_key, $result, 'linkflow_chat' ); 376 } 377 return new self( $result ); 378 } 379 380 return null; 321 322 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 323 $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE session_id = %s", $session_id), ARRAY_A); 324 325 return $result ? new self($result) : null; 381 326 } 382 327 … … 402 347 403 348 // Whitelist ORDER BY column and direction to avoid injection 404 $allowed_orderby = array( 'id', 'last_activity', 'started_at', 'service_id');405 $order_by = in_array( $args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'last_activity';406 $order_direction = strtoupper( $args['order']) === 'ASC' ? 'ASC' : 'DESC';349 $allowed_orderby = array('id', 'last_activity', 'started_at', 'service_id'); 350 $order_by = in_array($args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'last_activity'; 351 $order_direction = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC'; 407 352 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses whitelisted column and static direction 408 353 $order_clause = "ORDER BY {$order_by} {$order_direction}"; 409 // Build explicit query strings so placeholder counts are clear to static analysis tools. 410 // Use a cache keyed by service and args to avoid repeated DB queries. 411 $cache_key = 'linkflow_conversations_service_' . $service_id . '_' . md5( serialize( $args ) ); 412 if ( function_exists( 'wp_cache_get' ) ) { 413 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 414 if ( $cached ) { 415 $conversations = array(); 416 foreach ( $cached as $row ) { 417 $conversations[] = new self( $row ); 418 } 419 return $conversations; 420 } 421 } 422 423 if ( $args['limit'] > 0 ) { 424 if ( $args['offset'] > 0 ) { 354 355 if ($args['limit'] > 0) { 356 if ($args['offset'] > 0) { 425 357 $query = "SELECT * FROM {$table_name} WHERE service_id = %d {$order_clause} LIMIT %d, %d"; 426 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 427 $results = $wpdb->get_results( $wpdb->prepare( $query, $service_id, $args['offset'], $args['limit'] ), ARRAY_A);358 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 359 $results = $wpdb->get_results($wpdb->prepare($query, $service_id, $args['offset'], $args['limit']), ARRAY_A); 428 360 } else { 429 361 $query = "SELECT * FROM {$table_name} WHERE service_id = %d {$order_clause} LIMIT %d"; 430 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 431 $results = $wpdb->get_results( $wpdb->prepare( $query, $service_id, $args['limit'] ), ARRAY_A);362 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 363 $results = $wpdb->get_results($wpdb->prepare($query, $service_id, $args['limit']), ARRAY_A); 432 364 } 433 365 } else { 434 366 $query = "SELECT * FROM {$table_name} WHERE service_id = %d {$order_clause}"; 435 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery436 $results = $wpdb->get_results( $wpdb->prepare( $query, $service_id ), ARRAY_A);367 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 368 $results = $wpdb->get_results($wpdb->prepare($query, $service_id), ARRAY_A); 437 369 } 438 370 439 371 $conversations = array(); 440 foreach ( $results as $result ) { 441 $conversations[] = new self( $result ); 442 } 443 444 if ( function_exists( 'wp_cache_set' ) ) { 445 wp_cache_set( $cache_key, $results, 'linkflow_chat' ); 372 if ($results) { 373 foreach ($results as $result) { 374 $conversations[] = new self($result); 375 } 446 376 } 447 377 … … 498 428 global $wpdb; 499 429 $table_name = self::get_table_name(); 500 $cache_key = "linkflow_conversations_count_{$service_id}"; 501 if ( function_exists( 'wp_cache_get' ) ) { 502 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 503 if ( $cached !== false ) { 504 return (int) $cached; 505 } 506 } 507 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 508 $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d", $service_id ) ); 509 510 if ( function_exists( 'wp_cache_set' ) ) { 511 wp_cache_set( $cache_key, $count, 'linkflow_chat' ); 512 } 430 431 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 432 $count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d", $service_id)); 513 433 514 434 return $count; -
linkflow-chat/trunk/includes/models/class-knowledge-model.php
r3375551 r3417967 102 102 103 103 /** 104 * Get per-service cache version token.105 *106 * This allows easy invalidation of all service-scoped caches when data changes.107 *108 * @param int $service_id109 * @return int110 */111 private static function get_service_cache_version( $service_id ) {112 $key = "linkflow_knowledge_service_version_" . (int) $service_id;113 $version = wp_cache_get( $key, 'linkflow_chat' );114 if ( $version === false ) {115 $version = 1;116 wp_cache_set( $key, $version, 'linkflow_chat' );117 }118 return (int) $version;119 }120 121 /**122 * Increment per-service cache version to invalidate service-scoped caches.123 *124 * @param int $service_id125 * @return void126 */127 private static function increment_service_cache_version( $service_id ) {128 $key = "linkflow_knowledge_service_version_" . (int) $service_id;129 // Try to increment; if the key doesn't exist, initialize it to 2 (previous default 1 -> 2)130 if ( wp_cache_get( $key, 'linkflow_chat' ) === false ) {131 wp_cache_set( $key, 2, 'linkflow_chat' );132 return;133 }134 // wp_cache_incr may not be available in some backends; fall back to set.135 if ( function_exists( 'wp_cache_incr' ) ) {136 wp_cache_incr( $key, 1, 'linkflow_chat' );137 } else {138 $v = wp_cache_get( $key, 'linkflow_chat' );139 $v = ( $v ? (int) $v + 1 : 2 );140 wp_cache_set( $key, $v, 'linkflow_chat' );141 }142 }143 144 /**145 104 * Validate model data 146 105 * … … 210 169 if ( $result !== false ) { 211 170 $this->id = $wpdb->insert_id; 212 // Invalidate caches for this service213 if ( !empty( $this->service_id ) ) {214 self::increment_service_cache_version( $this->service_id );215 }216 171 return $this->id; 217 172 } … … 250 205 'id' => $this->id, 251 206 ), array('%d') ); 252 if ( $result !== false ) { 253 if ( !empty( $this->service_id ) ) { 254 self::increment_service_cache_version( $this->service_id ); 255 } 256 return true; 257 } 258 return false; 207 return $result !== false; 259 208 } 260 209 … … 284 233 global $wpdb; 285 234 $table_name = self::get_table_name(); 286 $id = (int) $id; 287 $cache_key = "linkflow_knowledge_find_{$id}"; 288 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 289 if ( $cached !== false ) { 290 return ( $cached ? new self($cached) : null ); 291 } 292 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 293 $sql = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ); 294 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 295 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 296 $result = $wpdb->get_row( $sql, ARRAY_A ); 297 wp_cache_set( $cache_key, $result, 'linkflow_chat' ); 235 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 236 $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id ), ARRAY_A ); 298 237 return ( $result ? new self($result) : null ); 299 238 } … … 354 293 } 355 294 } 356 // Use a cache key that includes ordering/search/limit/offset and service version 357 $service_version = self::get_service_cache_version( $service_id ); 358 $cache_key = 'linkflow_knowledge_by_service_' . (int) $service_id . '_' . md5( serialize( $args ) ) . '_v' . $service_version; 359 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 360 if ( $cached !== false ) { 361 return $cached; 362 } 363 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 364 $prepared = $wpdb->prepare( $sql, $params ); 365 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 366 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 367 $results = $wpdb->get_results( $prepared, ARRAY_A ); 295 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 296 $results = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A ); 368 297 $knowledge_entries = array(); 369 foreach ( $results as $result ) { 370 $knowledge_entries[] = new self($result); 371 } 372 wp_cache_set( $cache_key, $knowledge_entries, 'linkflow_chat' ); 298 if ( $results ) { 299 foreach ( $results as $result ) { 300 $knowledge_entries[] = new self($result); 301 } 302 } 373 303 return $knowledge_entries; 374 304 } … … 407 337 // Build SQL with placeholders; table name is a known-safe identifier 408 338 $sql = "SELECT *, (CASE WHEN question LIKE %s THEN 2 WHEN answer LIKE %s THEN 1 ELSE 0 END) as relevance_score " . "FROM {$table_name} WHERE service_id = %d AND (question LIKE %s OR answer LIKE %s) " . "ORDER BY relevance_score DESC, created_at DESC LIMIT %d"; 409 $service_version = self::get_service_cache_version( $service_id ); 410 $cache_key = 'linkflow_knowledge_search_' . (int) $service_id . '_' . md5( $query . '|' . (int) $limit ) . '_v' . $service_version; 411 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 412 if ( $cached !== false ) { 413 return $cached; 414 } 415 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 416 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 339 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 417 340 $results = $wpdb->get_results( $wpdb->prepare( 418 341 $sql, … … 425 348 ), ARRAY_A ); 426 349 $knowledge_entries = array(); 427 foreach ( $results as $result ) { 428 $knowledge_entries[] = new self($result); 429 } 430 wp_cache_set( $cache_key, $knowledge_entries, 'linkflow_chat' ); 350 if ( $results ) { 351 foreach ( $results as $result ) { 352 $knowledge_entries[] = new self($result); 353 } 354 } 431 355 return $knowledge_entries; 432 356 } … … 441 365 global $wpdb; 442 366 $table_name = self::get_table_name(); 443 $service_version = self::get_service_cache_version( $service_id ); 444 $cache_key = 'linkflow_knowledge_count_' . (int) $service_id . '_v' . $service_version; 445 $cached = wp_cache_get( $cache_key, 'linkflow_chat' ); 446 if ( $cached !== false ) { 447 return (int) $cached; 448 } 449 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 450 $sql = $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d", (int) $service_id ); 451 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 452 $count = (int) $wpdb->get_var( $sql ); 453 wp_cache_set( $cache_key, $count, 'linkflow_chat' ); 367 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 368 $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d", (int) $service_id ) ); 454 369 return $count; 455 370 } … … 480 395 */ 481 396 public static function bulk_insert( $service_id, $entries ) { 482 global $wpdb;483 $table_name = self::get_table_name();484 397 $limit_check = self::can_add_more_entries( $service_id ); 485 398 if ( !$limit_check['can_add'] ) { … … 503 416 } 504 417 } 505 if ( $inserted > 0 ) {506 self::increment_service_cache_version( $service_id );507 }508 418 return $inserted; 509 419 } … … 522 432 'service_id' => $service_id, 523 433 ), array('%d') ); 524 if ( $result !== false ) { 525 // Invalidate caches for this service 526 self::increment_service_cache_version( $service_id ); 527 return $result; 528 } 529 return 0; 434 return ( $result !== false ? $result : 0 ); 530 435 } 531 436 -
linkflow-chat/trunk/includes/models/class-message-model.php
r3375551 r3417967 60 60 */ 61 61 private static $valid_types = array('user', 'ai', 'system'); 62 /**63 * Cache group name for object cache entries64 *65 * @var string66 */67 private static $cache_group = 'linkflow_chat_messages';68 62 69 63 /** … … 186 180 if ($result !== false) { 187 181 $this->id = $wpdb->insert_id; 188 // Clear related caches for this conversation189 if ( function_exists( 'wp_cache_get' ) ) {190 $index_key = sprintf( 'conversation:%d:index', (int) $this->conversation_id );191 $index = (array) wp_cache_get( $index_key, self::$cache_group );192 if ( ! empty( $index ) ) {193 foreach ( $index as $key ) {194 wp_cache_delete( $key, self::$cache_group );195 }196 }197 wp_cache_delete( $index_key, self::$cache_group );198 // Delete any cached id entry for this message199 wp_cache_delete( "id:{$this->id}", self::$cache_group );200 }201 202 182 return $this->id; 203 183 } … … 215 195 ); 216 196 217 if ( $result !== false ) { 218 // Invalidate caches for this conversation and this message id 219 if ( function_exists( 'wp_cache_get' ) ) { 220 $index_key = sprintf( 'conversation:%d:index', (int) $this->conversation_id ); 221 $index = (array) wp_cache_get( $index_key, self::$cache_group ); 222 if ( ! empty( $index ) ) { 223 foreach ( $index as $key ) { 224 wp_cache_delete( $key, self::$cache_group ); 225 } 226 } 227 wp_cache_delete( $index_key, self::$cache_group ); 228 wp_cache_delete( "id:{$id}", self::$cache_group ); 229 } 230 231 return true; 232 } 233 234 return false; 197 return $result !== false; 235 198 } 236 199 … … 250 213 global $wpdb; 251 214 $table_name = self::get_table_name(); 252 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 215 216 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 253 217 $result = $wpdb->delete( 254 218 $table_name, … … 256 220 array('%d') 257 221 ); 258 if ( $result !== false && function_exists( 'wp_cache_get' ) ) {259 // Invalidate caches for this conversation and this message id260 $index_key = sprintf( 'conversation:%d:index', (int) $this->conversation_id );261 $index = (array) wp_cache_get( $index_key, self::$cache_group );262 if ( ! empty( $index ) ) {263 foreach ( $index as $key ) {264 wp_cache_delete( $key, self::$cache_group );265 }266 }267 wp_cache_delete( $index_key, self::$cache_group );268 wp_cache_delete( "id:{$this->id}", self::$cache_group );269 }270 222 271 223 return $result !== false; … … 296 248 global $wpdb; 297 249 $table_name = self::get_table_name(); 298 // Try object cache first 299 $cache_key = "id:{$id}"; 300 if ( function_exists( 'wp_cache_get' ) ) { 301 $cached = wp_cache_get( $cache_key, self::$cache_group ); 302 if ( $cached !== false ) { 303 return $cached ? new self( $cached ) : null; 304 } 305 } 306 307 // Prepared select by ID 308 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 309 $result = $wpdb->get_row( 310 $wpdb->prepare( 311 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe 312 "SELECT * FROM {$table_name} WHERE id = %d", 313 $id 314 ), 315 ARRAY_A 316 ); 317 318 if ( function_exists( 'wp_cache_set' ) ) { 319 // Cache raw array or false 320 wp_cache_set( $cache_key, $result, self::$cache_group, 300 ); 321 } 250 251 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 252 $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $id), ARRAY_A); 322 253 323 254 return $result ? new self($result) : null; … … 347 278 // Build where clause and params for a single prepare 348 279 $where = 'conversation_id = %d'; 349 $params = array( $conversation_id);280 $params = array($conversation_id); 350 281 351 282 if (!empty($args['type']) && in_array($args['type'], self::$valid_types)) { … … 355 286 356 287 // Whitelist ORDER BY field and direction 357 $allowed_orderby = array( 'id', 'created_at', 'conversation_id', 'message_type');358 $order_by = in_array( $args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'created_at';359 $order_dir = ( strtoupper( $args['order'] ) === 'ASC') ? 'ASC' : 'DESC';288 $allowed_orderby = array('id', 'created_at', 'conversation_id', 'message_type'); 289 $order_by = in_array($args['orderby'], $allowed_orderby, true) ? $args['orderby'] : 'created_at'; 290 $order_dir = (strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC'; 360 291 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses a whitelisted column and static direction 361 292 $order_clause = "ORDER BY {$order_by} {$order_dir}"; … … 377 308 $query = 'SELECT * FROM ' . $table_name . ' WHERE ' . $where . ' ' . $order_clause . ' ' . $limit_sql; 378 309 379 // Build a cache key for this query 380 $cache_key = sprintf( 381 'conversation:%d:limit:%d:offset:%d:orderby:%s:order:%s:type:%s', 382 (int) $conversation_id, 383 (int) $limit, 384 (int) $offset, 385 $order_by, 386 $order_dir, 387 isset($args['type']) ? $args['type'] : '' 388 ); 389 390 if ( function_exists( 'wp_cache_get' ) ) { 391 $cached = wp_cache_get( $cache_key, self::$cache_group ); 392 if ( $cached !== false ) { 393 $messages = array(); 394 foreach ( $cached as $result ) { 395 $messages[] = new self( $result ); 396 } 397 398 return $messages; 310 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 311 $results = $wpdb->get_results($wpdb->prepare($query, ...$params), ARRAY_A); 312 313 $messages = array(); 314 if ($results) { 315 foreach ($results as $result) { 316 $messages[] = new self($result); 399 317 } 400 }401 402 // Always use prepare with parameters403 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery404 $results = $wpdb->get_results( $wpdb->prepare( $query, ...$params ), ARRAY_A );405 406 if ( function_exists( 'wp_cache_set' ) ) {407 wp_cache_set( $cache_key, $results, self::$cache_group, 300 );408 // register index for invalidation409 $index_key = sprintf( 'conversation:%d:index', (int) $conversation_id );410 $index = (array) wp_cache_get( $index_key, self::$cache_group );411 if ( ! in_array( $cache_key, $index, true ) ) {412 $index[] = $cache_key;413 wp_cache_set( $index_key, $index, self::$cache_group, 0 );414 }415 }416 417 $messages = array();418 foreach ($results as $result) {419 $messages[] = new self($result);420 318 } 421 319 … … 436 334 // Build where and params for count 437 335 $where = 'conversation_id = %d'; 438 $params = array( $conversation_id);336 $params = array($conversation_id); 439 337 440 338 if (!empty($type) && in_array($type, self::$valid_types)) { … … 445 343 $query = 'SELECT COUNT(*) FROM ' . $table_name . ' WHERE ' . $where; 446 344 447 $cache_key = sprintf('count:conversation:%d:type:%s', (int) $conversation_id, $type ? $type : ''); 448 if ( function_exists( 'wp_cache_get' ) ) { 449 $cached = wp_cache_get( $cache_key, self::$cache_group ); 450 if ( $cached !== false ) { 451 return (int) $cached; 452 } 453 } 454 455 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery 456 $count = (int) $wpdb->get_var( $wpdb->prepare( $query, ...$params ) ); 457 458 if ( function_exists( 'wp_cache_set' ) ) { 459 wp_cache_set( $cache_key, $count, self::$cache_group, 300 ); 460 $index_key = sprintf( 'conversation:%d:index', (int) $conversation_id ); 461 $index = (array) wp_cache_get( $index_key, self::$cache_group ); 462 if ( ! in_array( $cache_key, $index, true ) ) { 463 $index[] = $cache_key; 464 wp_cache_set( $index_key, $index, self::$cache_group, 0 ); 465 } 466 } 345 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 346 $count = (int) $wpdb->get_var($wpdb->prepare($query, ...$params)); 467 347 468 348 return $count; … … 478 358 global $wpdb; 479 359 $table_name = self::get_table_name(); 480 // Prepared latest message by conversation 481 $cache_key = sprintf('latest:conversation:%d', (int) $conversation_id ); 482 if ( function_exists( 'wp_cache_get' ) ) { 483 $cached = wp_cache_get( $cache_key, self::$cache_group ); 484 if ( $cached !== false ) { 485 return $cached ? new self( $cached ) : null; 486 } 487 } 488 489 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 490 $result = $wpdb->get_row( 491 $wpdb->prepare( 492 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe 493 "SELECT * FROM {$table_name} WHERE conversation_id = %d ORDER BY created_at DESC LIMIT 1", 494 $conversation_id 495 ), 496 ARRAY_A 497 ); 498 499 if ( function_exists( 'wp_cache_set' ) ) { 500 wp_cache_set( $cache_key, $result, self::$cache_group, 300 ); 501 $index_key = sprintf( 'conversation:%d:index', (int) $conversation_id ); 502 $index = (array) wp_cache_get( $index_key, self::$cache_group ); 503 if ( ! in_array( $cache_key, $index, true ) ) { 504 $index[] = $cache_key; 505 wp_cache_set( $index_key, $index, self::$cache_group, 0 ); 506 } 507 } 360 361 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 362 $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE conversation_id = %d ORDER BY created_at DESC LIMIT 1", $conversation_id), ARRAY_A); 508 363 509 364 return $result ? new self($result) : null; … … 519 374 global $wpdb; 520 375 $table_name = self::get_table_name(); 521 // Prepared deletion of old messages 522 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 523 $affected_conversations = $wpdb->get_col( 524 $wpdb->prepare( 525 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe 526 "SELECT DISTINCT conversation_id FROM {$table_name} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", 527 absint( $days ) 528 ) 529 ); 530 531 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 532 $result = $wpdb->query( 533 $wpdb->prepare( 534 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is safe 535 "DELETE FROM {$table_name} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", 536 absint($days) 537 ) 538 ); 539 540 if ( $result !== false && ! empty( $affected_conversations ) && function_exists( 'wp_cache_get' ) ) { 541 foreach ( $affected_conversations as $conv_id ) { 542 $index_key = sprintf( 'conversation:%d:index', (int) $conv_id ); 543 $index = (array) wp_cache_get( $index_key, self::$cache_group ); 544 if ( ! empty( $index ) ) { 545 foreach ( $index as $key ) { 546 wp_cache_delete( $key, self::$cache_group ); 547 } 548 } 549 wp_cache_delete( $index_key, self::$cache_group ); 550 } 551 } 376 377 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 378 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", absint($days))); 552 379 553 380 return $result !== false ? $result : 0; -
linkflow-chat/trunk/linkflow-chat.php
r3400911 r3417967 5 5 * Plugin URI: https://linkflow.chat 6 6 * Description: LinkFlow Chat integrates advanced AI like ChatGPT into WordPress for smart conversations. It seamlessly hands off to WhatsApp and social platforms when needed, not only boosting satisfaction but also helping you grow your follower count and community. 7 * Version: 1.0. 77 * Version: 1.0.8 8 8 * Author: LinkFlow.chat 9 9 * License: GPL v2 or later … … 49 49 do_action( 'linkflowChat_fs_loaded' ); 50 50 // Define plugin constants 51 define( 'LINKFLOW_CHAT_VERSION', '1.0. 7' );51 define( 'LINKFLOW_CHAT_VERSION', '1.0.8' ); 52 52 define( 'LINKFLOW_CHAT_PLUGIN_FILE', __FILE__ ); 53 53 define( 'LINKFLOW_CHAT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
linkflow-chat/trunk/plugin-info.json
r3400911 r3417967 2 2 "name": "LinkFlow Chat – AI Chatbot With 20+ Social Media Buttons & Human Support", 3 3 "slug": "linkflow-chat", 4 "version": "1.0. 7",4 "version": "1.0.8", 5 5 "requires": "5.9", 6 6 "tested": "6.8", 7 7 "requires_php": "7.0", 8 "last_updated": "2025-1 1-22 11:46:07",8 "last_updated": "2025-12-05 10:57:43", 9 9 "sections": { 10 10 "description": "LinkFlow Chat integrates advanced AI like ChatGPT into WordPress for smart conversations. It seamlessly hands off to WhatsApp and social platforms when needed, not only boosting satisfaction but also helping you grow your follower count and community.", -
linkflow-chat/trunk/readme.txt
r3400911 r3417967 6 6 Tested up to: 6.8 7 7 Requires PHP: 7.0 8 Stable tag: 1.0. 78 Stable tag: 1.0.8 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 54 54 55 55 == Changelog == 56 = 1.0. 7=56 = 1.0.8 = 57 57 * Fix known issues 58 = 1.0.6 =59 * Fix known issues60 = 1.0.5 =61 * Fix known issues62 = 1.0.4 =63 * Fix known issues64 = 1.0.3 =65 * Fix UI issues66 58 = 1.0.2 = 67 59 * Supported version bump to 6.8 -
linkflow-chat/trunk/version.json
r3400911 r3417967 1 1 { 2 "version": "1.0. 7",3 "build_date": "2025-1 1-22T11:46:07Z",4 "build_hash": "af aa53c50a7ac1cc32ddec01d9fbf28bb6cb241f",2 "version": "1.0.8", 3 "build_date": "2025-12-05T10:57:43Z", 4 "build_hash": "af978f13f655bb15e5156c7c0d912595221587eb", 5 5 "php_version_required": "7.0", 6 6 "wordpress_version_required": "5.9",
Note: See TracChangeset
for help on using the changeset viewer.