PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/core/class-database.php +87 -79 1.0.02.7.0 View file →
@@ -1,5 +1,6 @@
1 1 <?php
2 +
2 3 /**
3 4 * Database Manager Class
4 5 *
5 6 * Handles database operations and provides repository pattern
@@ -25,9 +26,9 @@
25 26 *
26 27 * @since 1.0.0
27 28 */
28 29 class Database {
29 -
30 +
30 31 /**
31 32 * WordPress database instance
32 33 *
33 34 * @var \wpdb
@@ -32,9 +33,9 @@
32 33 *
33 34 * @var \wpdb
34 35 */
35 36 private \wpdb $wpdb;
36 -
37 +
37 38 /**
38 39 * Table names
39 40 *
40 41 * @var array
@@ -39,9 +40,9 @@
39 40 *
40 41 * @var array
41 42 */
42 43 private array $tables;
43 -
44 +
44 45 /**
45 46 * Constructor
46 47 */
47 48 public function __construct() {
@@ -46,9 +47,9 @@
46 47 */
47 48 public function __construct() {
48 49 global $wpdb;
49 50 $this->wpdb = $wpdb;
50 -
51 +
51 52 $this->tables = [
52 53 'ai_cache' => $wpdb->prefix . 'thinkrank_ai_cache',
53 54 'ai_usage' => $wpdb->prefix . 'thinkrank_ai_usage',
54 55 'content_briefs' => $wpdb->prefix . 'thinkrank_content_briefs',
@@ -53,11 +54,12 @@
53 54 'ai_usage' => $wpdb->prefix . 'thinkrank_ai_usage',
54 55 'content_briefs' => $wpdb->prefix . 'thinkrank_content_briefs',
55 56 'seo_scores' => $wpdb->prefix . 'thinkrank_seo_scores',
56 57 'seo_performance' => $wpdb->prefix . 'thinkrank_seo_performance',
58 + 'instant_indexing_logs' => $wpdb->prefix . 'thinkrank_instant_indexing_logs',
57 59 ];
58 60 }
59 -
61 +
60 62 /**
61 63 * Initialize database operations
62 64 *
63 65 * @return void
@@ -68,9 +70,9 @@
68 70
69 71 // Hook the missing usage analytics cron handler
70 72 add_action('thinkrank_usage_analytics', [$this, 'process_weekly_analytics']);
71 73 }
72 -
74 +
73 75 /**
74 76 * Get table name
75 77 *
76 78 * @param string $table Table identifier
@@ -80,12 +82,12 @@
80 82 public function get_table(string $table): string {
81 83 if (!isset($this->tables[$table])) {
82 84 throw new \InvalidArgumentException(sprintf("Table '%s' not found", esc_html($table)));
83 85 }
84 -
86 +
85 87 return $this->tables[$table];
86 88 }
87 -
89 +
88 90 /**
89 91 * Execute prepared query safely
90 92 *
91 93 * @param string $query SQL query with placeholders
@@ -94,19 +96,19 @@
94 96 */
95 97 public function query(string $query, array $args = []) {
96 98 if (!empty($args)) {
97 99 // Prepare the query first, then execute
98 - // 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
99 101 $prepared_query = $this->wpdb->prepare($query, $args);
100 - // 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
101 103 return $this->wpdb->query($prepared_query);
102 104 }
103 105
104 106 // For queries without parameters, execute directly (safe for static queries)
105 - // 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
106 108 return $this->wpdb->query($query);
107 109 }
108 -
110 +
109 111 /**
110 112 * Get single row
111 113 *
112 114 * @param string $query SQL query with placeholders
@@ -116,19 +118,19 @@
116 118 */
117 119 public function get_row(string $query, array $args = [], string $output = OBJECT) {
118 120 if (!empty($args)) {
119 121 // Prepare the query first, then execute
120 - // 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
121 123 $prepared_query = $this->wpdb->prepare($query, $args);
122 - // 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
123 125 return $this->wpdb->get_row($prepared_query, $output);
124 126 }
125 127
126 128 // For queries without parameters, execute directly (safe for static queries)
127 - // 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
128 130 return $this->wpdb->get_row($query, $output);
129 131 }
130 -
132 +
131 133 /**
132 134 * Get multiple rows
133 135 *
134 136 * @param string $query SQL query with placeholders
@@ -138,21 +140,21 @@
138 140 */
139 141 public function get_results(string $query, array $args = [], string $output = OBJECT): array {
140 142 if (!empty($args)) {
141 143 // Prepare the query first, then execute
142 - // 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
143 145 $prepared_query = $this->wpdb->prepare($query, $args);
144 - // 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
145 147 $results = $this->wpdb->get_results($prepared_query, $output);
146 148 } else {
147 149 // For queries without parameters, execute directly (safe for static queries)
148 - // 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
149 151 $results = $this->wpdb->get_results($query, $output);
150 152 }
151 153
152 154 return is_array($results) ? $results : [];
153 155 }
154 -
156 +
155 157 /**
156 158 * Get single variable
157 159 *
158 160 * @param string $query SQL query with placeholders
@@ -161,19 +163,19 @@
161 163 */
162 164 public function get_var(string $query, array $args = []) {
163 165 if (!empty($args)) {
164 166 // Prepare the query first, then execute
165 - // 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
166 168 $prepared_query = $this->wpdb->prepare($query, $args);
167 - // 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
168 170 return $this->wpdb->get_var($prepared_query);
169 171 }
170 172
171 173 // For queries without parameters, execute directly (safe for static queries)
172 - // 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
173 175 return $this->wpdb->get_var($query);
174 176 }
175 -
177 +
176 178 /**
177 179 * Insert data into table
178 180 *
179 181 * @param string $table Table identifier
@@ -182,11 +184,11 @@
182 184 * @return int|false Insert ID or false on failure
183 185 */
184 186 public function insert(string $table, array $data, array $format = []) {
185 187 $table_name = $this->get_table($table);
186 -
188 +
187 189 $result = $this->wpdb->insert($table_name, $data, $format);
188 -
190 +
189 191 if (false === $result) {
190 192 $this->log_error('Insert failed', [
191 193 'table' => $table,
192 194 'data' => $data,
@@ -193,12 +195,12 @@
193 195 'error' => $this->wpdb->last_error
194 196 ]);
195 197 return false;
196 198 }
197 -
199 +
198 200 return $this->wpdb->insert_id;
199 201 }
200 -
202 +
201 203 /**
202 204 * Update data in table
203 205 *
204 206 * @param string $table Table identifier
@@ -209,11 +211,11 @@
209 211 * @return int|false Number of rows updated or false on failure
210 212 */
211 213 public function update(string $table, array $data, array $where, array $format = [], array $where_format = []) {
212 214 $table_name = $this->get_table($table);
213 -
215 +
214 216 $result = $this->wpdb->update($table_name, $data, $where, $format, $where_format);
215 -
217 +
216 218 if (false === $result) {
217 219 $this->log_error('Update failed', [
218 220 'table' => $table,
219 221 'data' => $data,
@@ -220,12 +222,12 @@
220 222 'where' => $where,
221 223 'error' => $this->wpdb->last_error
222 224 ]);
223 225 }
224 -
226 +
225 227 return $result;
226 228 }
227 -
229 +
228 230 /**
229 231 * Delete data from table
230 232 *
231 233 * @param string $table Table identifier
@@ -234,11 +236,11 @@
234 236 * @return int|false Number of rows deleted or false on failure
235 237 */
236 238 public function delete(string $table, array $where, array $where_format = []) {
237 239 $table_name = $this->get_table($table);
238 -
240 +
239 241 $result = $this->wpdb->delete($table_name, $where, $where_format);
240 -
242 +
241 243 if (false === $result) {
242 244 $this->log_error('Delete failed', [
243 245 'table' => $table,
244 246 'where' => $where,
@@ -244,12 +246,12 @@
244 246 'where' => $where,
245 247 'error' => $this->wpdb->last_error
246 248 ]);
247 249 }
248 -
250 +
249 251 return $result;
250 252 }
251 -
253 +
252 254 /**
253 255 * Start database transaction
254 256 *
255 257 * @return void
@@ -277,9 +279,9 @@
277 279 public function rollback(): void {
278 280 // Transaction commands don't need preparation as they contain no user input
279 281 $this->wpdb->query('ROLLBACK');
280 282 }
281 -
283 +
282 284 /**
283 285 * Get last database error
284 286 *
285 287 * @return string Last error message
@@ -286,9 +288,9 @@
286 288 */
287 289 public function get_last_error(): string {
288 290 return $this->wpdb->last_error;
289 291 }
290 -
292 +
291 293 /**
292 294 * Clean up expired cache entries
293 295 *
294 296 * @return void
@@ -294,9 +296,9 @@
294 296 * @return void
295 297 */
296 298 public function cleanup_expired_cache(): void {
297 299 $cache_table = $this->get_table('ai_cache');
298 -
300 +
299 301 // Check WordPress version for %i support (introduced in 6.2)
300 302 if (version_compare($GLOBALS['wp_version'], '6.2', '>=')) {
301 303 // Use %i placeholder for table identifier (WordPress 6.2+)
302 304 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder
@@ -301,11 +303,13 @@
301 303 // Use %i placeholder for table identifier (WordPress 6.2+)
302 304 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder
303 305 $deleted = $this->wpdb->query(
304 306 $this->wpdb->prepare(
305 - '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',
306 310 $cache_table,
307 - current_time('mysql')
311 + time()
308 312 )
309 313 );
310 314 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder
311 315 } else {
@@ -313,10 +317,10 @@
313 317 $escaped_table = esc_sql($cache_table);
314 318 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
315 319 $deleted = $this->wpdb->query(
316 320 $this->wpdb->prepare(
317 - "DELETE FROM `{$escaped_table}` WHERE expires_at < %s",
318 - current_time('mysql')
321 + "DELETE FROM `{$escaped_table}` WHERE expires_at < %d",
322 + time()
319 323 )
320 324 );
321 325 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
322 326 }
@@ -327,9 +331,9 @@
327 331 ['type' => 'info']
328 332 );
329 333 }
330 334 }
331 -
335 +
332 336 /**
333 337 * Get database statistics
334 338 *
335 339 * @return array Database statistics
@@ -335,9 +339,9 @@
335 339 * @return array Database statistics
336 340 */
337 341 public function get_stats(): array {
338 342 $stats = [];
339 -
343 +
340 344 foreach ($this->tables as $key => $table) {
341 345 // Check WordPress version for %i support (introduced in 6.2)
342 346 if (version_compare($GLOBALS['wp_version'], '6.2', '>=')) {
343 347 // Use %i placeholder for table identifier (WordPress 6.2+)
@@ -348,17 +352,17 @@
348 352 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder
349 353 } else {
350 354 // Fallback for older WordPress versions - table name is from our controlled list
351 355 $escaped_table = esc_sql($table);
352 - // 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
353 357 $count = $this->wpdb->get_var("SELECT COUNT(*) FROM `{$escaped_table}`");
354 358 }
355 359 $stats[$key] = (int) $count;
356 360 }
357 -
361 +
358 362 return $stats;
359 363 }
360 -
364 +
361 365 /**
362 366 * Log database errors
363 367 *
364 368 * @param string $message Error message
@@ -420,9 +424,8 @@
420 424 'cleanup_results' => $cleanup_results,
421 425 'cache_results' => $cache_results,
422 426 'processed_at' => current_time('mysql')
423 427 ]);
424 -
425 428 } catch (\Exception $e) {
426 429 // Log error but don't throw to prevent cron job failures
427 430 $this->log_debug('Weekly analytics processing failed', [
428 431 'error' => $e->getMessage(),
@@ -440,12 +443,13 @@
440 443 $results = [];
441 444
442 445 // Define retention periods (in days)
443 446 $retention_config = [
444 - 'ai_usage' => 365, // 1 year
445 - 'seo_scores' => 180, // 6 months
446 - 'content_briefs' => 90, // 3 months
447 - '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)
448 452 ];
449 453
450 454 foreach ($retention_config as $table_key => $retention_days) {
451 455 try {
@@ -507,20 +511,20 @@
507 511
508 512 $date_column = $date_columns[$table_key] ?? 'created_at';
509 513
510 514 // Delete old records
511 - // 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
512 516 $deleted = $this->wpdb->query(
513 - // 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
514 518 $this->wpdb->prepare(
515 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table and column names are validated internally
516 - "DELETE FROM `{$table_name}` WHERE {$date_column} < %s",
517 - // 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
518 522 $cutoff_date
519 523 )
520 524 );
521 525
522 - return $deleted !== false ? (int)$deleted : 0;
526 + return $deleted !== false ? (int) $deleted : 0;
523 527 }
524 528
525 529 /**
526 530 * Clear analytics-related cache
@@ -547,20 +551,24 @@
547 551 }
548 552 }
549 553
550 554 // Also clear any user-specific analytics cache
551 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache cleanup requires direct database access
552 - $user_cache_deleted = $this->wpdb->query(
553 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- wpdb->options is WordPress core table, safe to use
554 - "DELETE FROM {$this->wpdb->options}
555 - WHERE option_name LIKE '_transient_thinkrank_analytics_%'
556 - 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
557 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 );
558 566
559 567 return [
560 568 'transients_cleared' => $cleared_count,
561 - 'user_cache_cleared' => $user_cache_deleted !== false ? (int)$user_cache_deleted : 0,
562 - 'total_cleared' => $cleared_count + ($user_cache_deleted !== false ? (int)$user_cache_deleted : 0)
569 + 'user_cache_cleared' => $user_cache_deleted !== false ? (int) $user_cache_deleted : 0,
570 + 'total_cleared' => $cleared_count + ($user_cache_deleted !== false ? (int) $user_cache_deleted : 0)
563 571 ];
564 572 }
565 573
566 574 /**
@@ -581,21 +589,21 @@
581 589
582 590 foreach ($tables as $table_key) {
583 591 $table_name = $this->get_table($table_key);
584 592
585 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table size monitoring requires direct database access
586 - $size_result = $this->wpdb->get_row(
587 - $this->wpdb->prepare(
588 - "SELECT
589 - table_name,
590 - ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb,
591 - table_rows
592 - FROM information_schema.TABLES
593 - WHERE table_schema = %s AND table_name = %s",
594 - DB_NAME,
595 - $table_name
596 - )
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
597 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 );
598 606
599 607 if ($size_result) {
600 608 $sizes[$table_key] = [
601 609 'size_mb' => (float) $size_result->size_mb,