PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.0
WebberZone Top 10 — Popular Posts v4.4.0
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.4.0, at includes/class-database.php

1,267 lines 47.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 * Record a single visit using the configured tracking method.
683 *
684 * Funnel tracking (default) appends the visit to the funnel table which is
685 * drained into the count tables by the aggregation cron. Legacy tracking
686 * writes directly to the count tables on every visit (pre-4.3 behaviour)
687 * and does not populate the visits log.
688 *
689 * @since 4.3.3
690 *
691 * @param int $post_id Post ID.
692 * @param int $blog_id Blog ID.
693 * @param int $activate_counter Counter flag: 1 = overall, 10 = daily, 11 = both.
694 * @param int $source Traffic source: 0 = web, 1 = feed. Only stored by funnel tracking.
695 * @return int|false Rows inserted/updated or false on error.
696 */
697 public static function record_view( $post_id, $blog_id, $activate_counter = 11, $source = 0 ) {
698 if ( 'legacy' === \tptn_get_option( 'tracking_method', 'funnel' ) ) {
699 return self::update_counts_direct( $post_id, $blog_id, $activate_counter );
700 }
701
702 return self::append_to_funnel( $post_id, $blog_id, $activate_counter, $source );
703 }
704
705 /**
706 * Write a single visit directly to the overall and daily count tables.
707 *
708 * This is the legacy (pre-4.3) tracking method: an immediate upsert per view,
709 * bypassing the funnel table and the aggregation cron. The visits log is not
710 * populated by this method.
711 *
712 * @since 4.3.3
713 *
714 * @param int $post_id Post ID.
715 * @param int $blog_id Blog ID.
716 * @param int $activate_counter Counter flag: 1 = overall, 10 = daily, 11 = both.
717 * @return int|false Rows inserted/updated or false on error.
718 */
719 public static function update_counts_direct( $post_id, $blog_id, $activate_counter = 11 ) {
720 global $wpdb;
721
722 $post_id = absint( $post_id );
723 $blog_id = absint( $blog_id );
724 $activate_counter = (int) $activate_counter;
725 $rows = 0;
726
727 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
728 if ( in_array( $activate_counter, array( 1, 11 ), true ) ) {
729 $table = self::get_table( false );
730 $result = $wpdb->query(
731 $wpdb->prepare(
732 "INSERT INTO {$table} (postnumber, cntaccess, blog_id) VALUES (%d, 1, %d) ON DUPLICATE KEY UPDATE cntaccess = cntaccess + 1",
733 $post_id,
734 $blog_id
735 )
736 );
737 if ( false === $result ) {
738 return false;
739 }
740 $rows += (int) $result;
741 }
742
743 if ( in_array( $activate_counter, array( 10, 11 ), true ) ) {
744 $table = self::get_table( true );
745 $dp_date = current_time( 'Y-m-d H' ) . ':00:00';
746 $result = $wpdb->query(
747 $wpdb->prepare(
748 "INSERT INTO {$table} (postnumber, cntaccess, dp_date, blog_id) VALUES (%d, 1, %s, %d) ON DUPLICATE KEY UPDATE cntaccess = cntaccess + 1",
749 $post_id,
750 $dp_date,
751 $blog_id
752 )
753 );
754 if ( false === $result ) {
755 return false;
756 }
757 $rows += (int) $result;
758 }
759 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
760
761 return $rows;
762 }
763
764 /**
765 * Append a single visit to the funnel table.
766 *
767 * @since 4.3.0
768 *
769 * @param int $post_id Post ID.
770 * @param int $blog_id Blog ID.
771 * @param int $activate_counter Counter flag: 1 = overall, 10 = daily, 11 = both.
772 * @param int $source Traffic source: 0 = web, 1 = feed.
773 * @return int|false Rows inserted or false on error.
774 */
775 public static function append_to_funnel( $post_id, $blog_id, $activate_counter = 11, $source = 0 ) {
776 global $wpdb;
777
778 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
779 return $wpdb->insert(
780 self::get_funnel_table(),
781 array(
782 'postnumber' => absint( $post_id ),
783 'blog_id' => absint( $blog_id ),
784 'visited_at' => current_time( 'mysql' ),
785 'activate_counter' => (int) $activate_counter,
786 'source' => (int) $source,
787 ),
788 array( '%d', '%d', '%s', '%d', '%d' )
789 );
790 }
791
792 /**
793 * Drain the funnel into the log and count tables, then empty the funnel.
794 *
795 * All four operations (copy to log, aggregate to daily, aggregate to overall,
796 * delete from funnel) run inside one transaction. A failure rolls back cleanly
797 * and the next run retries the same rows with no double-counting risk.
798 *
799 * @since 4.3.0
800 *
801 * @param int $batch_size Maximum funnel rows to process per run.
802 * @return true|false|int|\WP_Error True if rows processed, false if lock not acquired, 0 if funnel empty, WP_Error on DB failure.
803 */
804 public static function aggregate_visit_log( $batch_size = 10000 ) {
805 global $wpdb;
806
807 // Detect SQLite (e.g. WordPress Playground) vs MySQL/MariaDB.
808 // DATABASE_TYPE is defined by the WordPress SQLite Database Integration drop-in.
809 $is_sqlite = ( defined( 'DATABASE_TYPE' ) && 'sqlite' === DATABASE_TYPE )
810 || false !== strpos( strtolower( (string) $wpdb->db_server_info() ), 'sqlite' );
811
812 $funnel_table = self::get_funnel_table();
813 $log_table = self::get_log_table();
814 $daily_table = self::get_table( true );
815 $full_table = self::get_table( false );
816
817 // MySQL-specific locking and transactions.
818 if ( ! $is_sqlite ) {
819 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
820 $lock_acquired = $wpdb->get_var( "SELECT GET_LOCK('tptn_aggregation', 0)" );
821 if ( '1' !== (string) $lock_acquired ) {
822 return false;
823 }
824
825 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
826 if ( false === $wpdb->query( 'START TRANSACTION' ) ) {
827 $wpdb->query( "SELECT RELEASE_LOCK('tptn_aggregation')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
828 return new \WP_Error( 'tptn_transaction_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Could not start transaction.', 'top-10' ) );
829 }
830 }
831
832 try {
833 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
834 $max_id = (int) $wpdb->get_var( "SELECT MAX(id) FROM {$funnel_table}" );
835 if ( 0 === $max_id ) {
836 if ( ! $is_sqlite ) {
837 $wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
838 }
839 return 0;
840 }
841
842 $cap_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$funnel_table} ORDER BY id ASC LIMIT %d, 1", $batch_size ) );
843 $was_capped = false;
844 if ( null !== $cap_id ) {
845 $capped_max = (int) $cap_id - 1;
846 if ( $capped_max > 0 ) {
847 $max_id = $capped_max;
848 $was_capped = true;
849 }
850 }
851
852 $r = $wpdb->query(
853 $wpdb->prepare(
854 "INSERT INTO {$log_table} (postnumber, blog_id, visited_at, source)
855 SELECT postnumber, blog_id, visited_at, source
856 FROM {$funnel_table}
857 WHERE id <= %d",
858 $max_id
859 )
860 );
861 if ( false === $r ) {
862 if ( ! $is_sqlite ) {
863 $wpdb->query( 'ROLLBACK' );
864 }
865 return new \WP_Error( 'tptn_log_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to copy visits to log table.', 'top-10' ) );
866 }
867
868 $r = $wpdb->query(
869 $wpdb->prepare(
870 "INSERT INTO {$daily_table} (postnumber, cntaccess, dp_date, blog_id)
871 SELECT postnumber, COUNT(*) AS cntaccess,
872 DATE_FORMAT(visited_at, '%%Y-%%m-%%d %%H:00:00') AS dp_date, blog_id
873 FROM {$funnel_table}
874 WHERE id <= %d AND activate_counter IN (10, 11)
875 GROUP BY postnumber, DATE_FORMAT(visited_at, '%%Y-%%m-%%d %%H:00:00'), blog_id
876 ON DUPLICATE KEY UPDATE cntaccess = {$daily_table}.cntaccess + VALUES(cntaccess)",
877 $max_id
878 )
879 );
880 if ( false === $r ) {
881 if ( ! $is_sqlite ) {
882 $wpdb->query( 'ROLLBACK' );
883 }
884 return new \WP_Error( 'tptn_daily_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to aggregate visits into daily table.', 'top-10' ) );
885 }
886
887 $r = $wpdb->query(
888 $wpdb->prepare(
889 "INSERT INTO {$full_table} (postnumber, cntaccess, blog_id)
890 SELECT postnumber, COUNT(*) AS cntaccess, blog_id
891 FROM {$funnel_table}
892 WHERE id <= %d AND activate_counter IN (1, 11)
893 GROUP BY postnumber, blog_id
894 ON DUPLICATE KEY UPDATE cntaccess = {$full_table}.cntaccess + VALUES(cntaccess)",
895 $max_id
896 )
897 );
898 if ( false === $r ) {
899 if ( ! $is_sqlite ) {
900 $wpdb->query( 'ROLLBACK' );
901 }
902 return new \WP_Error( 'tptn_overall_insert_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to aggregate visits into overall table.', 'top-10' ) );
903 }
904
905 $r = $wpdb->query( $wpdb->prepare( "DELETE FROM {$funnel_table} WHERE id <= %d", $max_id ) );
906 if ( false === $r ) {
907 if ( ! $is_sqlite ) {
908 $wpdb->query( 'ROLLBACK' );
909 }
910 return new \WP_Error( 'tptn_funnel_delete_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Failed to drain funnel table.', 'top-10' ) );
911 }
912 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
913
914 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
915 if ( ! $is_sqlite && false === $wpdb->query( 'COMMIT' ) ) {
916 $wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
917 return new \WP_Error( 'tptn_commit_failed', $wpdb->last_error ? $wpdb->last_error : __( 'Transaction commit failed.', 'top-10' ) );
918 }
919
920 do_action( 'tptn_count_updated', 0, 0, false );
921
922 if ( $was_capped && ! wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
923 wp_schedule_single_event( time(), 'tptn_aggregation_cron_hook' );
924 }
925
926 return true;
927 } finally {
928 if ( ! $is_sqlite ) {
929 $wpdb->query( "SELECT RELEASE_LOCK('tptn_aggregation')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
930 }
931 }
932 }
933
934 /**
935 * Recreate a table.
936 *
937 * This method recreates a table by creating a backup, dropping the original table,
938 * and then creating a new table with the original name and inserting the data from the backup.
939 *
940 * @since 4.2.0
941 *
942 * @param string $table_name The name of the table to recreate.
943 * @param string $create_table_sql The SQL statement to create the new table.
944 * @param bool $backup Whether to backup the table or not.
945 * @param array $fields The fields to include in the temporary table and on duplicate key code.
946 * @param array $group_by_fields The fields to group by in the temporary table.
947 *
948 * @return bool|\WP_Error True if recreated, error message if failed.
949 */
950 public static function recreate_table(
951 $table_name,
952 $create_table_sql,
953 $backup = true,
954 $fields = array( 'postnumber', 'cntaccess', 'blog_id' ),
955 $group_by_fields = array( 'postnumber', 'blog_id' )
956 ) {
957 global $wpdb;
958
959 $backup_table_name = ( $backup ) ? $table_name . '_backup' : $table_name . '_temp';
960 $success = false;
961
962 $fields_sql = implode( ', ', $fields );
963 $fields_sql_with_sum = str_replace( 'cntaccess', 'SUM(cntaccess) as cntaccess', $fields_sql );
964 $group_by_sql = implode( ', ', $group_by_fields );
965
966 if ( $backup ) {
967 $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
968 if ( false !== $success ) {
969 $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
970 } else {
971 /* translators: 1: Site number, 2: Error message */
972 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 ) );
973 }
974 } else {
975 $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
976 $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
977 }
978
979 if ( false !== $success ) {
980 $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
981
982 // Direct table creation without dbDelta for recreation.
983 $wpdb->query( $create_table_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
984
985 $insert_fields_sql = 'tt.' . implode( ', tt.', $fields );
986
987 $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
988
989 if ( false === $success ) {
990 /* translators: 1: Site number, 2: Error message */
991 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 ) );
992 }
993 }
994
995 if ( ! $backup ) {
996 $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
997 }
998
999 return $success;
1000 }
1001
1002 /**
1003 * Recreate overall table.
1004 *
1005 * @since 4.2.0
1006 *
1007 * @param bool $backup Whether to backup the table or not.
1008 *
1009 * @return bool|\WP_Error True if recreated, error message if failed.
1010 */
1011 public static function recreate_overall_table( $backup = true ) {
1012 global $wpdb;
1013 return self::recreate_table(
1014 $wpdb->base_prefix . 'top_ten',
1015 self::create_full_table_sql(),
1016 $backup
1017 );
1018 }
1019
1020 /**
1021 * Recreate daily table.
1022 *
1023 * @since 4.2.0
1024 *
1025 * @param bool $backup Whether to backup the table or not.
1026 *
1027 * @return bool|\WP_Error True if recreated, error message if failed.
1028 */
1029 public static function recreate_daily_table( $backup = true ) {
1030 global $wpdb;
1031 return self::recreate_table(
1032 $wpdb->base_prefix . 'top_ten_daily',
1033 self::create_daily_table_sql(),
1034 $backup,
1035 array( 'postnumber', 'cntaccess', 'dp_date', 'blog_id' ),
1036 array( 'postnumber', 'dp_date', 'blog_id' )
1037 );
1038 }
1039
1040 /**
1041 * Recreate visits funnel table.
1042 *
1043 * @since 4.3.0
1044 *
1045 * @param bool $backup Whether to create a permanent backup table before recreating.
1046 *
1047 * @return bool|\WP_Error True if recreated, error message if failed.
1048 */
1049 public static function recreate_funnel_table( $backup = true ) {
1050 global $wpdb;
1051
1052 $table_name = self::get_funnel_table();
1053 $backup_table_name = $backup ? $table_name . '_backup' : $table_name . '_temp';
1054 $fields_sql = 'postnumber, blog_id, visited_at, activate_counter, source';
1055 $success = false;
1056
1057 if ( $backup ) {
1058 $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
1059 if ( false === $success ) {
1060 /* translators: 1: Site number, 2: Error message */
1061 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 ) );
1062 }
1063 $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
1064 } else {
1065 $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
1066 $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
1067 }
1068
1069 if ( false !== $success ) {
1070 $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
1071 $wpdb->query( self::create_funnel_table_sql() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
1072
1073 $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
1074
1075 if ( false === $success ) {
1076 /* translators: 1: Site number, 2: Error message */
1077 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 ) );
1078 }
1079 }
1080
1081 if ( ! $backup ) {
1082 $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
1083 }
1084
1085 return $success;
1086 }
1087
1088 /**
1089 * Recreate visits log table.
1090 *
1091 * @since 4.3.0
1092 *
1093 * @param bool $backup Whether to create a permanent backup table before recreating.
1094 *
1095 * @return bool|\WP_Error True if recreated, error message if failed.
1096 */
1097 public static function recreate_log_table( $backup = true ) {
1098 global $wpdb;
1099
1100 $table_name = self::get_log_table();
1101 $backup_table_name = $backup ? $table_name . '_backup' : $table_name . '_temp';
1102 $fields_sql = 'postnumber, blog_id, visited_at, source';
1103 $success = false;
1104
1105 if ( $backup ) {
1106 $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
1107 if ( false === $success ) {
1108 /* translators: 1: Site number, 2: Error message */
1109 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 ) );
1110 }
1111 $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
1112 } else {
1113 $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
1114 $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
1115 }
1116
1117 if ( false !== $success ) {
1118 $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
1119 $wpdb->query( self::create_log_table_sql() ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
1120
1121 $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
1122
1123 if ( false === $success ) {
1124 /* translators: 1: Site number, 2: Error message */
1125 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 ) );
1126 }
1127 }
1128
1129 if ( ! $backup ) {
1130 $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
1131 }
1132
1133 return $success;
1134 }
1135
1136 /**
1137 * Truncate a table.
1138 *
1139 * @since 4.2.0
1140 *
1141 * @param string $table_name Table name to truncate.
1142 * @return bool True on success, false on failure.
1143 */
1144 public static function truncate_table( $table_name ) {
1145 global $wpdb;
1146
1147 // Table names cannot be parameterized in TRUNCATE statements.
1148 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1149 return $wpdb->query( "TRUNCATE TABLE $table_name" );
1150 }
1151
1152 /**
1153 * Count rows in the daily table that would be pruned up to a given date.
1154 *
1155 * @since 4.3.0
1156 *
1157 * @param string $to_date Rows with dp_date at or before this value are counted.
1158 * @return int Row count.
1159 */
1160 public static function count_deletable_daily_rows( string $to_date ): int {
1161 global $wpdb;
1162 $table = self::get_table( true );
1163 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1164 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE dp_date <= %s", $to_date ) );
1165 }
1166
1167 /**
1168 * Count rows in the visits log table older than a given datetime.
1169 *
1170 * @since 4.3.0
1171 *
1172 * @param string $before_datetime Rows with visited_at before this value are counted.
1173 * @return int Row count.
1174 */
1175 public static function count_deletable_log_rows( string $before_datetime ): int {
1176 global $wpdb;
1177 $table = self::get_log_table();
1178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1179 return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE visited_at < %s", $before_datetime ) );
1180 }
1181
1182 /**
1183 * Delete rows from the visits log table older than a given datetime.
1184 *
1185 * @since 4.3.0
1186 *
1187 * @param string $before_datetime Rows with visited_at before this value are deleted.
1188 * @param int $batch_size Maximum rows to delete per call.
1189 * @return int|false Rows deleted, or false on failure.
1190 */
1191 public static function prune_log_table( string $before_datetime, int $batch_size = 1000 ) {
1192 global $wpdb;
1193 $table = self::get_log_table();
1194 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1195 return $wpdb->query( $wpdb->prepare( "DELETE FROM `{$table}` WHERE visited_at < %s LIMIT %d", $before_datetime, $batch_size ) );
1196 }
1197
1198 /**
1199 * Count rows in the visits funnel table.
1200 *
1201 * @since 4.3.0
1202 *
1203 * @return int Row count.
1204 */
1205 public static function count_funnel_rows(): int {
1206 global $wpdb;
1207 $table = self::get_funnel_table();
1208 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1209 return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
1210 }
1211
1212 /**
1213 * Count orphaned rows in a count table (rows with no matching post).
1214 *
1215 * Only inspects rows belonging to the current blog so that posts on other
1216 * sites in a multisite network are not falsely reported as orphans.
1217 *
1218 * @since 4.3.0
1219 *
1220 * @param string $table_name Count table to inspect.
1221 * @return int Row count.
1222 */
1223 public static function count_orphan_counts( string $table_name ): int {
1224 global $wpdb;
1225 $blog_id = get_current_blog_id();
1226 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1227 return (int) $wpdb->get_var(
1228 $wpdb->prepare(
1229 "SELECT COUNT(*) FROM `{$table_name}` t
1230 LEFT JOIN `{$wpdb->posts}` p ON t.postnumber = p.ID
1231 WHERE p.ID IS NULL AND t.blog_id = %d",
1232 $blog_id
1233 )
1234 );
1235 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1236 }
1237
1238 /**
1239 * Delete orphaned rows from a count table (rows with no matching post).
1240 *
1241 * Only deletes rows belonging to the current blog so that posts on other
1242 * sites in a multisite network are not falsely treated as orphans.
1243 *
1244 * @since 4.3.0
1245 *
1246 * @param string $table_name Count table to clean.
1247 * @param int $batch_size Maximum rows to delete per call.
1248 * @return int|false Rows deleted, or false on failure.
1249 */
1250 public static function delete_orphan_counts( string $table_name, int $batch_size = 1000 ) {
1251 global $wpdb;
1252 $blog_id = get_current_blog_id();
1253 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1254 return $wpdb->query(
1255 $wpdb->prepare(
1256 "DELETE t FROM `{$table_name}` t
1257 LEFT JOIN `{$wpdb->posts}` p ON t.postnumber = p.ID
1258 WHERE p.ID IS NULL AND t.blog_id = %d
1259 LIMIT %d",
1260 $blog_id,
1261 $batch_size
1262 )
1263 );
1264 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1265 }
1266 }
1267