PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
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 1.6.2 All 116 releases
top-10 / includes / admin / class-statistics-table.php

class-statistics-table.php in WebberZone Top 10 — Popular Posts trunk, at includes/admin/class-statistics-table.php

950 lines 31.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Statistics Table class.
4 *
5 * @package WebberZone\Top_Ten\Admin
6 */
7
8 namespace WebberZone\Top_Ten\Admin;
9
10 use WebberZone\Top_Ten\Database;
11 use WebberZone\Top_Ten\Util\Helpers;
12
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 if ( ! class_exists( '\WP_List_Table' ) ) {
18 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
19 }
20
21 /**
22 * Statistics Table class to display the popular counts.
23 *
24 * @since 3.3.0
25 */
26 class Statistics_Table extends \WP_List_Table {
27
28 /**
29 * Holds the post type array elements for translation.
30 *
31 * @var array
32 */
33 public $all_post_type;
34
35 /**
36 * Network wide popular posts flag.
37 *
38 * @var bool
39 */
40 public $network_wide;
41
42 /**
43 * Class constructor.
44 *
45 * @param bool $network_wide Network wide popular posts.
46 */
47 public function __construct( $network_wide = false ) {
48 parent::__construct(
49 array(
50 'singular' => __( 'popular_post', 'top-10' ), // Singular name of the listed records.
51 'plural' => __( 'popular_posts', 'top-10' ), // plural name of the listed records.
52 )
53 );
54 $this->all_post_type = array(
55 'all' => __( 'All post types', 'top-10' ),
56 );
57 $this->network_wide = $network_wide;
58 }
59
60 /**
61 * Get the fixed IDs reserved for site-wide tracking contexts.
62 *
63 * The class only exists in the Pro build. Keeping this lookup optional lets
64 * the shared statistics table continue to work in the free plugin.
65 *
66 * @return array<int,int> Reserved context IDs.
67 */
68 protected function get_sitewide_context_ids() {
69 if ( ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
70 return array();
71 }
72
73 return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_ids();
74 }
75
76 /**
77 * Build a SQL condition matching any reserved site-wide context ID.
78 *
79 * @param string $alias Count-table alias.
80 * @return string SQL condition, or an always-false condition when Pro is not loaded.
81 */
82 protected function get_sitewide_context_condition( $alias = 'ttt' ) {
83 $ids = $this->get_sitewide_context_ids();
84 if ( empty( $ids ) ) {
85 return '0=1';
86 }
87
88 return sprintf( '%s.postnumber IN (%s)', $alias, implode( ',', array_map( 'intval', $ids ) ) );
89 }
90
91 /**
92 * Get the site-wide context key for a result row.
93 *
94 * @param array $item Result row.
95 * @return string Context key, or an empty string for a post row.
96 */
97 protected function get_item_context_key( $item ) {
98 if ( ! isset( $item['ID'] ) || ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
99 return '';
100 }
101
102 return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_key( $item['ID'], $item['blog_id'] ?? null );
103 }
104
105 /**
106 * Get a translated label for a site-wide context key.
107 *
108 * @param string $context_key Context key.
109 * @return string Context label.
110 */
111 protected function get_sitewide_context_label( $context_key ) {
112 if ( '' === $context_key || ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
113 return '';
114 }
115
116 return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_label( $context_key );
117 }
118
119 /**
120 * Build the search condition for site-wide context labels and keys.
121 *
122 * @param string $search Search text.
123 * @return string SQL condition, or an always-false condition when Pro is not loaded.
124 */
125 protected function get_sitewide_context_search_condition( $search ) {
126 if ( ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
127 return '0=1';
128 }
129
130 global $wpdb;
131
132 $like = '%' . $wpdb->esc_like( $search ) . '%';
133 $conditions = array();
134 foreach ( \WebberZone\Top_Ten\Pro\Sitewide_Database::CONTEXT_IDS as $context_key => $context_id ) {
135 $searchable = $context_key . ' ' . $this->get_sitewide_context_label( $context_key );
136 $conditions[] = $wpdb->prepare( 'ttt.postnumber = %d AND %s LIKE %s', $context_id, $searchable, $like );
137 }
138
139 return '(' . implode( ' OR ', $conditions ) . ')';
140 }
141
142 /**
143 * Resolve a safe ordering clause for statistics queries.
144 *
145 * @since 4.5.0
146 *
147 * @param array $args Query arguments.
148 * @return array{orderby:string,order:string} Ordering fields.
149 */
150 protected function get_ordering( $args ) {
151 $orderby = '';
152 if ( ! empty( $_REQUEST['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
153 $orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
154 } elseif ( ! empty( $args['orderby'] ) ) {
155 $orderby = $args['orderby'];
156 }
157
158 if ( ! in_array( $orderby, array( 'title', 'daily_count', 'total_count' ), true ) ) {
159 $orderby = 'total_count';
160 }
161
162 $order = '';
163 if ( ! empty( $_REQUEST['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
164 $order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
165 } elseif ( ! empty( $args['order'] ) ) {
166 $order = $args['order'];
167 }
168
169 if ( ! in_array( $order, array( 'asc', 'ASC', 'desc', 'DESC' ), true ) ) {
170 $order = 'DESC';
171 }
172
173 return array(
174 'orderby' => $orderby,
175 'order' => strtoupper( $order ),
176 );
177 }
178
179 /**
180 * Fetch network-wide popular posts in two phases.
181 *
182 * The first query selects only the requested page in the sort order. The
183 * second query fills in the other count using exact post/blog pairs, which
184 * avoids joining and grouping the entire network's count tables.
185 *
186 * @since 4.5.0
187 *
188 * @param int $per_page Posts per page.
189 * @param int $page_number Page number.
190 * @param array $args Query arguments.
191 * @param string $from_date Inclusive lower date boundary.
192 * @param string $to_date Exclusive upper date boundary.
193 * @return array Array of popular posts.
194 */
195 protected function get_network_popular_posts( $per_page, $page_number, $args, $from_date, $to_date ) {
196 global $wpdb;
197
198 $ordering = $this->get_ordering( $args );
199 $orderby = 'daily_count' === $ordering['orderby'] ? 'daily_count' : 'total_count';
200 $offset = max( 0, ( (int) $page_number - 1 ) * (int) $per_page );
201 $limit = max( 0, (int) $per_page );
202
203 if ( 0 === $limit ) {
204 return array();
205 }
206
207 $daily_table = $wpdb->base_prefix . 'top_ten_daily';
208 $total_table = $wpdb->base_prefix . 'top_ten';
209
210 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
211 if ( 'daily_count' === $orderby ) {
212 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
213 $sql = $wpdb->prepare(
214 "SELECT postnumber AS ID, blog_id, SUM(cntaccess) AS daily_count
215 FROM {$daily_table}
216 WHERE dp_date >= %s AND dp_date < %s
217 GROUP BY postnumber, blog_id
218 ORDER BY daily_count {$ordering['order']}, postnumber ASC, blog_id ASC
219 LIMIT %d, %d",
220 $from_date,
221 $to_date,
222 $offset,
223 $limit
224 );
225 } else {
226 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
227 $sql = $wpdb->prepare(
228 "SELECT postnumber AS ID, blog_id, cntaccess AS total_count
229 FROM {$total_table}
230 ORDER BY cntaccess {$ordering['order']}, postnumber ASC, blog_id ASC
231 LIMIT %d, %d",
232 $offset,
233 $limit
234 );
235 }
236
237 $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
238 if ( empty( $results ) ) {
239 return array();
240 }
241
242 $pair_conditions = array();
243 foreach ( $results as $result ) {
244 $pair_conditions[] = $wpdb->prepare( '(postnumber = %d AND blog_id = %d)', $result['ID'], $result['blog_id'] );
245 }
246 $pairs = implode( ' OR ', $pair_conditions );
247
248 if ( 'daily_count' === $orderby ) {
249 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
250 $other_results = $wpdb->get_results(
251 "SELECT postnumber AS ID, blog_id, cntaccess AS total_count FROM {$total_table} WHERE {$pairs}",
252 ARRAY_A
253 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
254 } else {
255 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
256 $other_results = $wpdb->get_results(
257 $wpdb->prepare(
258 "SELECT postnumber AS ID, blog_id, SUM(cntaccess) AS daily_count
259 FROM {$daily_table}
260 WHERE dp_date >= %s AND dp_date < %s AND ({$pairs})
261 GROUP BY postnumber, blog_id",
262 $from_date,
263 $to_date
264 ),
265 ARRAY_A
266 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
267 }
268
269 $other_counts = array();
270 foreach ( $other_results as $result ) {
271 $key = (int) $result['ID'] . ':' . (int) $result['blog_id'];
272 $other_counts[ $key ] = $result[ 'daily_count' === $orderby ? 'total_count' : 'daily_count' ];
273 }
274
275 foreach ( $results as &$result ) {
276 $key = (int) $result['ID'] . ':' . (int) $result['blog_id'];
277 if ( 'daily_count' === $orderby ) {
278 $result['total_count'] = isset( $other_counts[ $key ] ) ? $other_counts[ $key ] : 0;
279 } else {
280 $result['daily_count'] = isset( $other_counts[ $key ] ) ? $other_counts[ $key ] : 0;
281 }
282 }
283 unset( $result );
284
285 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
286 return $results;
287 }
288
289 /**
290 * Retrieve the Top 10 posts
291 *
292 * @param int $per_page Posts per page.
293 * @param int $page_number Page number.
294 * @param array $args Array of arguments.
295 *
296 * @return array Array of popular posts
297 */
298 public function get_popular_posts( $per_page = 20, $page_number = 1, $args = null ) {
299
300 global $wpdb;
301 $args = is_array( $args ) ? $args : array();
302
303 // Initialise some variables.
304 $fields = array();
305 $where = '';
306 $join = '';
307 $groupby = '';
308 $orderby = '';
309 $limits = '';
310 $sql = '';
311
312 $blog_id = get_current_blog_id();
313
314 $from_date = isset( $args['post-date-filter-from'] ) ? $args['post-date-filter-from'] : current_time( 'd M Y' );
315 $from_date = gmdate( 'Y-m-d', strtotime( $from_date ) );
316 $to_date = isset( $args['post-date-filter-to'] ) ? $args['post-date-filter-to'] : current_time( 'd M Y' );
317 $to_date = gmdate( 'Y-m-d', strtotime( $to_date ) );
318 $from_date = $from_date . ' 00:00:00';
319 $to_date = gmdate( 'Y-m-d 00:00:00', strtotime( $to_date . ' +1 day' ) );
320
321 if ( $this->network_wide ) {
322 $result = $this->get_network_popular_posts( $per_page, $page_number, $args, $from_date, $to_date );
323
324 foreach ( $result as &$row ) {
325 $row['context_key'] = $this->get_item_context_key( $row );
326 if ( '' !== $row['context_key'] ) {
327 $row['title'] = $this->get_sitewide_context_label( $row['context_key'] );
328 $row['post_type'] = __( 'Site-wide', 'top-10' );
329 $row['post_date'] = '';
330 $row['post_author'] = 0;
331 }
332 }
333 unset( $row );
334
335 return $result;
336 }
337
338 /* Start creating the SQL */
339 $table_name_daily = $wpdb->base_prefix . 'top_ten_daily';
340 $table_name = $wpdb->base_prefix . 'top_ten AS ttt';
341 $sitewide_sql = $this->get_sitewide_context_condition();
342
343 // Fields to return.
344 $fields[] = "{$wpdb->posts}.post_title as title";
345 $fields[] = "{$wpdb->posts}.post_type";
346 $fields[] = "{$wpdb->posts}.post_date";
347 $fields[] = "{$wpdb->posts}.post_author";
348
349 $fields[] = 'ttt.postnumber as ID';
350 $fields[] = 'ttt.cntaccess as total_count';
351 $fields[] = 'SUM(ttd.daily_count) as daily_count';
352 $fields[] = 'ttt.blog_id as blog_id';
353
354 $fields = implode( ', ', $fields );
355
356 // Create the JOIN clause.
357 $join .= " LEFT JOIN {$wpdb->posts} ON ttt.postnumber={$wpdb->posts}.ID ";
358 $join .= $wpdb->prepare(
359 " LEFT JOIN (
360 SELECT postnumber, blog_id, SUM(cntaccess) AS daily_count FROM {$table_name_daily} " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
361 'WHERE dp_date >= %s AND dp_date < %s AND blog_id = %d
362 GROUP BY postnumber, blog_id
363 ) AS ttd
364 ON ttt.postnumber=ttd.postnumber AND ttt.blog_id=ttd.blog_id
365 ',
366 $from_date,
367 $to_date,
368 $blog_id
369 );
370
371 // Create the base WHERE clause.
372 $where .= $wpdb->prepare( ' AND ttt.blog_id = %d ', $blog_id ); // Posts need to be from the current blog only.
373 $where .= " AND (($wpdb->posts.post_status = 'publish' OR $wpdb->posts.post_status = 'inherit') OR {$sitewide_sql}) "; // Show published posts, attachments and site-wide contexts.
374
375 // If search argument is set, do a search for it.
376 if ( ! empty( $args['search'] ) ) {
377 $post_search = $wpdb->prepare( "$wpdb->posts.post_title LIKE %s", '%' . $wpdb->esc_like( $args['search'] ) . '%' );
378 $context_search = $this->get_sitewide_context_search_condition( $args['search'] );
379 $where .= " AND ({$post_search} OR {$context_search}) ";
380 }
381
382 // If post filter argument is set, do a search for it.
383 if ( isset( $args['post-type-filter'] ) && $this->all_post_type['all'] !== $args['post-type-filter'] ) {
384 $where .= $wpdb->prepare( " AND $wpdb->posts.post_type = %s ", $args['post-type-filter'] );
385 } else {
386 $post_types = get_post_types(
387 array(
388 'public' => true,
389 )
390 );
391 $where .= " AND ($wpdb->posts.post_type IN ('" . join( "', '", $post_types ) . "') OR {$sitewide_sql}) ";
392 }
393
394 $ordering = $this->get_ordering( $args );
395 $orderby = $ordering['orderby'] . ' ' . $ordering['order'];
396
397 // Create the base LIMITS clause.
398 $limits = $wpdb->prepare( ' LIMIT %d, %d ', ( $page_number - 1 ) * $per_page, $per_page );
399
400 $groupby = ' GROUP BY ttt.postnumber, ttt.blog_id ';
401 $orderby = " ORDER BY {$orderby} ";
402
403 $sql = "SELECT $fields FROM {$table_name} $join WHERE 1=1 $where $groupby $orderby $limits";
404
405 $result = $wpdb->get_results( $sql, 'ARRAY_A' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
406
407 foreach ( $result as &$row ) {
408 $row['context_key'] = $this->get_item_context_key( $row );
409 if ( '' !== $row['context_key'] ) {
410 $row['title'] = $this->get_sitewide_context_label( $row['context_key'] );
411 $row['post_type'] = __( 'Site-wide', 'top-10' );
412 $row['post_date'] = '';
413 $row['post_author'] = 0;
414 }
415 }
416 unset( $row );
417
418 return $result;
419 }
420
421 /**
422 * Returns the count of records in the database.
423 *
424 * @param array $args Array of arguments.
425 * @return null|string null|string
426 */
427 public function record_count( $args = null ) {
428
429 global $wpdb;
430 $args = is_array( $args ) ? $args : array();
431
432 if ( $this->network_wide ) {
433 $cache_key = 'tptn_network_popular_posts_count';
434 $cached = get_site_transient( $cache_key );
435 if ( false !== $cached ) {
436 return (string) (int) $cached;
437 }
438
439 $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->base_prefix}top_ten" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
440 /** This filter is documented in includes/admin/class-dashboard-widgets.php */
441 $ttl = max( 0, (int) apply_filters( 'tptn_network_dashboard_cache_ttl', 15 * MINUTE_IN_SECONDS ) );
442 if ( $ttl > 0 ) {
443 set_site_transient( $cache_key, $count, $ttl );
444 }
445
446 return (string) $count;
447 }
448
449 $where = '';
450 $join = '';
451
452 $sql = "SELECT COUNT(*) FROM {$wpdb->base_prefix}top_ten as ttt";
453
454 $join = "LEFT JOIN {$wpdb->posts} ON ttt.postnumber={$wpdb->posts}.ID";
455 $sitewide_sql = $this->get_sitewide_context_condition();
456 $where .= $wpdb->prepare( ' AND ttt.blog_id = %d ', get_current_blog_id() );
457 $where .= " AND (($wpdb->posts.post_status = 'publish' OR $wpdb->posts.post_status = 'inherit') OR {$sitewide_sql}) ";
458
459 if ( ! empty( $args['search'] ) ) {
460 $post_search = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", '%' . $wpdb->esc_like( $args['search'] ) . '%' );
461 $context_search = $this->get_sitewide_context_search_condition( $args['search'] );
462 $where .= " AND ({$post_search} OR {$context_search}) ";
463 }
464
465 if ( isset( $args['post-type-filter'] ) && $this->all_post_type['all'] !== $args['post-type-filter'] ) {
466 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type = %s ", $args['post-type-filter'] );
467 } else {
468 $post_types = get_post_types(
469 array(
470 'public' => true,
471 )
472 );
473 $where .= " AND ($wpdb->posts.post_type IN ('" . join( "', '", $post_types ) . "') OR {$sitewide_sql}) ";
474 }
475 return $wpdb->get_var( "$sql $join WHERE 1=1 $where" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
476 }
477
478 /**
479 * Delete the post count for this post.
480 *
481 * @param int $id Post ID.
482 * @param int $blog_id Blog ID.
483 */
484 public static function delete_post_count( $id, $blog_id ) {
485 \WebberZone\Top_Ten\Counter::delete_count( $id, $blog_id, false );
486 \WebberZone\Top_Ten\Counter::delete_count( $id, $blog_id, true );
487 }
488
489 /**
490 * Text displayed when no post data is available
491 */
492 public function no_items() {
493 esc_html_e( 'No popular posts available.', 'top-10' );
494 }
495
496
497 /**
498 * Render a column when no column specific method exist.
499 *
500 * @param array $item Current item.
501 * @param string $column_name Column name.
502 *
503 * @return mixed
504 */
505 public function column_default( $item, $column_name ) {
506 switch ( $column_name ) {
507 case 'daily_count':
508 return \WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item[ $column_name ] ) );
509 default:
510 // Show the whole array for troubleshooting purposes.
511 return '';
512 }
513 }
514
515 /**
516 * Render the checkbox column.
517 *
518 * @param array $item Current item.
519 * @return string
520 */
521 public function column_cb( $item ) {
522 return sprintf(
523 '<input type="checkbox" name="%1$s[]" value="%2$s-%3$s" />',
524 'bulk-delete',
525 $item['ID'],
526 $item['blog_id']
527 );
528 }
529
530 /**
531 * Render the title column.
532 *
533 * @param array $item Current item.
534 * @return string
535 */
536 public function column_title( $item ) {
537 $context_key = $this->get_item_context_key( $item );
538 if ( '' !== $context_key ) {
539 return sprintf(
540 '<strong>%1$s</strong> <span style="color:grey">(%2$s, id:%3$s)</span>',
541 esc_html( $this->get_sitewide_context_label( $context_key ) ),
542 esc_html__( 'site-wide', 'top-10' ),
543 esc_html( $item['ID'] )
544 );
545 }
546
547 $delete_nonce = wp_create_nonce( 'tptn_delete_entry' );
548 $page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
549 $post = $this->network_wide ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );
550
551 if ( null === $post ) {
552 return sprintf(
553 '%s <span style="color:grey">(id:%2$s)</span>',
554 __( 'Invalid post ID. This post might have been deleted.', 'top-10' ),
555 $item['ID']
556 );
557 }
558
559 $actions = array(
560 'view' => sprintf( '<a href="%s" target="_blank">' . __( 'View', 'top-10' ) . '</a>', get_permalink( $item['ID'] ) ),
561 'edit' => sprintf( '<a href="%s">' . __( 'Edit', 'top-10' ) . '</a>', get_edit_post_link( $item['ID'] ) ),
562 'delete' => sprintf(
563 '<a href="?page=%1$s&action=%2$s&post=%3$s&blog_id=%4$s&_wpnonce=%5$s">' . __( 'Delete', 'top-10' ) . '</a>',
564 esc_attr( $page ),
565 'delete',
566 absint( $item['ID'] ),
567 absint( $item['blog_id'] ),
568 $delete_nonce
569 ),
570 );
571
572 // Return the title contents.
573 return sprintf(
574 '<a href="%4$s" target="_blank">%1$s</a> <span style="color:grey">(id:%2$s)</span>%3$s',
575 $post->post_title,
576 $item['ID'],
577 $this->network_wide ? '' : $this->row_actions( $actions ),
578 is_multisite() ? get_blog_permalink( $item['blog_id'], $item['ID'] ) : get_permalink( $item['ID'] )
579 );
580 }
581
582
583 /**
584 * Handles the post date column output.
585 *
586 * @param array $item Current item.
587 * @return string Post date.
588 */
589 public function column_date( $item ) {
590 if ( '' !== $this->get_item_context_key( $item ) ) {
591 return '';
592 }
593
594 $post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );
595
596 if ( $post ) {
597 $m_time = strtotime( $post->post_date );
598 $h_time = wp_date( get_option( 'date_format' ), $m_time );
599
600 return sprintf(
601 '<abbr title="%1$s">%1$s</abbr>',
602 esc_attr( $h_time )
603 );
604 }
605 return '';
606 }
607
608 /**
609 * Handles the post_type column output.
610 *
611 * @param array $item Current item.
612 * @return string Post Type.
613 */
614 public function column_post_type( $item ) {
615 if ( '' !== $this->get_item_context_key( $item ) ) {
616 return esc_html__( 'Site-wide', 'top-10' );
617 }
618
619 $post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );
620
621 if ( $post ) {
622 $pt = get_post_type_object( $post->post_type );
623 if ( empty( $pt ) ) {
624 return $post->post_type;
625 }
626 $name = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $pt->labels->name;
627 return $name;
628 }
629 return '';
630 }
631
632 /**
633 * Handles the post author column output.
634 *
635 * @param array $item Current item.
636 * @return string Post Author.
637 */
638 public function column_author( $item ) {
639 if ( '' !== $this->get_item_context_key( $item ) ) {
640 return '';
641 }
642
643 $post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );
644 if ( ! $post ) {
645 return '';
646 }
647
648 $author_info = get_userdata( (int) $post->post_author );
649 $author_name = ( false === $author_info ) ? '' : ucwords( trim( stripslashes( $author_info->display_name ), " \t\n\r\0\x0B" ) );
650
651 return sprintf(
652 '<a href="%s">%s</a>',
653 esc_url(
654 add_query_arg(
655 array(
656 'post_type' => $post->post_type,
657 'author' => ( false === $author_info ) ? 0 : $author_info->ID,
658 ),
659 get_admin_url( $item['blog_id'], 'edit.php' )
660 )
661 ),
662 esc_html( $author_name )
663 );
664 }
665
666 /**
667 * Render the Total Count column.
668 *
669 * @param array $item Current item.
670 * @return string
671 */
672 public function column_total_count( $item ) {
673 if ( '' !== $this->get_item_context_key( $item ) ) {
674 return \WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item['total_count'] ) );
675 }
676
677 return sprintf(
678 '<div contentEditable="true" class="live_edit" id="total_count_%1$s" data-wp-post-id="%1$s" data-wp-count="%2$s"%3$s>%4$s</div>',
679 $item['ID'],
680 $item['total_count'],
681 $this->network_wide ? ' data-wp-blog-id="' . esc_attr( $item['blog_id'] ) . '"' : '',
682 \WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item['total_count'] ) )
683 );
684 }
685
686 /**
687 * Handles the blog id column output.
688 *
689 * @param array $item Current item.
690 * @return void
691 */
692 public function column_blog_id( $item ) {
693
694 $blog_details = get_blog_details( $item['blog_id'] );
695
696 printf(
697 '<a href="%s" target="_blank">%s</a>',
698 esc_url(
699 add_query_arg(
700 array(
701 'page' => 'tptn_popular_posts',
702 ),
703 get_admin_url( $item['blog_id'] ) . 'admin.php'
704 )
705 ),
706 esc_html( $blog_details->blogname )
707 );
708 }
709
710 /**
711 * Get the number of days spanned by the current post-date-filter-from/to request,
712 * mirroring the default used in get_popular_posts() (today only when unset).
713 *
714 * @since 4.3.4
715 *
716 * @return int Number of days, minimum 1.
717 */
718 protected function get_current_filter_days() {
719 $from_date = isset( $_REQUEST['post-date-filter-from'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-from'] ) ) : current_time( 'd M Y' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
720 $to_date = isset( $_REQUEST['post-date-filter-to'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-to'] ) ) : current_time( 'd M Y' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
721
722 $from_timestamp = strtotime( $from_date );
723 $to_timestamp = strtotime( $to_date );
724
725 if ( false === $from_timestamp || false === $to_timestamp ) {
726 return 1;
727 }
728
729 $days = (int) round( ( $to_timestamp - $from_timestamp ) / DAY_IN_SECONDS ) + 1;
730
731 return max( 1, $days );
732 }
733
734 /**
735 * Associative array of columns
736 *
737 * @return array
738 */
739 public function get_columns() {
740 $columns = array(
741 'cb' => '<input type="checkbox" />',
742 'title' => __( 'Title', 'top-10' ),
743 'total_count' => __( 'Total visits', 'top-10' ),
744 /* translators: %s: Custom period label (e.g. Daily, Custom (7 days)). */
745 'daily_count' => sprintf( __( '%s visits', 'top-10' ), Helpers::get_daily_range_label( $this->get_current_filter_days() ) ),
746 'post_type' => __( 'Post type', 'top-10' ),
747 'author' => __( 'Author', 'top-10' ),
748 'date' => __( 'Date', 'top-10' ),
749 );
750
751 if ( $this->network_wide ) {
752 $columns['blog_id'] = __( 'Blog', 'top-10' );
753 }
754
755 /**
756 * Filter the columns displayed in the Posts list table.
757 *
758 * @since 1.5.0
759 *
760 * @param array $columns An array of column names.
761 */
762 return apply_filters( 'manage_pop_posts_columns', $columns );
763 }
764
765 /**
766 * Columns to make sortable.
767 *
768 * @return array
769 */
770 public function get_sortable_columns() {
771 $sortable_columns = array(
772 'total_count' => array( 'total_count', true ),
773 'daily_count' => array( 'daily_count', true ),
774 );
775 if ( ! $this->network_wide ) {
776 $sortable_columns['title'] = array( 'title', false );
777 }
778 return $sortable_columns;
779 }
780
781 /**
782 * Returns an associative array containing the bulk action
783 *
784 * @return array
785 */
786 public function get_bulk_actions() {
787 $actions = array(
788 'bulk-delete' => __( 'Delete Count', 'top-10' ),
789 );
790 return $actions;
791 }
792
793 /**
794 * Handles data query and filter, sorting, and pagination.
795 */
796 public function prepare_items() {
797
798 $this->_column_headers = $this->get_column_info();
799
800 /** Process bulk action */
801 $this->process_bulk_action();
802
803 $per_page = $this->get_items_per_page( 'pop_posts_per_page', 20 );
804
805 $current_page = $this->get_pagenum();
806
807 $args = array();
808
809 // If this is a search?
810 if ( isset( $_REQUEST['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
811 $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
812 }
813 // If this is a post type filter?
814 if ( isset( $_REQUEST['post-type-filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
815 $args['post-type-filter'] = sanitize_text_field( wp_unslash( $_REQUEST['post-type-filter'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
816 }
817
818 // If this is a post date filter?
819 if ( isset( $_REQUEST['post-date-filter-to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
820 $args['post-date-filter-to'] = sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
821 }
822 if ( isset( $_REQUEST['post-date-filter-from'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
823 $args['post-date-filter-from'] = sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-from'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
824 }
825
826 $this->items = self::get_popular_posts( $per_page, $current_page, $args );
827 $total_items = (int) self::record_count( $args );
828
829 $this->set_pagination_args(
830 array(
831 'total_items' => $total_items, // WE have to calculate the total number of items.
832 'per_page' => $per_page, // WE have to determine how many items to show on a page.
833 'total_pages' => (int) ceil( $total_items / $per_page ), // WE have to calculate the total number of pages.
834 )
835 );
836 }
837
838 /**
839 * Handles any bulk actions
840 */
841 public function process_bulk_action() {
842
843 // Detect when a bulk action is being triggered...
844 if ( 'delete' === $this->current_action() ) {
845 // In our file that handles the request, verify the nonce.
846 $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
847 $blog_id = isset( $_GET['blog_id'] ) ? absint( $_GET['blog_id'] ) : get_current_blog_id();
848
849 if ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'tptn_delete_entry' ) ) {
850 self::delete_post_count( $post_id, $blog_id );
851 } else {
852 die( esc_html__( 'Are you sure you want to do this', 'top-10' ) );
853 }
854 }
855
856 // If the delete bulk action is triggered.
857 if ( ( isset( $_REQUEST['action'] ) && 'bulk-delete' === $_REQUEST['action'] )
858 || ( isset( $_REQUEST['action2'] ) && 'bulk-delete' === $_REQUEST['action2'] )
859 ) {
860 $delete_ids = isset( $_REQUEST['bulk-delete'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_REQUEST['bulk-delete'] ) ) : array();
861
862 // Loop over the array of record IDs and delete them.
863 $post_ids = array();
864 $blog_ids = array();
865 foreach ( $delete_ids as $id ) {
866 $pieces = explode( '-', $id );
867 $post_ids[] = absint( $pieces[0] );
868 $blog_ids[] = absint( $pieces[1] );
869 }
870 \WebberZone\Top_Ten\Counter::delete_counts(
871 array(
872 'post_id' => $post_ids,
873 'blog_id' => $blog_ids,
874 'daily' => true,
875 )
876 );
877 \WebberZone\Top_Ten\Counter::delete_counts(
878 array(
879 'post_id' => $post_ids,
880 'blog_id' => $blog_ids,
881 'daily' => false,
882 )
883 );
884 }
885 }
886
887 /**
888 * Adds extra navigation elements to the table.
889 *
890 * @param string $which Which part of the table are we.
891 */
892 public function extra_tablenav( $which ) {
893 ?>
894 <div class="alignleft actions">
895 <?php
896 if ( 'top' === $which ) {
897 ob_start();
898
899 // Add date selector.
900 $current_date = current_time( 'd M Y' );
901
902 $post_date_from = isset( $_REQUEST['post-date-filter-from'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-from'] ) ) : $current_date; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
903 echo '<input type="text" id="datepicker-from" name="post-date-filter-from" value="' . esc_attr( $post_date_from ) . '" size="11" />';
904
905 $post_date_to = isset( $_REQUEST['post-date-filter-to'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-to'] ) ) : $current_date; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
906 echo '<input type="text" id="datepicker-to" name="post-date-filter-to" value="' . esc_attr( $post_date_to ) . '" size="11" />';
907
908 if ( ! $this->network_wide ) {
909 $post_types = get_post_types(
910 array(
911 'public' => true,
912 )
913 );
914 $post_types = $this->all_post_type + $post_types;
915
916 if ( $post_types ) {
917
918 echo '<select name="post-type-filter">';
919
920 foreach ( $post_types as $post_type ) {
921 $pt = get_post_type_object( $post_type );
922 $label = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $post_type;
923
924 $selected = '';
925 if ( isset( $_REQUEST['post-type-filter'] ) && $_REQUEST['post-type-filter'] === $post_type ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
926 $selected = ' selected = "selected"';
927 }
928 ?>
929 <option value="<?php echo esc_attr( $post_type ); ?>" <?php echo esc_attr( $selected ); ?>><?php echo esc_attr( $label ); ?></option>
930 <?php
931 }
932
933 echo '</select>';
934
935 }
936 }
937
938 $output = ob_get_clean();
939
940 if ( ! empty( $output ) ) {
941 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
942 submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'top-10-query-submit' ) );
943 }
944 }
945 ?>
946 </div>
947 <?php
948 }
949 }
950