Plugin Directory

Changeset 3417967


Ignore:
Timestamp:
12/12/2025 07:25:58 AM (4 months ago)
Author:
linkflow
Message:

Fix cache issue

Location:
linkflow-chat/trunk
Files:
14 edited

Legend:

Unmodified
Added
Removed
  • linkflow-chat/trunk/assets/admin/js/knowledge.js

    r3375551 r3417967  
    11(function($){
    22  $(function(){
    3     // Search functionality
    4     $('#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 removed
    16 
    173    // Textarea auto-resize
    184    $('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  
    915915        $selected_service = null;
    916916        $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')) : '';
    919917       
    920918        if ($service_id > 0) {
    921919            $selected_service = AI_Service_Model::find($service_id);
    922920            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);
    927922            }
    928923        }
     
    12371232       
    12381233        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);
    12421244            exit;
    12431245        } 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);
    12471256            exit;
    12481257        }
  • linkflow-chat/trunk/includes/admin/views/knowledge-list.php

    r3375551 r3417967  
    4848?>
    4949            </select>
    50            
    51             <?php
    52 if ( $selected_service ) {
    53     ?>
    54                 <input type="search" id="knowledge-search" name="search" value="<?php
    55     echo esc_attr( $search );
    56     ?>" placeholder="<?php
    57     esc_attr_e( 'Search knowledge base...', 'linkflow-chat' );
    58     ?>">
    59                 <button type="button" id="search-submit" class="button"><?php
    60     esc_html_e( 'Search', 'linkflow-chat' );
    61     ?></button>
    62             <?php
    63 }
    64 ?>
    6550        </div>
    6651       
  • linkflow-chat/trunk/includes/class-database-manager.php

    r3375551 r3417967  
    222222    public function tables_exist() {
    223223        global $wpdb;
    224         // Cache the result for a short period to avoid frequent direct DB checks
    225         $cache_key = 'linkflow_chat_tables_exist';
    226         $cached = wp_cache_get($cache_key);
    227         if ($cached !== false) {
    228             return (bool) $cached;
    229         }
    230224
    231225        $tables = array(
     
    237231
    238232        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));
    242235            if ($result !== $table) {
    243                 wp_cache_set($cache_key, false, '', 300);
    244236                return false;
    245237            }
    246238        }
    247239
    248         wp_cache_set($cache_key, true, '', 300);
    249240        return true;
    250241    }
     
    344335     */
    345336    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);
    373354        }
    374355       
     
    404385   
    405386    /**
    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',
    415401        );
    416402       
    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                )
    441417            );
    442418           
    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;
    471425       
    472426        return $size_info;
  • linkflow-chat/trunk/includes/class-qa-manager.php

    r3375551 r3417967  
    6060            $result = $knowledge->save();
    6161            if ( $result ) {
    62                 // Clear related caches for this service and question
    63                 self::clear_service_cache( $service_id );
    64                 self::clear_duplicate_cache( $service_id, $knowledge->question );
    6562                return array(
    6663                    'success' => true,
     
    122119            $result = $knowledge->save();
    123120            if ( $result ) {
    124                 // Clear related caches for this service and question
    125                 self::clear_service_cache( $knowledge->service_id );
    126                 self::clear_duplicate_cache( $knowledge->service_id, $knowledge->question );
    127121                return array(
    128122                    'success' => true,
     
    163157            $result = $knowledge->delete();
    164158            if ( $result ) {
    165                 // Clear related caches for this service and question
    166                 self::clear_service_cache( $knowledge->service_id );
    167                 self::clear_duplicate_cache( $knowledge->service_id, $knowledge->question );
    168159                return array(
    169160                    'success' => true,
     
    230221    }
    231222
    232     // Knowledge import removed
    233     /**
    234      * Export QA entries to various formats
    235      *
    236      * @param int $service_id
    237      * @param string $format
    238      * @return array Result with success status and exported data
    239      */
    240     // Knowledge export removed
    241223    /**
    242224     * Search QA entries
     
    265247    public static function get_qa_stats( $service_id ) {
    266248        $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 ) );
    288255        return array(
    289256            'total_entries'       => $total_count,
     
    303270     */
    304271    private static function is_duplicate_question( $service_id, $question, $exclude_id = null ) {
    305         // Cache duplicate checks per service + question + exclude_id
    306         $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         }
    312272        global $wpdb;
    313273        $table_name = Knowledge_Model::get_table_name();
    314         // Escape table name since table names cannot be used as placeholders in prepare().
    315274        $table_name = esc_sql( $table_name );
    316275        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
    318277            $count = $wpdb->get_var( $wpdb->prepare(
    319                 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    320278                "SELECT COUNT(*) FROM {$table_name} WHERE service_id = %d AND question = %s AND id != %d",
    321279                $service_id,
     
    324282            ) );
    325283        } 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;
    342288    }
    343289
     
    430376
    431377    /**
    432      * Generate CSV data
    433      *
    434      * @param array $entries
    435      * @return string
    436      */
    437     // CSV generator removed
    438     /**
    439378     * Get average question length
    440379     *
     
    443382     */
    444383    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 ) );
    466389        return floatval( $avg );
    467390    }
     
    474397     */
    475398    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 ) );
    497404        return floatval( $avg );
    498405    }
    499406
    500     /**
    501      * Clear caches related to a service ID
    502      *
    503      * @param int $service_id
    504      * @return void
    505      */
    506     private static function clear_service_cache( $service_id ) {
    507         // Clear known keys; Knowledge_Model may also have its own cache keys
    508         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_Model
    512         wp_cache_delete( "linkflow_qa_list_{$service_id}", 'linkflow-chat' );
    513     }
    514 
    515     /**
    516      * Clear duplicate check cache for a specific question
    517      *
    518      * @param int $service_id
    519      * @param string $question
    520      * @return void
    521      */
    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 
    529407}
  • linkflow-chat/trunk/includes/class-version-manager.php

    r3375551 r3417967  
    398398     */
    399399    private function clear_upgrade_caches() {
    400         // Clear WordPress caches
     400        // Clear WordPress object cache
    401401        if (function_exists('wp_cache_flush')) {
    402402            wp_cache_flush();
    403403        }
    404        
    405         // Clear plugin-specific caches
    406         wp_cache_delete('linkflow_chat_table_stats');
    407         wp_cache_delete('linkflow_chat_active_services');
    408404       
    409405        // Clear transients
  • linkflow-chat/trunk/includes/models/class-ai-service-model.php

    r3375551 r3417967  
    1818     */
    1919    private static $table_name = 'plugin_linkflow_chat_services';
    20     /**
    21      * Cache group for object cache
    22      *
    23      * @var string
    24      */
    25     private static $cache_group = 'linkflow_chat';
    2620   
    2721    /**
     
    142136        foreach ($fillable as $field) {
    143137            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                }
    145144            }
    146145        }
     
    155154        global $wpdb;
    156155        return $wpdb->prefix . self::$table_name;
    157     }
    158 
    159     /**
    160      * Generate cache key for ID
    161      *
    162      * @param int $id
    163      * @return string
    164      */
    165     private static function get_cache_key_by_id( $id ) {
    166         return "ai_service_id_{$id}";
    167     }
    168 
    169     /**
    170      * Generate cache key for name
    171      *
    172      * @param string $name
    173      * @return string
    174      */
    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 $args
    183      * @return string
    184      */
    185     private static function get_cache_key_for_args( $args ) {
    186         return 'ai_service_list_' . md5( serialize( $args ) );
    187156    }
    188157   
     
    276245            if ($result !== false) {
    277246                $this->id = $wpdb->insert_id;
    278                 // Invalidate related caches
    279                 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 );
    282247                return $this->id;
    283248            }
     
    295260            );
    296261           
    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) {
    304263                return true;
    305264            }
     
    323282        global $wpdb;
    324283        $table_name = self::get_table_name();
    325             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     284        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
    326285        $result = $wpdb->delete(
    327286            $table_name,
     
    330289        );
    331290
    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;
    343292    }
    344293   
     
    375324        global $wpdb;
    376325        $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);
    389329       
    390330        return $result ? new self($result) : null;
     
    400340        global $wpdb;
    401341        $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;
    416347    }
    417348   
     
    434365       
    435366        $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         }
    447367       
    448368        // 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';
    452372        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses whitelisted column and static direction
    453373        $order_clause = "ORDER BY {$order_by} {$order_direction}";
    454374
    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);
    464379            } 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);
    467382            }
    468 
    469383        } 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);
    473386        }
    474387       
    475388        $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        }
    482394       
    483395        return $services;
  • linkflow-chat/trunk/includes/models/class-conversation-model.php

    r3375551 r3417967  
    176176        }
    177177       
    178     $table_name = self::get_table_name();
     178        $table_name = self::get_table_name();
    179179        $data = $this->to_array();
    180180       
     
    190190            if ($result !== false) {
    191191                $this->id = $wpdb->insert_id;
    192                 // Prime cache for the newly created conversation
    193                 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 service
    196                     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 
    200192                return $this->id;
    201193            }
     
    213205            );
    214206
    215             if ( $result !== false ) {
    216                 // Update cached conversation and clear related lists/counts
    217                 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 
    224207            return $result !== false;
    225208        }
     
    240223        global $wpdb;
    241224        $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
    244228        $result = $wpdb->update(
    245229            $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) {
    253237            $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             }
    258238            return true;
    259239        }
     
    276256        // Delete associated messages first
    277257        $messages_table = $wpdb->prefix . 'plugin_linkflow_chat_messages';
    278             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     258        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    279259        $wpdb->delete(
    280260            $messages_table,
     
    285265        // Delete conversation
    286266        $table_name = self::get_table_name();
    287             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     267        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    288268        $result = $wpdb->delete(
    289269            $table_name,
     
    292272        );
    293273       
    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;
    304275    }
    305276   
     
    332303        global $wpdb;
    333304        $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;
    352310    }
    353311   
     
    361319        global $wpdb;
    362320        $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;
    381326    }
    382327   
     
    402347       
    403348        // 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';
    407352        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses whitelisted column and static direction
    408353        $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) {
    425357                $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);
    428360            } else {
    429361                $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);
    432364            }
    433365        } else {
    434366            $query = "SELECT * FROM {$table_name} WHERE service_id = %d {$order_clause}";
    435                 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery
    436             $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);
    437369        }
    438370       
    439371        $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            }
    446376        }
    447377
     
    498428        global $wpdb;
    499429        $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));
    513433
    514434        return $count;
  • linkflow-chat/trunk/includes/models/class-knowledge-model.php

    r3375551 r3417967  
    102102
    103103    /**
    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_id
    109      * @return int
    110      */
    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_id
    125      * @return void
    126      */
    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     /**
    145104     * Validate model data
    146105     *
     
    210169            if ( $result !== false ) {
    211170                $this->id = $wpdb->insert_id;
    212                 // Invalidate caches for this service
    213                 if ( !empty( $this->service_id ) ) {
    214                     self::increment_service_cache_version( $this->service_id );
    215                 }
    216171                return $this->id;
    217172            }
     
    250205            'id' => $this->id,
    251206        ), 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;
    259208    }
    260209
     
    284233        global $wpdb;
    285234        $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 );
    298237        return ( $result ? new self($result) : null );
    299238    }
     
    354293            }
    355294        }
    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 );
    368297        $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        }
    373303        return $knowledge_entries;
    374304    }
     
    407337        // Build SQL with placeholders; table name is a known-safe identifier
    408338        $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
    417340        $results = $wpdb->get_results( $wpdb->prepare(
    418341            $sql,
     
    425348        ), ARRAY_A );
    426349        $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        }
    431355        return $knowledge_entries;
    432356    }
     
    441365        global $wpdb;
    442366        $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 ) );
    454369        return $count;
    455370    }
     
    480395     */
    481396    public static function bulk_insert( $service_id, $entries ) {
    482         global $wpdb;
    483         $table_name = self::get_table_name();
    484397        $limit_check = self::can_add_more_entries( $service_id );
    485398        if ( !$limit_check['can_add'] ) {
     
    503416            }
    504417        }
    505         if ( $inserted > 0 ) {
    506             self::increment_service_cache_version( $service_id );
    507         }
    508418        return $inserted;
    509419    }
     
    522432            'service_id' => $service_id,
    523433        ), 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 );
    530435    }
    531436
  • linkflow-chat/trunk/includes/models/class-message-model.php

    r3375551 r3417967  
    6060     */
    6161    private static $valid_types = array('user', 'ai', 'system');
    62     /**
    63      * Cache group name for object cache entries
    64      *
    65      * @var string
    66      */
    67     private static $cache_group = 'linkflow_chat_messages';
    6862   
    6963    /**
     
    186180            if ($result !== false) {
    187181                $this->id = $wpdb->insert_id;
    188                 // Clear related caches for this conversation
    189                 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 message
    199                     wp_cache_delete( "id:{$this->id}", self::$cache_group );
    200                 }
    201 
    202182                return $this->id;
    203183            }
     
    215195            );
    216196           
    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;
    235198        }
    236199       
     
    250213        global $wpdb;
    251214        $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
    253217        $result = $wpdb->delete(
    254218            $table_name,
     
    256220            array('%d')
    257221        );
    258         if ( $result !== false && function_exists( 'wp_cache_get' ) ) {
    259             // Invalidate caches for this conversation and this message id
    260             $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         }
    270222
    271223        return $result !== false;
     
    296248        global $wpdb;
    297249        $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);
    322253
    323254        return $result ? new self($result) : null;
     
    347278        // Build where clause and params for a single prepare
    348279        $where = 'conversation_id = %d';
    349         $params = array( $conversation_id );
     280        $params = array($conversation_id);
    350281
    351282        if (!empty($args['type']) && in_array($args['type'], self::$valid_types)) {
     
    355286       
    356287        // 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';
    360291        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- ORDER BY uses a whitelisted column and static direction
    361292        $order_clause = "ORDER BY {$order_by} {$order_dir}";
     
    377308        $query = 'SELECT * FROM ' . $table_name . ' WHERE ' . $where . ' ' . $order_clause . ' ' . $limit_sql;
    378309
    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);
    399317            }
    400         }
    401 
    402     // Always use prepare with parameters
    403     // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery
    404     $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 invalidation
    409             $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);
    420318        }
    421319
     
    436334        // Build where and params for count
    437335        $where = 'conversation_id = %d';
    438         $params = array( $conversation_id );
     336        $params = array($conversation_id);
    439337
    440338        if (!empty($type) && in_array($type, self::$valid_types)) {
     
    445343        $query = 'SELECT COUNT(*) FROM ' . $table_name . ' WHERE ' . $where;
    446344
    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));
    467347
    468348        return $count;
     
    478358        global $wpdb;
    479359        $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);
    508363
    509364        return $result ? new self($result) : null;
     
    519374        global $wpdb;
    520375        $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)));
    552379
    553380        return $result !== false ? $result : 0;
  • linkflow-chat/trunk/linkflow-chat.php

    r3400911 r3417967  
    55 * Plugin URI: https://linkflow.chat
    66 * 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.7
     7 * Version: 1.0.8
    88 * Author: LinkFlow.chat
    99 * License: GPL v2 or later
     
    4949do_action( 'linkflowChat_fs_loaded' );
    5050// Define plugin constants
    51 define( 'LINKFLOW_CHAT_VERSION', '1.0.7' );
     51define( 'LINKFLOW_CHAT_VERSION', '1.0.8' );
    5252define( 'LINKFLOW_CHAT_PLUGIN_FILE', __FILE__ );
    5353define( 'LINKFLOW_CHAT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • linkflow-chat/trunk/plugin-info.json

    r3400911 r3417967  
    22    "name": "LinkFlow Chat – AI Chatbot With 20+ Social Media Buttons & Human Support",
    33    "slug": "linkflow-chat",
    4     "version": "1.0.7",
     4    "version": "1.0.8",
    55    "requires": "5.9",
    66    "tested": "6.8",
    77    "requires_php": "7.0",
    8     "last_updated": "2025-11-22 11:46:07",
     8    "last_updated": "2025-12-05 10:57:43",
    99    "sections": {
    1010    "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  
    66Tested up to: 6.8
    77Requires PHP: 7.0
    8 Stable tag: 1.0.7
     8Stable tag: 1.0.8
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    5454
    5555== Changelog ==
    56 = 1.0.7 =
     56= 1.0.8 =
    5757* Fix known issues
    58 = 1.0.6 =
    59 * Fix known issues
    60 = 1.0.5 =
    61 * Fix known issues
    62 = 1.0.4 =
    63 * Fix known issues
    64 = 1.0.3 =
    65 * Fix UI issues
    6658= 1.0.2 =
    6759* Supported version bump to 6.8
  • linkflow-chat/trunk/version.json

    r3400911 r3417967  
    11{
    2     "version": "1.0.7",
    3     "build_date": "2025-11-22T11:46:07Z",
    4     "build_hash": "afaa53c50a7ac1cc32ddec01d9fbf28bb6cb241f",
     2    "version": "1.0.8",
     3    "build_date": "2025-12-05T10:57:43Z",
     4    "build_hash": "af978f13f655bb15e5156c7c0d912595221587eb",
    55        "php_version_required": "7.0",
    66        "wordpress_version_required": "5.9",
Note: See TracChangeset for help on using the changeset viewer.