PluginProbe
WebberZone Top 10 — Popular Posts / 4.3.2
WebberZone Top 10 — Popular Posts v4.3.2
4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 All 117 releases
top-10 / includes / class-database.php

class-database.php in WebberZone Top 10 — Popular Posts 4.3.2, at includes/class-database.php

1,184 lines 44.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Database class.
4 *
5 * @package WebberZone\Top_Ten
6 */
7
8 namespace WebberZone\Top_Ten;
9
10 use WebberZone\Top_Ten\Util\Helpers;
11
12 /**
13 * Database operations class.
14 *
15 * @since 4.2.0
16 */
17 class Database {
18
19 /**
20 * Constructor.
21 */
22 public function __construct() {
23 // No initialization needed for static methods.
24 }
25
26 /**
27 * Get the table name for overall or daily counts.
28 *
29 * @since 4.2.0
30 *
31 * @param bool $daily Whether to get the daily table.
32 * @return string Table name.
33 */
34 public static function get_table( $daily = false ) {
35 global $wpdb;
36
37 $table_name = $wpdb->base_prefix . 'top_ten';
38 if ( $daily ) {
39 $table_name .= '_daily';
40 }
41 return $table_name;
42 }
43
44 /**
45 * Get count for a specific post.
46 *
47 * @since 4.2.0
48 *
49 * @param int $post_id Post ID.
50 * @param int $blog_id Blog ID (optional, defaults to current blog).
51 * @param bool $daily Whether to get daily count.
52 * @param array $date_range Date range array for daily counts ['from_date', 'to_date'].
53 * @return int Post count.
54 */
55 public static function get_count( $post_id, $blog_id = null, $daily = false, $date_range = array() ) {
56 global $wpdb;
57
58 $blog_id = $blog_id ?? get_current_blog_id();
59 $table = self::get_table( $daily );
60
61 if ( $daily && ! empty( $date_range ) ) {
62 $where = $wpdb->prepare( 'WHERE postnumber = %d AND blog_id = %d', $post_id, $blog_id );
63
64 if ( ! empty( $date_range['from_date'] ) ) {
65 $where .= $wpdb->prepare( ' AND dp_date >= %s', $date_range['from_date'] );
66 }
67 if ( ! empty( $date_range['to_date'] ) ) {
68 $where .= $wpdb->prepare( ' AND dp_date <= %s', $date_range['to_date'] );
69 }
70
71 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
72 $sql = "SELECT SUM(cntaccess) FROM {$table} {$where}";
73 } else {
74 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
75 $sql = $wpdb->prepare( "SELECT cntaccess FROM {$table} WHERE postnumber = %d AND blog_id = %d", $post_id, $blog_id );
76 }
77
78 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
79 return (int) $wpdb->get_var( $sql );
80 }
81
82 /**
83 * Update count for a post.
84 *
85 * @since 4.2.0
86 * @deprecated 4.3.0 Use {@see Database::append_to_funnel()} instead.
87 *
88 * @param int $post_id Post ID.
89 * @param int $blog_id Blog ID (optional, defaults to current blog).
90 * @param bool $daily Whether to update daily count.
91 * @return int|false Number of rows affected or false on error.
92 */
93 public static function update_count( $post_id, $blog_id = null, $daily = false ) {
94 _deprecated_function( __METHOD__, '4.3.0', 'Database::append_to_funnel()' );
95
96 $blog_id = $blog_id ?? get_current_blog_id();
97 $activate_counter = $daily ? 10 : 1;
98
99 return self::append_to_funnel( $post_id, $blog_id, $activate_counter );
100 }
101
102 /**
103 * Set count for a post to a specific value.
104 *
105 * @since 4.2.0
106 *
107 * @param int $post_id Post ID.
108 * @param int $count Count value to set.
109 * @param int $blog_id Blog ID (optional, defaults to current blog).
110 * @param bool $daily Whether to update daily count.
111 * @return int|false Number of rows affected or false on error.
112 */
113 public static function set_count( $post_id, $count, $blog_id = null, $daily = false ) {
114 global $wpdb;
115
116 $blog_id = $blog_id ?? get_current_blog_id();
117 $table = self::get_table( $daily );
118
119 if ( $daily ) {
120 $dp_date = current_time( 'Y-m-d H' );
121 $sql = $wpdb->prepare(
122 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
123 "INSERT INTO {$table} (postnumber, cntaccess, dp_date, blog_id) VALUES (%d, %d, %s, %d) ON DUPLICATE KEY UPDATE cntaccess = %d",
124 $post_id,
125 $count,
126 $dp_date,
127 $blog_id,
128 $count
129 );
130 } else {
131 $sql = $wpdb->prepare(
132 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
133 "INSERT INTO {$table} (postnumber, cntaccess, blog_id) VALUES (%d, %d, %d) ON DUPLICATE KEY UPDATE cntaccess = %d",
134 $post_id,
135 $count,
136 $blog_id,
137 $count
138 );
139 }
140
141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
142 $result = $wpdb->query( $sql );
143
144 // Trigger action to clear cache.
145 if ( false !== $result ) {
146 do_action( 'tptn_set_count', $post_id, $count, $blog_id, $daily );
147 }
148
149 return $result;
150 }
151
152 /**
153 * Delete counts based on criteria.
154 *
155 * @since 4.2.0
156 *
157 * @param array $args {
158 * Optional. Array of arguments.
159 *
160 * @type array $post_ids Array of post IDs to delete.
161 * @type int $blog_id Blog ID to delete from.
162 * @type string $from_date Delete entries from this date (daily table only).
163 * @type string $to_date Delete entries until this date (daily table only).
164 * @type bool $daily Whether to delete from daily table.
165 * @type int $limit Maximum number of rows to delete per call (0 = no limit).
166 * }
167 * @return int|false Number of rows deleted or false on error.
168 */
169 public static function delete_counts( $args = array() ) {
170 global $wpdb;
171
172 $defaults = array(
173 'post_ids' => array(),
174 'blog_id' => null,
175 'from_date' => '',
176 'to_date' => '',
177 'daily' => false,
178 'limit' => 0,
179 );
180 $args = wp_parse_args( $args, $defaults );
181
182 $table = self::get_table( $args['daily'] );
183 $where = array();
184
185 if ( ! empty( $args['post_ids'] ) ) {
186 $post_ids = array_map( 'intval', $args['post_ids'] );
187 $where[] = 'postnumber IN (' . implode( ',', $post_ids ) . ')';
188 }
189
190 if ( null !== $args['blog_id'] ) {
191 $where[] = $wpdb->prepare( 'blog_id = %d', $args['blog_id'] );
192 }
193
194 if ( $args['daily'] ) {
195 if ( ! empty( $args['from_date'] ) ) {
196 $where[] = $wpdb->prepare( 'dp_date >= %s', $args['from_date'] );
197 }
198 if ( ! empty( $args['to_date'] ) ) {
199 $where[] = $wpdb->prepare( 'dp_date <= %s', $args['to_date'] );
200 }
201 }
202
203 $sql = "DELETE FROM {$table}";
204 if ( ! empty( $where ) ) {
205 $sql .= ' WHERE ' . implode( ' AND ', $where );
206 }
207
208 if ( ! empty( $args['limit'] ) && $args['limit'] > 0 && ! empty( $where ) ) {
209 $sql .= $wpdb->prepare( ' LIMIT %d', $args['limit'] );
210 }
211
212 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
213 $result = $wpdb->query( $sql );
214
215 // Trigger action to clear cache.
216 if ( false !== $result ) {
217 do_action( 'tptn_delete_counts', $args );
218 }
219
220 return $result;
221 }
222
223 /**
224 * Get table statistics including entry count and size.
225 *
226 * @since 4.2.0
227 *
228 * @return array Array of table statistics with entry count and size.
229 */
230 public static function get_table_statistics() {
231 $cache_key = 'tptn_table_statistics';
232 $stats = wp_cache_get( $cache_key, 'top-10' );
233
234 if ( false === $stats ) {
235 $stats = array();
236
237 $tables = array(
238 'top_ten' => self::get_table( false ),
239 'top_ten_daily' => self::get_table( true ),
240 'top_ten_visits_funnel' => self::get_funnel_table(),
241 'top_ten_visits_log' => self::get_log_table(),
242 );
243
244 foreach ( $tables as $key => $table_name ) {
245 if ( self::is_table_installed( $table_name ) ) {
246 $stats[ $key ] = self::get_single_table_statistics( $table_name );
247 }
248 }
249
250 // Cache for 5 minutes.
251 wp_cache_set( $cache_key, $stats, 'top-10', 300 );
252 }
253
254 /**
255 * Filter the table statistics.
256 *
257 * @since 4.2.0
258 *
259 * @param array $stats Array of table statistics.
260 */
261 return apply_filters( 'tptn_table_statistics', $stats );
262 }
263
264 /**
265 * Get entry count and estimated size for a single table.
266 *
267 * @since 4.3.0
268 *
269 * @param string $table_name Table name.
270 * @return array {
271 * @type int $entries Number of entries.
272 * @type float $size Estimated size in bytes.
273 * }
274 */
275 private static function get_single_table_statistics( $table_name ) {
276 global $wpdb;
277
278 // Get row count.
279 if ( is_network_admin() ) {
280 // In network admin, count all entries.
281 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
282 $count = $wpdb->get_var( "SELECT COUNT(*) FROM `{$table_name}`" );
283 } else {
284 // In individual site admin, count only entries for this blog.
285 $count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
286 $wpdb->prepare(
287 "SELECT COUNT(*) FROM `{$table_name}` WHERE blog_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
288 get_current_blog_id()
289 )
290 );
291 }
292
293 // Refresh InnoDB stats so information_schema reflects the current state.
294 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
295 $wpdb->query( "ANALYZE TABLE `{$table_name}`" );
296
297 // Get table size in bytes (always shows total size across all blogs).
298 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
299 $size = $wpdb->get_var(
300 $wpdb->prepare(
301 'SELECT (data_length + index_length) FROM information_schema.TABLES WHERE table_schema = %s AND table_name = %s',
302 defined( 'DB_NAME' ) ? DB_NAME : '', // @codingStandardsIgnoreLine - WordPress constant
303 $table_name
304 )
305 );
306
307 // Calculate size for individual sites in multisite.
308 $calculated_size = $size ? (int) $size : 0;
309 if ( is_multisite() && ! is_network_admin() && $calculated_size > 0 ) {
310 // Get total entries to calculate ratio.
311 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
312 $total_count = $wpdb->get_var( "SELECT COUNT(*) FROM `{$table_name}`" );
313
314 if ( $total_count > 0 && $count > 0 ) {
315 // Estimate size based on entry count ratio.
316 $calculated_size = ( $count / $total_count ) * $calculated_size;
317 }
318 }
319
320 return array(
321 'entries' => absint( $count ),
322 'size' => $calculated_size,
323 );
324 }
325
326 /**
327 * Clear the table statistics cache.
328 *
329 * @since 4.2.0
330 */
331 public static function clear_table_statistics_cache() {
332 wp_cache_delete( 'tptn_table_statistics', 'top-10' );
333 }
334
335 /**
336 * Check if a table exists.
337 *
338 * @since 4.2.0
339 *
340 * @param string $table Table name to check.
341 * @return bool True if table exists, false otherwise.
342 */
343 public static function is_table_installed( $table ) {
344 global $wpdb;
345
346 static $cache = array();
347
348 if ( isset( $cache[ $table ] ) ) {
349 return $cache[ $table ];
350 }
351
352 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
353 $result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
354 $cache[ $table ] = ( $result === $table );
355
356 return $cache[ $table ];
357 }
358
359 /**
360 * Get counts with post information (JOIN with wp_posts).
361 *
362 * @since 4.2.0
363 *
364 * @param array $args {
365 * Optional. Array of arguments.
366 *
367 * @type bool $daily Whether to get daily counts.
368 * @type int $blog_id Blog ID to filter by.
369 * @type string $from_date From date for daily counts.
370 * @type string $to_date To date for daily counts.
371 * @type int $limit Number of results to return.
372 * @type int $offset Offset for pagination.
373 * @type string $order Order direction (ASC/DESC).
374 * @type string $post_type Post type to filter by.
375 * @type array $post_ids Specific post IDs to include.
376 * }
377 * @return array Array of results with post and count information.
378 */
379 public static function get_counts_with_posts( $args = array() ) {
380 global $wpdb;
381
382 $defaults = array(
383 'daily' => false,
384 'blog_id' => null,
385 'from_date' => '',
386 'to_date' => '',
387 'limit' => 10,
388 'offset' => 0,
389 'order' => 'DESC',
390 'post_type' => 'post',
391 'post_ids' => array(),
392 );
393 $args = wp_parse_args( $args, $defaults );
394
395 $table = self::get_table( $args['daily'] );
396 $where = array();
397 $join = " LEFT JOIN {$wpdb->posts} ON t.postnumber = {$wpdb->posts}.ID ";
398 $select_col = $args['daily'] ? 'SUM(t.cntaccess) as cntaccess' : 't.cntaccess';
399
400 if ( null !== $args['blog_id'] ) {
401 $where[] = $wpdb->prepare( 't.blog_id = %d', $args['blog_id'] );
402 }
403
404 if ( $args['daily'] ) {
405 if ( ! empty( $args['from_date'] ) ) {
406 $where[] = $wpdb->prepare( 'DATE(t.dp_date) >= DATE(%s)', $args['from_date'] );
407 }
408 if ( ! empty( $args['to_date'] ) ) {
409 $where[] = $wpdb->prepare( 'DATE(t.dp_date) <= DATE(%s)', $args['to_date'] );
410 }
411 }
412
413 if ( ! empty( $args['post_ids'] ) ) {
414 $post_ids = array_map( 'intval', $args['post_ids'] );
415 $where[] = 't.postnumber IN (' . implode( ',', $post_ids ) . ')';
416 }
417
418 $where[] = $wpdb->prepare( "{$wpdb->posts}.post_type = %s", $args['post_type'] );
419 $where[] = "{$wpdb->posts}.post_status = 'publish'";
420
421 $sql = "SELECT t.postnumber, {$select_col}, t.blog_id, {$wpdb->posts}.post_title, {$wpdb->posts}.post_date ";
422 $sql .= "FROM {$table} t {$join}";
423 $sql .= ' WHERE ' . implode( ' AND ', $where );
424
425 if ( $args['daily'] ) {
426 $sql .= " GROUP BY t.postnumber, t.blog_id, {$wpdb->posts}.post_title, {$wpdb->posts}.post_date ";
427 }
428
429 // Sanitize order parameter.
430 $order = in_array( strtoupper( $args['order'] ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $args['order'] ) : 'DESC';
431 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
432 $sql .= $wpdb->prepare( " ORDER BY cntaccess {$order} LIMIT %d OFFSET %d", $args['limit'], $args['offset'] );
433
434 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
435 return $wpdb->get_results( $sql, ARRAY_A );
436 }
437
438 /**
439 * Bulk upsert counts for import operations.
440 *
441 * @since 4.2.0
442 *
443 * @param array $data Array of data to insert. Each element should be an array with postnumber, cntaccess, blog_id keys.
444 * @param bool $daily Whether this is for daily table (includes dp_date).
445 * @return int|false Number of rows affected or false on error.
446 */
447 public static function bulk_upsert( $data, $daily = false ) {
448 global $wpdb;
449
450 if ( empty( $data ) ) {
451 return false;
452 }
453
454 $table = self::get_table( $daily );
455 $values = array();
456
457 foreach ( $data as $row ) {
458 if ( $daily ) {
459 $dp_date = isset( $row['dp_date'] ) ? $row['dp_date'] : current_time( 'Y-m-d H' );
460 $values[] = $wpdb->prepare( '( %d, %d, %s, %d )', $row['postnumber'], $row['cntaccess'], $dp_date, $row['blog_id'] );
461 } else {
462 $values[] = $wpdb->prepare( '( %d, %d, %d )', $row['postnumber'], $row['cntaccess'], $row['blog_id'] );
463 }
464 }
465
466 if ( $daily ) {
467 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
468 $sql = "INSERT INTO {$table} (postnumber, cntaccess, dp_date, blog_id) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE cntaccess = VALUES(cntaccess)';
469 } else {
470 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
471 $sql = "INSERT INTO {$table} (postnumber, cntaccess, blog_id) VALUES " . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE cntaccess = VALUES(cntaccess)';
472 }
473
474 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
475 return $wpdb->query( $sql );
476 }
477
478 /**
479 * Get total count for all posts.
480 *
481 * @since 4.2.0
482 *
483 * @param int $blog_id Blog ID (optional, defaults to current blog).
484 * @param bool $daily Whether to get daily total.
485 * @param string $from_date From date for daily counts.
486 * @param string $to_date To date for daily counts.
487 * @return int Total count.
488 */
489 public static function get_total_count( $blog_id = null, $daily = false, $from_date = '', $to_date = '' ) {
490 global $wpdb;
491
492 $blog_id = $blog_id ?? get_current_blog_id();
493 $table = self::get_table( $daily );
494 $where = $wpdb->prepare( 'WHERE blog_id = %d', $blog_id );
495
496 if ( $daily ) {
497 if ( ! empty( $from_date ) ) {
498 $where .= $wpdb->prepare( ' AND dp_date >= %s', $from_date );
499 }
500 if ( ! empty( $to_date ) ) {
501 $where .= $wpdb->prepare( ' AND dp_date <= %s', $to_date );
502 }
503 }
504
505 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
506 $sql = "SELECT SUM(cntaccess) FROM {$table} {$where}";
507
508 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
509 return (int) $wpdb->get_var( $sql );
510 }
511
512 /**
513 * Get popular posts with caching support.
514 *
515 * @since 4.2.0
516 *
517 * @param array $args Query arguments (same format as Top_Ten_Core_Query).
518 * @return array Array of post IDs.
519 */
520 public static function get_popular_posts( $args = array() ) {
521 // This method integrates with the existing Top_Ten_Core_Query class
522 // but provides a simpler interface for basic operations.
523
524 $defaults = array(
525 'daily' => false,
526 'limit' => 10,
527 'post_type' => 'post',
528 'blog_id' => null,
529 );
530 $args = wp_parse_args( $args, $defaults );
531
532 // Use the existing query class for complex operations.
533 $query = new \Top_Ten_Query( $args );
534 $posts = $query->get_posts();
535
536 return wp_list_pluck( $posts, 'ID' );
537 }
538
539 /**
540 * Check if the Top Ten tables exist.
541 *
542 * @since 4.2.0
543 *
544 * @return bool True if both tables exist, false otherwise.
545 */
546 public static function are_tables_installed() {
547 return self::is_table_installed( self::get_table( false ) )
548 && self::is_table_installed( self::get_table( true ) );
549 }
550
551 /**
552 * Create table SQL for the main top_ten table.
553 *
554 * @since 4.2.0
555 *
556 * @return string SQL to create the main table.
557 */
558 public static function create_full_table_sql() {
559 global $wpdb;
560
561 $charset_collate = $wpdb->get_charset_collate();
562 $table_name = $wpdb->base_prefix . 'top_ten';
563
564 $sql = "CREATE TABLE {$table_name}" . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
565 " (
566 postnumber bigint(20) NOT NULL,
567 cntaccess bigint(20) NOT NULL,
568 blog_id bigint(20) NOT NULL DEFAULT '1',
569 PRIMARY KEY (postnumber, blog_id),
570 KEY idx_blog_id (blog_id)
571 ) $charset_collate;";
572
573 return $sql;
574 }
575
576 /**
577 * Create table SQL for the daily top_ten_daily table.
578 *
579 * @since 4.2.0
580 *
581 * @return string SQL to create the daily table.
582 */
583 public static function create_daily_table_sql() {
584 global $wpdb;
585
586 $charset_collate = $wpdb->get_charset_collate();
587 $table_name = $wpdb->base_prefix . 'top_ten_daily';
588
589 $sql = "CREATE TABLE {$table_name}" . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
590 " (
591 postnumber bigint(20) NOT NULL,
592 cntaccess bigint(20) NOT NULL,
593 dp_date DATETIME NOT NULL,
594 blog_id bigint(20) NOT NULL DEFAULT '1',
595 PRIMARY KEY (postnumber, dp_date, blog_id),
596 KEY blog_date (blog_id, dp_date, postnumber),
597 KEY idx_dp_date (dp_date)
598 ) $charset_collate;";
599
600 return $sql;
601 }
602
603 /**
604 * Get the name of the visits funnel table (hot buffer, drained every 5 minutes).
605 *
606 * @since 4.3.0
607 *
608 * @return string Table name.
609 */
610 public static function get_funnel_table() {
611 global $wpdb;
612 return $wpdb->base_prefix . 'top_ten_visits_funnel';
613 }
614
615 /**
616 * SQL to create the visits funnel table.
617 *
618 * @since 4.3.0
619 *
620 * @return string CREATE TABLE SQL.
621 */
622 public static function create_funnel_table_sql() {
623 global $wpdb;
624
625 $charset_collate = $wpdb->get_charset_collate();
626 $table_name = self::get_funnel_table();
627
628 $sql = "CREATE TABLE {$table_name}" . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
629 " (
630 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
631 postnumber bigint(20) UNSIGNED NOT NULL,
632 blog_id bigint(20) UNSIGNED NOT NULL DEFAULT '1',
633 visited_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
634 activate_counter tinyint(2) UNSIGNED NOT NULL DEFAULT '11',
635 source tinyint(2) UNSIGNED NOT NULL DEFAULT '0',
636 PRIMARY KEY (id)
637 ) $charset_collate;";
638
639 return $sql;
640 }
641
642 /**
643 * Get the name of the visits log table (cold archive, pruned by maintenance cron).
644 *
645 * @since 4.3.0
646 *
647 * @return string Table name.
648 */
649 public static function get_log_table() {
650 global $wpdb;
651 return $wpdb->base_prefix . 'top_ten_visits_log';
652 }
653
654 /**
655 * SQL to create the visits log table.
656 *
657 * @since 4.3.0
658 *
659 * @return string CREATE TABLE SQL.
660 */
661 public static function create_log_table_sql() {
662 global $wpdb;
663
664 $charset_collate = $wpdb->get_charset_collate();
665 $table_name = self::get_log_table();
666
667 $sql = "CREATE TABLE {$table_name}" . // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange
668 " (
669 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
670 postnumber bigint(20) UNSIGNED NOT NULL,
671 blog_id bigint(20) UNSIGNED NOT NULL DEFAULT '1',
672 visited_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
673 source tinyint(2) UNSIGNED NOT NULL DEFAULT '0',
674 PRIMARY KEY (id),
675 KEY idx_visited_at (visited_at)
676 ) $charset_collate;";
677
678 return $sql;
679 }
680
681 /**
682 * Append a single visit to the funnel table.
683 *
684 * @since 4.3.0
685 *
686 * @param int $post_id Post ID.
687 * @param int $blog_id Blog ID.
688 * @param int $activate_counter Counter flag: 1 = overall, 10 = daily, 11 = both.
689 * @param int $source Traffic source: 0 = web, 1 = feed.
690 * @return int|false Rows inserted or false on error.
691 */
692 public static function append_to_funnel( $post_id, $blog_id, $activate_counter = 11, $source = 0 ) {
693 global $wpdb;
694
695 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
696 return $wpdb->insert(
697 self::get_funnel_table(),
698 array(
699 'postnumber' => absint( $post_id ),
700 'blog_id' => absint( $blog_id ),
701 'visited_at' => current_time( 'mysql' ),
702 'activate_counter' => (int) $activate_counter,
703 'source' => (int) $source,
704 ),
705 array( '%d', '%d', '%s', '%d', '%d' )
706 );
707 }
708
709 /**
710 * Drain the funnel into the log and count tables, then empty the funnel.
711 *
712 * All four operations (copy to log, aggregate to daily, aggregate to overall,
713 * delete from funnel) run inside one transaction. A failure rolls back cleanly
714 * and the next run retries the same rows with no double-counting risk.
715 *
716 * @since 4.3.0
717 *
718 * @param int $batch_size Maximum funnel rows to process per run.
719 * @return true|false|int|\WP_Error True if rows processed, false if lock not acquired, 0 if funnel empty, WP_Error on DB failure.
720 */
721 public static function aggregate_visit_log( $batch_size = 10000 ) {
722 global $wpdb;
723
724 // Detect SQLite (e.g. WordPress Playground) vs MySQL/MariaDB.
725 // DATABASE_TYPE is defined by the WordPress SQLite Database Integration drop-in.
726 $is_sqlite = ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE )
727 || false !== strpos( strtolower( (string) $wpdb->db_server_info() ), 'sqlite' );
728
729 $funnel_table = self::get_funnel_table();
730 $log_table = self::get_log_table();
731 $daily_table = self::get_table( true );
732 $full_table = self::get_table( false );
733
734 // MySQL-specific locking and transactions.
735 if ( ! $is_sqlite ) {
736 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
737 $lock_acquired = $wpdb->get_var( "SELECT GET_LOCK('tptn_aggregation', 0)" );
738 if ( '1' !== (string) $lock_acquired ) {
739 return false;
740 }
741
742 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
743 if ( false === $wpdb->query( 'START TRANSACTION' ) ) {
744 $wpdb->query( "SELECT RELEASE_LOCK('tptn_aggregation')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
745 return new \WP_Error( 'tptn_transaction_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Could not start transaction.', 'top-10' ) );
746 }
747 }
748
749 try {
750 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
751 $max_id = (int) $wpdb->get_var( "SELECT MAX(id) FROM {$funnel_table}" );
752 if ( 0 === $max_id ) {
753 if ( ! $is_sqlite ) {
754 $wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
755 }
756 return 0;
757 }
758
759 $cap_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$funnel_table} ORDER BY id ASC LIMIT %d, 1", $batch_size ) );
760 $was_capped = false;
761 if ( null !== $cap_id ) {
762 $capped_max = (int) $cap_id - 1;
763 if ( $capped_max > 0 ) {
764 $max_id = $capped_max;
765 $was_capped = true;
766 }
767 }
768
769 $r = $wpdb->query(
770 $wpdb->prepare(
771 "INSERT INTO {$log_table} (postnumber, blog_id, visited_at, source)
772 SELECT postnumber, blog_id, visited_at, source
773 FROM {$funnel_table}
774 WHERE id <= %d",
775 $max_id
776 )
777 );
778 if ( false === $r ) {
779 if ( ! $is_sqlite ) {
780 $wpdb->query( 'ROLLBACK' );
781 }
782 return new \WP_Error( 'tptn_log_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to copy visits to log table.', 'top-10' ) );
783 }
784
785 $r = $wpdb->query(
786 $wpdb->prepare(
787 "INSERT INTO {$daily_table} (postnumber, cntaccess, dp_date, blog_id)
788 SELECT postnumber, COUNT(*) AS cntaccess,
789 DATE_FORMAT(visited_at, '%%Y-%%m-%%d %%H:00:00') AS dp_date, blog_id
790 FROM {$funnel_table}
791 WHERE id <= %d AND activate_counter IN (10, 11)
792 GROUP BY postnumber, DATE_FORMAT(visited_at, '%%Y-%%m-%%d %%H:00:00'), blog_id
793 ON DUPLICATE KEY UPDATE cntaccess = {$daily_table}.cntaccess + VALUES(cntaccess)",
794 $max_id
795 )
796 );
797 if ( false === $r ) {
798 if ( ! $is_sqlite ) {
799 $wpdb->query( 'ROLLBACK' );
800 }
801 return new \WP_Error( 'tptn_daily_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to aggregate visits into daily table.', 'top-10' ) );
802 }
803
804 $r = $wpdb->query(
805 $wpdb->prepare(
806 "INSERT INTO {$full_table} (postnumber, cntaccess, blog_id)
807 SELECT postnumber, COUNT(*) AS cntaccess, blog_id
808 FROM {$funnel_table}
809 WHERE id <= %d AND activate_counter IN (1, 11)
810 GROUP BY postnumber, blog_id
811 ON DUPLICATE KEY UPDATE cntaccess = {$full_table}.cntaccess + VALUES(cntaccess)",
812 $max_id
813 )
814 );
815 if ( false === $r ) {
816 if ( ! $is_sqlite ) {
817 $wpdb->query( 'ROLLBACK' );
818 }
819 return new \WP_Error( 'tptn_overall_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to aggregate visits into overall table.', 'top-10' ) );
820 }
821
822 $r = $wpdb->query( $wpdb->prepare( "DELETE FROM {$funnel_table} WHERE id <= %d", $max_id ) );
823 if ( false === $r ) {
824 if ( ! $is_sqlite ) {
825 $wpdb->query( 'ROLLBACK' );
826 }
827 return new \WP_Error( 'tptn_funnel_delete_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to drain funnel table.', 'top-10' ) );
828 }
829 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
830
831 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
832 if ( ! $is_sqlite && false === $wpdb->query( 'COMMIT' ) ) {
833 $wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
834 return new \WP_Error( 'tptn_commit_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Transaction commit failed.', 'top-10' ) );
835 }
836
837 do_action( 'tptn_count_updated', 0, 0, false );
838
839 if ( $was_capped && ! wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
840 wp_schedule_single_event( time(), 'tptn_aggregation_cron_hook' );
841 }
842
843 return true;
844 } finally {
845 if ( ! $is_sqlite ) {
846 $wpdb->query( "SELECT RELEASE_LOCK('tptn_aggregation')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
847 }
848 }
849 }
850
851 /**
852 * Recreate a table.
853 *
854 * This method recreates a table by creating a backup, dropping the original table,
855 * and then creating a new table with the original name and inserting the data from the backup.
856 *
857 * @since 4.2.0
858 *
859 * @param string $table_name The name of the table to recreate.
860 * @param string $create_table_sql The SQL statement to create the new table.
861 * @param bool $backup Whether to backup the table or not.
862 * @param array $fields The fields to include in the temporary table and on duplicate key code.
863 * @param array $group_by_fields The fields to group by in the temporary table.
864 *
865 * @return bool|\WP_Error True if recreated, error message if failed.
866 */
867 public static function recreate_table(
868 $table_name,
869 $create_table_sql,
870 $backup = true,
871 $fields = array( 'postnumber', 'cntaccess', 'blog_id' ),
872 $group_by_fields = array( 'postnumber', 'blog_id' )
873 ) {
874 global $wpdb;
875
876 $backup_table_name = ( $backup ) ? $table_name . '_backup' : $table_name . '_temp';
877 $success = false;
878
879 $fields_sql = implode( ', ', $fields );
880 $fields_sql_with_sum = str_replace( 'cntaccess', 'SUM(cntaccess) as cntaccess', $fields_sql );
881 $group_by_sql = implode( ', ', $group_by_fields );
882
883 if ( $backup ) {
884 $success = $wpdb->query( "CREATE TABLE $backup_table_name LIKE $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
885 if ( false !== $success ) {
886 $success = $wpdb->query( "INSERT INTO $backup_table_name SELECT * FROM $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
887 } else {
888 /* translators: 1: Site number, 2: Error message */
889 return new \WP_Error( 'tptn_database_backup_failed', sprintf( esc_html__( 'Database backup failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
890 }
891 } else {
892 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
893 $success = $wpdb->query( "CREATE TEMPORARY TABLE $backup_table_name AS SELECT $fields_sql_with_sum FROM $table_name GROUP BY $group_by_sql" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
894 }
895
896 if ( false !== $success ) {
897 $wpdb->query( "DROP TABLE IF EXISTS $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
898
899 // Direct table creation without dbDelta for recreation.
900 $wpdb->query( $create_table_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
901
902 $insert_fields_sql = 'tt.' . implode( ', tt.', $fields );
903
904 $success = $wpdb->query( "INSERT INTO $table_name ($fields_sql) SELECT $insert_fields_sql FROM $backup_table_name AS tt ON DUPLICATE KEY UPDATE $table_name.cntaccess = $table_name.cntaccess + VALUES(cntaccess)" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
905
906 if ( false === $success ) {
907 /* translators: 1: Site number, 2: Error message */
908 return new \WP_Error( 'tptn_database_insert_failed', sprintf( esc_html__( 'Database insert failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
909 }
910 }
911
912 if ( ! $backup ) {
913 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
914 }
915
916 return $success;
917 }
918
919 /**
920 * Recreate overall table.
921 *
922 * @since 4.2.0
923 *
924 * @param bool $backup Whether to backup the table or not.
925 *
926 * @return bool|\WP_Error True if recreated, error message if failed.
927 */
928 public static function recreate_overall_table( $backup = true ) {
929 global $wpdb;
930 return self::recreate_table(
931 $wpdb->base_prefix . 'top_ten',
932 self::create_full_table_sql(),
933 $backup
934 );
935 }
936
937 /**
938 * Recreate daily table.
939 *
940 * @since 4.2.0
941 *
942 * @param bool $backup Whether to backup the table or not.
943 *
944 * @return bool|\WP_Error True if recreated, error message if failed.
945 */
946 public static function recreate_daily_table( $backup = true ) {
947 global $wpdb;
948 return self::recreate_table(
949 $wpdb->base_prefix . 'top_ten_daily',
950 self::create_daily_table_sql(),
951 $backup,
952 array( 'postnumber', 'cntaccess', 'dp_date', 'blog_id' ),
953 array( 'postnumber', 'dp_date', 'blog_id' )
954 );
955 }
956
957 /**
958 * Recreate visits funnel table.
959 *
960 * @since 4.3.0
961 *
962 * @param bool $backup Whether to create a permanent backup table before recreating.
963 *
964 * @return bool|\WP_Error True if recreated, error message if failed.
965 */
966 public static function recreate_funnel_table( $backup = true ) {
967 global $wpdb;
968
969 $table_name = self::get_funnel_table();
970 $backup_table_name = $backup ? $table_name . '_backup' : $table_name . '_temp';
971 $fields_sql = 'postnumber, blog_id, visited_at, activate_counter, source';
972 $success = false;
973
974 if ( $backup ) {
975 $success = $wpdb->query( "CREATE TABLE $backup_table_name LIKE $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
976 if ( false === $success ) {
977 /* translators: 1: Site number, 2: Error message */
978 return new \WP_Error( 'tptn_database_backup_failed', sprintf( esc_html__( 'Database backup failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
979 }
980 $wpdb->query( "INSERT INTO $backup_table_name SELECT * FROM $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
981 } else {
982 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
983 $success = $wpdb->query( "CREATE TEMPORARY TABLE $backup_table_name AS SELECT $fields_sql FROM $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
984 }
985
986 if ( false !== $success ) {
987 $wpdb->query( "DROP TABLE IF EXISTS $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
988 $wpdb->query( self::create_funnel_table_sql() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
989
990 $success = $wpdb->query( "INSERT INTO $table_name ($fields_sql) SELECT $fields_sql FROM $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
991
992 if ( false === $success ) {
993 /* translators: 1: Site number, 2: Error message */
994 return new \WP_Error( 'tptn_database_insert_failed', sprintf( esc_html__( 'Database insert failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
995 }
996 }
997
998 if ( ! $backup ) {
999 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1000 }
1001
1002 return $success;
1003 }
1004
1005 /**
1006 * Recreate visits log table.
1007 *
1008 * @since 4.3.0
1009 *
1010 * @param bool $backup Whether to create a permanent backup table before recreating.
1011 *
1012 * @return bool|\WP_Error True if recreated, error message if failed.
1013 */
1014 public static function recreate_log_table( $backup = true ) {
1015 global $wpdb;
1016
1017 $table_name = self::get_log_table();
1018 $backup_table_name = $backup ? $table_name . '_backup' : $table_name . '_temp';
1019 $fields_sql = 'postnumber, blog_id, visited_at, source';
1020 $success = false;
1021
1022 if ( $backup ) {
1023 $success = $wpdb->query( "CREATE TABLE $backup_table_name LIKE $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1024 if ( false === $success ) {
1025 /* translators: 1: Site number, 2: Error message */
1026 return new \WP_Error( 'tptn_database_backup_failed', sprintf( esc_html__( 'Database backup failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
1027 }
1028 $wpdb->query( "INSERT INTO $backup_table_name SELECT * FROM $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1029 } else {
1030 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1031 $success = $wpdb->query( "CREATE TEMPORARY TABLE $backup_table_name AS SELECT $fields_sql FROM $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1032 }
1033
1034 if ( false !== $success ) {
1035 $wpdb->query( "DROP TABLE IF EXISTS $table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1036 $wpdb->query( self::create_log_table_sql() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
1037
1038 $success = $wpdb->query( "INSERT INTO $table_name ($fields_sql) SELECT $fields_sql FROM $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1039
1040 if ( false === $success ) {
1041 /* translators: 1: Site number, 2: Error message */
1042 return new \WP_Error( 'tptn_database_insert_failed', sprintf( esc_html__( 'Database insert failed on site %1$s. Error message: %2$s', 'top-10' ), get_site_url(), $wpdb->last_error ) );
1043 }
1044 }
1045
1046 if ( ! $backup ) {
1047 $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS $backup_table_name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1048 }
1049
1050 return $success;
1051 }
1052
1053 /**
1054 * Truncate a table.
1055 *
1056 * @since 4.2.0
1057 *
1058 * @param string $table_name Table name to truncate.
1059 * @return bool True on success, false on failure.
1060 */
1061 public static function truncate_table( $table_name ) {
1062 global $wpdb;
1063
1064 // Table names cannot be parameterized in TRUNCATE statements.
1065 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1066 return $wpdb->query( "TRUNCATE TABLE $table_name" );
1067 }
1068
1069 /**
1070 * Count rows in the daily table that would be pruned up to a given date.
1071 *
1072 * @since 4.3.0
1073 *
1074 * @param string $to_date Rows with dp_date at or before this value are counted.
1075 * @return int Row count.
1076 */
1077 public static function count_deletable_daily_rows( string $to_date ): int {
1078 global $wpdb;
1079 $table = self::get_table( true );
1080 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1081 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE dp_date <= %s", $to_date ) );
1082 }
1083
1084 /**
1085 * Count rows in the visits log table older than a given datetime.
1086 *
1087 * @since 4.3.0
1088 *
1089 * @param string $before_datetime Rows with visited_at before this value are counted.
1090 * @return int Row count.
1091 */
1092 public static function count_deletable_log_rows( string $before_datetime ): int {
1093 global $wpdb;
1094 $table = self::get_log_table();
1095 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1096 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE visited_at < %s", $before_datetime ) );
1097 }
1098
1099 /**
1100 * Delete rows from the visits log table older than a given datetime.
1101 *
1102 * @since 4.3.0
1103 *
1104 * @param string $before_datetime Rows with visited_at before this value are deleted.
1105 * @param int $batch_size Maximum rows to delete per call.
1106 * @return int|false Rows deleted, or false on failure.
1107 */
1108 public static function prune_log_table( string $before_datetime, int $batch_size = 1000 ) {
1109 global $wpdb;
1110 $table = self::get_log_table();
1111 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1112 return $wpdb->query( $wpdb->prepare( "DELETE FROM `{$table}` WHERE visited_at < %s LIMIT %d", $before_datetime, $batch_size ) );
1113 }
1114
1115 /**
1116 * Count rows in the visits funnel table.
1117 *
1118 * @since 4.3.0
1119 *
1120 * @return int Row count.
1121 */
1122 public static function count_funnel_rows(): int {
1123 global $wpdb;
1124 $table = self::get_funnel_table();
1125 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1126 return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
1127 }
1128
1129 /**
1130 * Count orphaned rows in a count table (rows with no matching post).
1131 *
1132 * Only inspects rows belonging to the current blog so that posts on other
1133 * sites in a multisite network are not falsely reported as orphans.
1134 *
1135 * @since 4.3.0
1136 *
1137 * @param string $table_name Count table to inspect.
1138 * @return int Row count.
1139 */
1140 public static function count_orphan_counts( string $table_name ): int {
1141 global $wpdb;
1142 $blog_id = get_current_blog_id();
1143 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1144 return (int) $wpdb->get_var(
1145 $wpdb->prepare(
1146 "SELECT COUNT(*) FROM `{$table_name}` t
1147 LEFT JOIN `{$wpdb->posts}` p ON t.postnumber = p.ID
1148 WHERE p.ID IS NULL AND t.blog_id = %d",
1149 $blog_id
1150 )
1151 );
1152 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1153 }
1154
1155 /**
1156 * Delete orphaned rows from a count table (rows with no matching post).
1157 *
1158 * Only deletes rows belonging to the current blog so that posts on other
1159 * sites in a multisite network are not falsely treated as orphans.
1160 *
1161 * @since 4.3.0
1162 *
1163 * @param string $table_name Count table to clean.
1164 * @param int $batch_size Maximum rows to delete per call.
1165 * @return int|false Rows deleted, or false on failure.
1166 */
1167 public static function delete_orphan_counts( string $table_name, int $batch_size = 1000 ) {
1168 global $wpdb;
1169 $blog_id = get_current_blog_id();
1170 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1171 return $wpdb->query(
1172 $wpdb->prepare(
1173 "DELETE t FROM `{$table_name}` t
1174 LEFT JOIN `{$wpdb->posts}` p ON t.postnumber = p.ID
1175 WHERE p.ID IS NULL AND t.blog_id = %d
1176 LIMIT %d",
1177 $blog_id,
1178 $batch_size
1179 )
1180 );
1181 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1182 }
1183 }
1184