| @@ -96,16 +96,16 @@ | ||
| 96 | 96 | */ |
| 97 | 97 | public function query(string $query, array $args = []) { |
| 98 | 98 | if (!empty($args)) { |
| 99 | 99 | // Prepare the query first, then execute |
| 100 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared in the line below | |
| 100 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared in the line below | |
| 101 | 101 | $prepared_query = $this->wpdb->prepare($query, $args); |
| 102 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Using prepared query from above | |
| 102 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Using prepared query from above | |
| 103 | 103 | return $this->wpdb->query($prepared_query); |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | 106 | // For queries without parameters, execute directly (safe for static queries) |
| 107 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- No user input in static queries | |
| 107 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- No user input in static queries | |
| 108 | 108 | return $this->wpdb->query($query); |
| 109 | 109 | } |
| 110 | 110 | |
| 111 | 111 | /** |
| @@ -118,16 +118,16 @@ | ||
| 118 | 118 | */ |
| 119 | 119 | public function get_row(string $query, array $args = [], string $output = OBJECT) { |
| 120 | 120 | if (!empty($args)) { |
| 121 | 121 | // Prepare the query first, then execute |
| 122 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared in the line below | |
| 122 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared in the line below | |
| 123 | 123 | $prepared_query = $this->wpdb->prepare($query, $args); |
| 124 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Using prepared query from above | |
| 124 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Using prepared query from above | |
| 125 | 125 | return $this->wpdb->get_row($prepared_query, $output); |
| 126 | 126 | } |
| 127 | 127 | |
| 128 | 128 | // For queries without parameters, execute directly (safe for static queries) |
| 129 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- No user input in static queries | |
| 129 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- No user input in static queries | |
| 130 | 130 | return $this->wpdb->get_row($query, $output); |
| 131 | 131 | } |
| 132 | 132 | |
| 133 | 133 | /** |
| @@ -140,15 +140,15 @@ | ||
| 140 | 140 | */ |
| 141 | 141 | public function get_results(string $query, array $args = [], string $output = OBJECT): array { |
| 142 | 142 | if (!empty($args)) { |
| 143 | 143 | // Prepare the query first, then execute |
| 144 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared in the line below | |
| 144 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared in the line below | |
| 145 | 145 | $prepared_query = $this->wpdb->prepare($query, $args); |
| 146 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Using prepared query from above | |
| 146 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Using prepared query from above | |
| 147 | 147 | $results = $this->wpdb->get_results($prepared_query, $output); |
| 148 | 148 | } else { |
| 149 | 149 | // For queries without parameters, execute directly (safe for static queries) |
| 150 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- No user input in static queries | |
| 150 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- No user input in static queries | |
| 151 | 151 | $results = $this->wpdb->get_results($query, $output); |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | return is_array($results) ? $results : []; |
| @@ -163,16 +163,16 @@ | ||
| 163 | 163 | */ |
| 164 | 164 | public function get_var(string $query, array $args = []) { |
| 165 | 165 | if (!empty($args)) { |
| 166 | 166 | // Prepare the query first, then execute |
| 167 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared in the line below | |
| 167 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared in the line below | |
| 168 | 168 | $prepared_query = $this->wpdb->prepare($query, $args); |
| 169 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Using prepared query from above | |
| 169 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Using prepared query from above | |
| 170 | 170 | return $this->wpdb->get_var($prepared_query); |
| 171 | 171 | } |
| 172 | 172 | |
| 173 | 173 | // For queries without parameters, execute directly (safe for static queries) |
| 174 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- No user input in static queries | |
| 174 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- No user input in static queries | |
| 175 | 175 | return $this->wpdb->get_var($query); |
| 176 | 176 | } |
| 177 | 177 | |
| 178 | 178 | /** |
| @@ -303,11 +303,13 @@ | ||
| 303 | 303 | // Use %i placeholder for table identifier (WordPress 6.2+) |
| 304 | 304 | // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder |
| 305 | 305 | $deleted = $this->wpdb->query( |
| 306 | 306 | $this->wpdb->prepare( |
| 307 | - 'DELETE FROM %i WHERE expires_at < %s', | |
| 307 | + // expires_at is a bigint unix timestamp (see Cache_Manager), | |
| 308 | + // so compare against time(), not a MySQL datetime string. | |
| 309 | + 'DELETE FROM %i WHERE expires_at < %d', | |
| 308 | 310 | $cache_table, |
| 309 | - current_time('mysql') | |
| 311 | + time() | |
| 310 | 312 | ) |
| 311 | 313 | ); |
| 312 | 314 | // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder |
| 313 | 315 | } else { |
| @@ -315,10 +317,10 @@ | ||
| 315 | 317 | $escaped_table = esc_sql($cache_table); |
| 316 | 318 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared |
| 317 | 319 | $deleted = $this->wpdb->query( |
| 318 | 320 | $this->wpdb->prepare( |
| 319 | - "DELETE FROM `{$escaped_table}` WHERE expires_at < %s", | |
| 320 | - current_time('mysql') | |
| 321 | + "DELETE FROM `{$escaped_table}` WHERE expires_at < %d", | |
| 322 | + time() | |
| 321 | 323 | ) |
| 322 | 324 | ); |
| 323 | 325 | // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared |
| 324 | 326 | } |
| @@ -350,9 +352,9 @@ | ||
| 350 | 352 | // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder |
| 351 | 353 | } else { |
| 352 | 354 | // Fallback for older WordPress versions - table name is from our controlled list |
| 353 | 355 | $escaped_table = esc_sql($table); |
| 354 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly escaped and from controlled source | |
| 356 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly escaped and from controlled source | |
| 355 | 357 | $count = $this->wpdb->get_var("SELECT COUNT(*) FROM `{$escaped_table}`"); |
| 356 | 358 | } |
| 357 | 359 | $stats[$key] = (int) $count; |
| 358 | 360 | } |
| @@ -441,12 +443,13 @@ | ||
| 441 | 443 | $results = []; |
| 442 | 444 | |
| 443 | 445 | // Define retention periods (in days) |
| 444 | 446 | $retention_config = [ |
| 445 | - 'ai_usage' => 365, // 1 year | |
| 446 | - 'seo_scores' => 180, // 6 months | |
| 447 | - 'content_briefs' => 90, // 3 months | |
| 448 | - 'seo_performance' => 90 // 3 months (performance data grows fast) | |
| 447 | + 'ai_usage' => 365, // 1 year | |
| 448 | + 'seo_scores' => 180, // 6 months | |
| 449 | + 'content_briefs' => 90, // 3 months | |
| 450 | + 'seo_performance' => 90, // 3 months (performance data grows fast) | |
| 451 | + 'instant_indexing_logs' => 90 // 3 months (one row per URL per submit) | |
| 449 | 452 | ]; |
| 450 | 453 | |
| 451 | 454 | foreach ($retention_config as $table_key => $retention_days) { |
| 452 | 455 | try { |
| @@ -508,15 +511,15 @@ | ||
| 508 | 511 | |
| 509 | 512 | $date_column = $date_columns[$table_key] ?? 'created_at'; |
| 510 | 513 | |
| 511 | 514 | // Delete old records |
| 512 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Analytics cleanup requires direct database access, table and column names are validated internally | |
| 515 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Analytics cleanup requires direct database access, table and column names are validated internally | |
| 513 | 516 | $deleted = $this->wpdb->query( |
| 514 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is properly prepared with placeholders | |
| 517 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- SQL is properly prepared with placeholders | |
| 515 | 518 | $this->wpdb->prepare( |
| 516 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table and column names are validated internally | |
| 517 | - "DELETE FROM `{$table_name}` WHERE {$date_column} < %s", | |
| 518 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $cutoff_date is validated and used as parameter | |
| 519 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table and column names are validated internally | |
| 520 | + "DELETE FROM `{$table_name}` WHERE `{$date_column}` < %s", | |
| 521 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $cutoff_date is validated and used as parameter | |
| 519 | 522 | $cutoff_date |
| 520 | 523 | ) |
| 521 | 524 | ); |
| 522 | 525 | |
| @@ -548,15 +551,19 @@ | ||
| 548 | 551 | } |
| 549 | 552 | } |
| 550 | 553 | |
| 551 | 554 | // Also clear any user-specific analytics cache |
| 552 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache cleanup requires direct database access | |
| 553 | - $user_cache_deleted = $this->wpdb->query( | |
| 554 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- wpdb->options is WordPress core table, safe to use | |
| 555 | - "DELETE FROM {$this->wpdb->options} | |
| 556 | - WHERE option_name LIKE '_transient_thinkrank_analytics_%' | |
| 557 | - OR option_name LIKE '_transient_timeout_thinkrank_analytics_%'" | |
| 555 | + $analytics_like = $this->wpdb->esc_like('_transient_thinkrank_analytics_') . '%'; | |
| 556 | + $timeout_like = $this->wpdb->esc_like('_transient_timeout_thinkrank_analytics_') . '%'; | |
| 557 | + $options_table = $this->wpdb->options; | |
| 558 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $options_table is from $wpdb->options, a WordPress core table name. | |
| 559 | + $prepared_sql = $this->wpdb->prepare( | |
| 560 | + "DELETE FROM {$options_table} WHERE option_name LIKE %s OR option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 561 | + $analytics_like, | |
| 562 | + $timeout_like | |
| 558 | 563 | ); |
| 564 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared above. | |
| 565 | + $user_cache_deleted = $this->wpdb->query( $prepared_sql ); | |
| 559 | 566 | |
| 560 | 567 | return [ |
| 561 | 568 | 'transients_cleared' => $cleared_count, |
| 562 | 569 | 'user_cache_cleared' => $user_cache_deleted !== false ? (int) $user_cache_deleted : 0, |
| @@ -582,21 +589,21 @@ | ||
| 582 | 589 | |
| 583 | 590 | foreach ($tables as $table_key) { |
| 584 | 591 | $table_name = $this->get_table($table_key); |
| 585 | 592 | |
| 586 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table size monitoring requires direct database access | |
| 587 | - $size_result = $this->wpdb->get_row( | |
| 588 | - $this->wpdb->prepare( | |
| 589 | - "SELECT | |
| 590 | - table_name, | |
| 591 | - ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb, | |
| 592 | - table_rows | |
| 593 | - FROM information_schema.TABLES | |
| 594 | - WHERE table_schema = %s AND table_name = %s", | |
| 595 | - DB_NAME, | |
| 596 | - $table_name | |
| 597 | - ) | |
| 593 | + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared with placeholders below. | |
| 594 | + $prepared_sql = $this->wpdb->prepare( | |
| 595 | + "SELECT | |
| 596 | + table_name, | |
| 597 | + ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb, | |
| 598 | + table_rows | |
| 599 | + FROM information_schema.TABLES | |
| 600 | + WHERE table_schema = %s AND table_name = %s", | |
| 601 | + DB_NAME, | |
| 602 | + $table_name | |
| 598 | 603 | ); |
| 604 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is prepared above. | |
| 605 | + $size_result = $this->wpdb->get_row( $prepared_sql ); | |
| 599 | 606 | |
| 600 | 607 | if ($size_result) { |
| 601 | 608 | $sizes[$table_key] = [ |
| 602 | 609 | 'size_mb' => (float) $size_result->size_mb, |