# top-10/4.5.2/includes/admin/class-statistics-table.php

WebberZone Top 10 — Popular Posts, version 4.5.2. 950 lines.

- Page: https://pluginprobe.com/plugins/top-10/4.5.2/code/includes/admin/class-statistics-table.php
- Raw: https://pluginprobe.com/plugins/top-10/4.5.2/raw/includes/admin/class-statistics-table.php
- Modified: 2026-10-08T02:44:28+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/top-10/4.5.2/code/includes/admin/class-statistics-table.php#L10-L20`.

```php
<?php
/**
 * Statistics Table class.
 *
 * @package WebberZone\Top_Ten\Admin
 */

namespace WebberZone\Top_Ten\Admin;

use WebberZone\Top_Ten\Database;
use WebberZone\Top_Ten\Util\Helpers;

if ( ! defined( 'WPINC' ) ) {
	die;
}

if ( ! class_exists( '\WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Statistics Table class to display the popular counts.
 *
 * @since 3.3.0
 */
class Statistics_Table extends \WP_List_Table {

	/**
	 * Holds the post type array elements for translation.
	 *
	 * @var array
	 */
	public $all_post_type;

	/**
	 * Network wide popular posts flag.
	 *
	 * @var bool
	 */
	public $network_wide;

	/**
	 * Class constructor.
	 *
	 * @param bool $network_wide Network wide popular posts.
	 */
	public function __construct( $network_wide = false ) {
		parent::__construct(
			array(
				'singular' => __( 'popular_post', 'top-10' ), // Singular name of the listed records.
				'plural'   => __( 'popular_posts', 'top-10' ), // plural name of the listed records.
			)
		);
		$this->all_post_type = array(
			'all' => __( 'All post types', 'top-10' ),
		);
		$this->network_wide  = $network_wide;
	}

	/**
	 * Get the fixed IDs reserved for site-wide tracking contexts.
	 *
	 * The class only exists in the Pro build. Keeping this lookup optional lets
	 * the shared statistics table continue to work in the free plugin.
	 *
	 * @return array<int,int> Reserved context IDs.
	 */
	protected function get_sitewide_context_ids() {
		if ( ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
			return array();
		}

		return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_ids();
	}

	/**
	 * Build a SQL condition matching any reserved site-wide context ID.
	 *
	 * @param string $alias Count-table alias.
	 * @return string SQL condition, or an always-false condition when Pro is not loaded.
	 */
	protected function get_sitewide_context_condition( $alias = 'ttt' ) {
		$ids = $this->get_sitewide_context_ids();
		if ( empty( $ids ) ) {
			return '0=1';
		}

		return sprintf( '%s.postnumber IN (%s)', $alias, implode( ',', array_map( 'intval', $ids ) ) );
	}

	/**
	 * Get the site-wide context key for a result row.
	 *
	 * @param array $item Result row.
	 * @return string Context key, or an empty string for a post row.
	 */
	protected function get_item_context_key( $item ) {
		if ( ! isset( $item['ID'] ) || ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
			return '';
		}

		return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_key( $item['ID'], $item['blog_id'] ?? null );
	}

	/**
	 * Get a translated label for a site-wide context key.
	 *
	 * @param string $context_key Context key.
	 * @return string Context label.
	 */
	protected function get_sitewide_context_label( $context_key ) {
		if ( '' === $context_key || ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
			return '';
		}

		return \WebberZone\Top_Ten\Pro\Sitewide_Database::get_context_label( $context_key );
	}

	/**
	 * Build the search condition for site-wide context labels and keys.
	 *
	 * @param string $search Search text.
	 * @return string SQL condition, or an always-false condition when Pro is not loaded.
	 */
	protected function get_sitewide_context_search_condition( $search ) {
		if ( ! class_exists( 'WebberZone\\Top_Ten\\Pro\\Sitewide_Database' ) || ! \WebberZone\Top_Ten\Pro\Sitewide_Database::is_available() ) {
			return '0=1';
		}

		global $wpdb;

		$like       = '%' . $wpdb->esc_like( $search ) . '%';
		$conditions = array();
		foreach ( \WebberZone\Top_Ten\Pro\Sitewide_Database::CONTEXT_IDS as $context_key => $context_id ) {
			$searchable   = $context_key . ' ' . $this->get_sitewide_context_label( $context_key );
			$conditions[] = $wpdb->prepare( 'ttt.postnumber = %d AND %s LIKE %s', $context_id, $searchable, $like );
		}

		return '(' . implode( ' OR ', $conditions ) . ')';
	}

	/**
	 * Resolve a safe ordering clause for statistics queries.
	 *
	 * @since 4.5.0
	 *
	 * @param array $args Query arguments.
	 * @return array{orderby:string,order:string} Ordering fields.
	 */
	protected function get_ordering( $args ) {
		$orderby = '';
		if ( ! empty( $_REQUEST['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		} elseif ( ! empty( $args['orderby'] ) ) {
			$orderby = $args['orderby'];
		}

		if ( ! in_array( $orderby, array( 'title', 'daily_count', 'total_count' ), true ) ) {
			$orderby = 'total_count';
		}

		$order = '';
		if ( ! empty( $_REQUEST['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		} elseif ( ! empty( $args['order'] ) ) {
			$order = $args['order'];
		}

		if ( ! in_array( $order, array( 'asc', 'ASC', 'desc', 'DESC' ), true ) ) {
			$order = 'DESC';
		}

		return array(
			'orderby' => $orderby,
			'order'   => strtoupper( $order ),
		);
	}

	/**
	 * Fetch network-wide popular posts in two phases.
	 *
	 * The first query selects only the requested page in the sort order. The
	 * second query fills in the other count using exact post/blog pairs, which
	 * avoids joining and grouping the entire network's count tables.
	 *
	 * @since 4.5.0
	 *
	 * @param int    $per_page       Posts per page.
	 * @param int    $page_number    Page number.
	 * @param array  $args           Query arguments.
	 * @param string $from_date      Inclusive lower date boundary.
	 * @param string $to_date        Exclusive upper date boundary.
	 * @return array Array of popular posts.
	 */
	protected function get_network_popular_posts( $per_page, $page_number, $args, $from_date, $to_date ) {
		global $wpdb;

		$ordering = $this->get_ordering( $args );
		$orderby  = 'daily_count' === $ordering['orderby'] ? 'daily_count' : 'total_count';
		$offset   = max( 0, ( (int) $page_number - 1 ) * (int) $per_page );
		$limit    = max( 0, (int) $per_page );

		if ( 0 === $limit ) {
			return array();
		}

		$daily_table = $wpdb->base_prefix . 'top_ten_daily';
		$total_table = $wpdb->base_prefix . 'top_ten';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		if ( 'daily_count' === $orderby ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$sql = $wpdb->prepare(
				"SELECT postnumber AS ID, blog_id, SUM(cntaccess) AS daily_count
				FROM {$daily_table}
				WHERE dp_date >= %s AND dp_date < %s
				GROUP BY postnumber, blog_id
				ORDER BY daily_count {$ordering['order']}, postnumber ASC, blog_id ASC
				LIMIT %d, %d",
				$from_date,
				$to_date,
				$offset,
				$limit
			);
		} else {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$sql = $wpdb->prepare(
				"SELECT postnumber AS ID, blog_id, cntaccess AS total_count
				FROM {$total_table}
				ORDER BY cntaccess {$ordering['order']}, postnumber ASC, blog_id ASC
				LIMIT %d, %d",
				$offset,
				$limit
			);
		}

		$results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		if ( empty( $results ) ) {
			return array();
		}

		$pair_conditions = array();
		foreach ( $results as $result ) {
			$pair_conditions[] = $wpdb->prepare( '(postnumber = %d AND blog_id = %d)', $result['ID'], $result['blog_id'] );
		}
		$pairs = implode( ' OR ', $pair_conditions );

		if ( 'daily_count' === $orderby ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$other_results = $wpdb->get_results(
				"SELECT postnumber AS ID, blog_id, cntaccess AS total_count FROM {$total_table} WHERE {$pairs}",
				ARRAY_A
			); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		} else {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$other_results = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT postnumber AS ID, blog_id, SUM(cntaccess) AS daily_count
					FROM {$daily_table}
					WHERE dp_date >= %s AND dp_date < %s AND ({$pairs})
					GROUP BY postnumber, blog_id",
					$from_date,
					$to_date
				),
				ARRAY_A
			); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		}

		$other_counts = array();
		foreach ( $other_results as $result ) {
			$key                  = (int) $result['ID'] . ':' . (int) $result['blog_id'];
			$other_counts[ $key ] = $result[ 'daily_count' === $orderby ? 'total_count' : 'daily_count' ];
		}

		foreach ( $results as &$result ) {
			$key = (int) $result['ID'] . ':' . (int) $result['blog_id'];
			if ( 'daily_count' === $orderby ) {
				$result['total_count'] = isset( $other_counts[ $key ] ) ? $other_counts[ $key ] : 0;
			} else {
				$result['daily_count'] = isset( $other_counts[ $key ] ) ? $other_counts[ $key ] : 0;
			}
		}
		unset( $result );

		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $results;
	}

	/**
	 * Retrieve the Top 10 posts
	 *
	 * @param int   $per_page Posts per page.
	 * @param int   $page_number Page number.
	 * @param array $args Array of arguments.
	 *
	 * @return  array   Array of popular posts
	 */
	public function get_popular_posts( $per_page = 20, $page_number = 1, $args = null ) {

		global $wpdb;
		$args = is_array( $args ) ? $args : array();

		// Initialise some variables.
		$fields  = array();
		$where   = '';
		$join    = '';
		$groupby = '';
		$orderby = '';
		$limits  = '';
		$sql     = '';

		$blog_id = get_current_blog_id();

		$from_date = isset( $args['post-date-filter-from'] ) ? $args['post-date-filter-from'] : current_time( 'd M Y' );
		$from_date = gmdate( 'Y-m-d', strtotime( $from_date ) );
		$to_date   = isset( $args['post-date-filter-to'] ) ? $args['post-date-filter-to'] : current_time( 'd M Y' );
		$to_date   = gmdate( 'Y-m-d', strtotime( $to_date ) );
		$from_date = $from_date . ' 00:00:00';
		$to_date   = gmdate( 'Y-m-d 00:00:00', strtotime( $to_date . ' +1 day' ) );

		if ( $this->network_wide ) {
			$result = $this->get_network_popular_posts( $per_page, $page_number, $args, $from_date, $to_date );

			foreach ( $result as &$row ) {
				$row['context_key'] = $this->get_item_context_key( $row );
				if ( '' !== $row['context_key'] ) {
					$row['title']       = $this->get_sitewide_context_label( $row['context_key'] );
					$row['post_type']   = __( 'Site-wide', 'top-10' );
					$row['post_date']   = '';
					$row['post_author'] = 0;
				}
			}
			unset( $row );

			return $result;
		}

		/* Start creating the SQL */
		$table_name_daily = $wpdb->base_prefix . 'top_ten_daily';
		$table_name       = $wpdb->base_prefix . 'top_ten AS ttt';
		$sitewide_sql     = $this->get_sitewide_context_condition();

		// Fields to return.
		$fields[] = "{$wpdb->posts}.post_title as title";
		$fields[] = "{$wpdb->posts}.post_type";
		$fields[] = "{$wpdb->posts}.post_date";
		$fields[] = "{$wpdb->posts}.post_author";

		$fields[] = 'ttt.postnumber as ID';
		$fields[] = 'ttt.cntaccess as total_count';
		$fields[] = 'SUM(ttd.daily_count) as daily_count';
		$fields[] = 'ttt.blog_id as blog_id';

		$fields = implode( ', ', $fields );

		// Create the JOIN clause.
		$join .= " LEFT JOIN {$wpdb->posts} ON ttt.postnumber={$wpdb->posts}.ID ";
		$join .= $wpdb->prepare(
			" LEFT JOIN (
			SELECT postnumber, blog_id, SUM(cntaccess) AS daily_count FROM {$table_name_daily} " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			'WHERE dp_date >= %s AND dp_date < %s AND blog_id = %d
			GROUP BY postnumber, blog_id
			) AS ttd
			ON ttt.postnumber=ttd.postnumber AND ttt.blog_id=ttd.blog_id
			',
			$from_date,
			$to_date,
			$blog_id
		);

		// Create the base WHERE clause.
		$where .= $wpdb->prepare( ' AND ttt.blog_id = %d ', $blog_id ); // Posts need to be from the current blog only.
		$where .= " AND (($wpdb->posts.post_status = 'publish' OR $wpdb->posts.post_status = 'inherit') OR {$sitewide_sql}) ";   // Show published posts, attachments and site-wide contexts.

		// If search argument is set, do a search for it.
		if ( ! empty( $args['search'] ) ) {
			$post_search    = $wpdb->prepare( "$wpdb->posts.post_title LIKE %s", '%' . $wpdb->esc_like( $args['search'] ) . '%' );
			$context_search = $this->get_sitewide_context_search_condition( $args['search'] );
			$where         .= " AND ({$post_search} OR {$context_search}) ";
		}

		// If post filter argument is set, do a search for it.
		if ( isset( $args['post-type-filter'] ) && $this->all_post_type['all'] !== $args['post-type-filter'] ) {
			$where .= $wpdb->prepare( " AND $wpdb->posts.post_type = %s ", $args['post-type-filter'] );
		} else {
			$post_types = get_post_types(
				array(
					'public' => true,
				)
			);
			$where     .= " AND ($wpdb->posts.post_type IN ('" . join( "', '", $post_types ) . "') OR {$sitewide_sql}) ";
		}

		$ordering = $this->get_ordering( $args );
		$orderby  = $ordering['orderby'] . ' ' . $ordering['order'];

		// Create the base LIMITS clause.
		$limits = $wpdb->prepare( ' LIMIT %d, %d ', ( $page_number - 1 ) * $per_page, $per_page );

		$groupby = ' GROUP BY ttt.postnumber, ttt.blog_id ';
		$orderby = " ORDER BY {$orderby} ";

		$sql = "SELECT $fields FROM {$table_name} $join WHERE 1=1 $where $groupby $orderby $limits";

		$result = $wpdb->get_results( $sql, 'ARRAY_A' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		foreach ( $result as &$row ) {
			$row['context_key'] = $this->get_item_context_key( $row );
			if ( '' !== $row['context_key'] ) {
				$row['title']       = $this->get_sitewide_context_label( $row['context_key'] );
				$row['post_type']   = __( 'Site-wide', 'top-10' );
				$row['post_date']   = '';
				$row['post_author'] = 0;
			}
		}
		unset( $row );

		return $result;
	}

	/**
	 * Returns the count of records in the database.
	 *
	 * @param  array $args Array of arguments.
	 * @return null|string null|string
	 */
	public function record_count( $args = null ) {

		global $wpdb;
		$args = is_array( $args ) ? $args : array();

		if ( $this->network_wide ) {
			$cache_key = 'tptn_network_popular_posts_count';
			$cached    = get_site_transient( $cache_key );
			if ( false !== $cached ) {
				return (string) (int) $cached;
			}

			$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
			/** This filter is documented in includes/admin/class-dashboard-widgets.php */
			$ttl = max( 0, (int) apply_filters( 'tptn_network_dashboard_cache_ttl', 15 * MINUTE_IN_SECONDS ) );
			if ( $ttl > 0 ) {
				set_site_transient( $cache_key, $count, $ttl );
			}

			return (string) $count;
		}

		$where = '';
		$join  = '';

		$sql = "SELECT COUNT(*) FROM {$wpdb->base_prefix}top_ten as ttt";

		$join         = "LEFT JOIN {$wpdb->posts} ON ttt.postnumber={$wpdb->posts}.ID";
		$sitewide_sql = $this->get_sitewide_context_condition();
		$where       .= $wpdb->prepare( ' AND ttt.blog_id = %d ', get_current_blog_id() );
		$where       .= " AND (($wpdb->posts.post_status = 'publish' OR $wpdb->posts.post_status = 'inherit') OR {$sitewide_sql}) ";

		if ( ! empty( $args['search'] ) ) {
			$post_search    = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", '%' . $wpdb->esc_like( $args['search'] ) . '%' );
			$context_search = $this->get_sitewide_context_search_condition( $args['search'] );
			$where         .= " AND ({$post_search} OR {$context_search}) ";
		}

		if ( isset( $args['post-type-filter'] ) && $this->all_post_type['all'] !== $args['post-type-filter'] ) {
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type = %s ", $args['post-type-filter'] );
		} else {
			$post_types = get_post_types(
				array(
					'public' => true,
				)
			);
			$where     .= " AND ($wpdb->posts.post_type IN ('" . join( "', '", $post_types ) . "') OR {$sitewide_sql}) ";
		}
		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
	}

	/**
	 * Delete the post count for this post.
	 *
	 * @param int $id      Post ID.
	 * @param int $blog_id Blog ID.
	 */
	public static function delete_post_count( $id, $blog_id ) {
		\WebberZone\Top_Ten\Counter::delete_count( $id, $blog_id, false );
		\WebberZone\Top_Ten\Counter::delete_count( $id, $blog_id, true );
	}

	/**
	 * Text displayed when no post data is available
	 */
	public function no_items() {
		esc_html_e( 'No popular posts available.', 'top-10' );
	}


	/**
	 * Render a column when no column specific method exist.
	 *
	 * @param array  $item Current item.
	 * @param string $column_name Column name.
	 *
	 * @return mixed
	 */
	public function column_default( $item, $column_name ) {
		switch ( $column_name ) {
			case 'daily_count':
				return \WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item[ $column_name ] ) );
			default:
				// Show the whole array for troubleshooting purposes.
				return '';
		}
	}

	/**
	 * Render the checkbox column.
	 *
	 * @param array $item Current item.
	 * @return string
	 */
	public function column_cb( $item ) {
		return sprintf(
			'<input type="checkbox" name="%1$s[]" value="%2$s-%3$s" />',
			'bulk-delete',
			$item['ID'],
			$item['blog_id']
		);
	}

	/**
	 * Render the title column.
	 *
	 * @param array $item Current item.
	 * @return string
	 */
	public function column_title( $item ) {
		$context_key = $this->get_item_context_key( $item );
		if ( '' !== $context_key ) {
			return sprintf(
				'<strong>%1$s</strong> <span style="color:grey">(%2$s, id:%3$s)</span>',
				esc_html( $this->get_sitewide_context_label( $context_key ) ),
				esc_html__( 'site-wide', 'top-10' ),
				esc_html( $item['ID'] )
			);
		}

		$delete_nonce = wp_create_nonce( 'tptn_delete_entry' );
		$page         = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$post         = $this->network_wide ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );

		if ( null === $post ) {
			return sprintf(
				'%s <span style="color:grey">(id:%2$s)</span>',
				__( 'Invalid post ID. This post might have been deleted.', 'top-10' ),
				$item['ID']
			);
		}

		$actions = array(
			'view'   => sprintf( '<a href="%s" target="_blank">' . __( 'View', 'top-10' ) . '</a>', get_permalink( $item['ID'] ) ),
			'edit'   => sprintf( '<a href="%s">' . __( 'Edit', 'top-10' ) . '</a>', get_edit_post_link( $item['ID'] ) ),
			'delete' => sprintf(
				'<a href="?page=%1$s&action=%2$s&post=%3$s&blog_id=%4$s&_wpnonce=%5$s">' . __( 'Delete', 'top-10' ) . '</a>',
				esc_attr( $page ),
				'delete',
				absint( $item['ID'] ),
				absint( $item['blog_id'] ),
				$delete_nonce
			),
		);

		// Return the title contents.
		return sprintf(
			'<a href="%4$s" target="_blank">%1$s</a> <span style="color:grey">(id:%2$s)</span>%3$s',
			$post->post_title,
			$item['ID'],
			$this->network_wide ? '' : $this->row_actions( $actions ),
			is_multisite() ? get_blog_permalink( $item['blog_id'], $item['ID'] ) : get_permalink( $item['ID'] )
		);
	}


	/**
	 * Handles the post date column output.
	 *
	 * @param array $item Current item.
	 * @return string Post date.
	 */
	public function column_date( $item ) {
		if ( '' !== $this->get_item_context_key( $item ) ) {
			return '';
		}

		$post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );

		if ( $post ) {
			$m_time = strtotime( $post->post_date );
			$h_time = wp_date( get_option( 'date_format' ), $m_time );

			return sprintf(
				'<abbr title="%1$s">%1$s</abbr>',
				esc_attr( $h_time )
			);
		}
		return '';
	}

	/**
	 * Handles the post_type column output.
	 *
	 * @param array $item Current item.
	 * @return string Post Type.
	 */
	public function column_post_type( $item ) {
		if ( '' !== $this->get_item_context_key( $item ) ) {
			return esc_html__( 'Site-wide', 'top-10' );
		}

		$post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );

		if ( $post ) {
			$pt = get_post_type_object( $post->post_type );
			if ( empty( $pt ) ) {
				return $post->post_type;
			}
			$name = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $pt->labels->name;
			return $name;
		}
		return '';
	}

	/**
	 * Handles the post author column output.
	 *
	 * @param array $item Current item.
	 * @return string Post Author.
	 */
	public function column_author( $item ) {
		if ( '' !== $this->get_item_context_key( $item ) ) {
			return '';
		}

		$post = is_multisite() ? get_blog_post( $item['blog_id'], $item['ID'] ) : get_post( $item['ID'] );
		if ( ! $post ) {
			return '';
		}

		$author_info = get_userdata( (int) $post->post_author );
		$author_name = ( false === $author_info ) ? '' : ucwords( trim( stripslashes( $author_info->display_name ), " \t\n\r\0\x0B" ) );

		return sprintf(
			'<a href="%s">%s</a>',
			esc_url(
				add_query_arg(
					array(
						'post_type' => $post->post_type,
						'author'    => ( false === $author_info ) ? 0 : $author_info->ID,
					),
					get_admin_url( $item['blog_id'], 'edit.php' )
				)
			),
			esc_html( $author_name )
		);
	}

	/**
	 * Render the Total Count column.
	 *
	 * @param array $item Current item.
	 * @return string
	 */
	public function column_total_count( $item ) {
		if ( '' !== $this->get_item_context_key( $item ) ) {
			return \WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item['total_count'] ) );
		}

		return sprintf(
			'<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>',
			$item['ID'],
			$item['total_count'],
			$this->network_wide ? ' data-wp-blog-id="' . esc_attr( $item['blog_id'] ) . '"' : '',
			\WebberZone\Top_Ten\Util\Helpers::number_format_i18n( absint( $item['total_count'] ) )
		);
	}

	/**
	 * Handles the blog id column output.
	 *
	 * @param array $item Current item.
	 * @return void
	 */
	public function column_blog_id( $item ) {

		$blog_details = get_blog_details( $item['blog_id'] );

		printf(
			'<a href="%s" target="_blank">%s</a>',
			esc_url(
				add_query_arg(
					array(
						'page' => 'tptn_popular_posts',
					),
					get_admin_url( $item['blog_id'] ) . 'admin.php'
				)
			),
			esc_html( $blog_details->blogname )
		);
	}

	/**
	 * Get the number of days spanned by the current post-date-filter-from/to request,
	 * mirroring the default used in get_popular_posts() (today only when unset).
	 *
	 * @since 4.3.4
	 *
	 * @return int Number of days, minimum 1.
	 */
	protected function get_current_filter_days() {
		$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
		$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

		$from_timestamp = strtotime( $from_date );
		$to_timestamp   = strtotime( $to_date );

		if ( false === $from_timestamp || false === $to_timestamp ) {
			return 1;
		}

		$days = (int) round( ( $to_timestamp - $from_timestamp ) / DAY_IN_SECONDS ) + 1;

		return max( 1, $days );
	}

	/**
	 * Associative array of columns
	 *
	 * @return array
	 */
	public function get_columns() {
		$columns = array(
			'cb'          => '<input type="checkbox" />',
			'title'       => __( 'Title', 'top-10' ),
			'total_count' => __( 'Total visits', 'top-10' ),
			/* translators: %s: Custom period label (e.g. Daily, Custom (7 days)). */
			'daily_count' => sprintf( __( '%s visits', 'top-10' ), Helpers::get_daily_range_label( $this->get_current_filter_days() ) ),
			'post_type'   => __( 'Post type', 'top-10' ),
			'author'      => __( 'Author', 'top-10' ),
			'date'        => __( 'Date', 'top-10' ),
		);

		if ( $this->network_wide ) {
			$columns['blog_id'] = __( 'Blog', 'top-10' );
		}

		/**
		 * Filter the columns displayed in the Posts list table.
		 *
		 * @since 1.5.0
		 *
		 * @param   array   $columns    An array of column names.
		 */
		return apply_filters( 'manage_pop_posts_columns', $columns );
	}

	/**
	 * Columns to make sortable.
	 *
	 * @return array
	 */
	public function get_sortable_columns() {
		$sortable_columns = array(
			'total_count' => array( 'total_count', true ),
			'daily_count' => array( 'daily_count', true ),
		);
		if ( ! $this->network_wide ) {
			$sortable_columns['title'] = array( 'title', false );
		}
		return $sortable_columns;
	}

	/**
	 * Returns an associative array containing the bulk action
	 *
	 * @return array
	 */
	public function get_bulk_actions() {
		$actions = array(
			'bulk-delete' => __( 'Delete Count', 'top-10' ),
		);
		return $actions;
	}

	/**
	 * Handles data query and filter, sorting, and pagination.
	 */
	public function prepare_items() {

		$this->_column_headers = $this->get_column_info();

		/** Process bulk action */
		$this->process_bulk_action();

		$per_page = $this->get_items_per_page( 'pop_posts_per_page', 20 );

		$current_page = $this->get_pagenum();

		$args = array();

		// If this is a search?
		if ( isset( $_REQUEST['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}
		// If this is a post type filter?
		if ( isset( $_REQUEST['post-type-filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['post-type-filter'] = sanitize_text_field( wp_unslash( $_REQUEST['post-type-filter'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		// If this is a post date filter?
		if ( isset( $_REQUEST['post-date-filter-to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['post-date-filter-to'] = sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}
		if ( isset( $_REQUEST['post-date-filter-from'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['post-date-filter-from'] = sanitize_text_field( wp_unslash( $_REQUEST['post-date-filter-from'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		$this->items = self::get_popular_posts( $per_page, $current_page, $args );
		$total_items = (int) self::record_count( $args );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items, // WE have to calculate the total number of items.
				'per_page'    => $per_page, // WE have to determine how many items to show on a page.
				'total_pages' => (int) ceil( $total_items / $per_page ), // WE have to calculate the total number of pages.
			)
		);
	}

	/**
	 * Handles any bulk actions
	 */
	public function process_bulk_action() {

		// Detect when a bulk action is being triggered...
		if ( 'delete' === $this->current_action() ) {
			// In our file that handles the request, verify the nonce.
			$post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
			$blog_id = isset( $_GET['blog_id'] ) ? absint( $_GET['blog_id'] ) : get_current_blog_id();

			if ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'tptn_delete_entry' ) ) {
				self::delete_post_count( $post_id, $blog_id );
			} else {
				die( esc_html__( 'Are you sure you want to do this', 'top-10' ) );
			}
		}

		// If the delete bulk action is triggered.
		if ( ( isset( $_REQUEST['action'] ) && 'bulk-delete' === $_REQUEST['action'] )
			|| ( isset( $_REQUEST['action2'] ) && 'bulk-delete' === $_REQUEST['action2'] )
		) {
			$delete_ids = isset( $_REQUEST['bulk-delete'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_REQUEST['bulk-delete'] ) ) : array();

			// Loop over the array of record IDs and delete them.
			$post_ids = array();
			$blog_ids = array();
			foreach ( $delete_ids as $id ) {
				$pieces     = explode( '-', $id );
				$post_ids[] = absint( $pieces[0] );
				$blog_ids[] = absint( $pieces[1] );
			}
			\WebberZone\Top_Ten\Counter::delete_counts(
				array(
					'post_id' => $post_ids,
					'blog_id' => $blog_ids,
					'daily'   => true,
				)
			);
			\WebberZone\Top_Ten\Counter::delete_counts(
				array(
					'post_id' => $post_ids,
					'blog_id' => $blog_ids,
					'daily'   => false,
				)
			);
		}
	}

	/**
	 * Adds extra navigation elements to the table.
	 *
	 * @param string $which Which part of the table are we.
	 */
	public function extra_tablenav( $which ) {
		?>
		<div class="alignleft actions">
		<?php
		if ( 'top' === $which ) {
			ob_start();

			// Add date selector.
			$current_date = current_time( 'd M Y' );

			$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
			echo '<input type="text" id="datepicker-from" name="post-date-filter-from" value="' . esc_attr( $post_date_from ) . '" size="11" />';

			$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
			echo '<input type="text" id="datepicker-to" name="post-date-filter-to" value="' . esc_attr( $post_date_to ) . '" size="11" />';

			if ( ! $this->network_wide ) {
				$post_types = get_post_types(
					array(
						'public' => true,
					)
				);
				$post_types = $this->all_post_type + $post_types;

				if ( $post_types ) {

					echo '<select name="post-type-filter">';

					foreach ( $post_types as $post_type ) {
						$pt    = get_post_type_object( $post_type );
						$label = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $post_type;

						$selected = '';
						if ( isset( $_REQUEST['post-type-filter'] ) && $_REQUEST['post-type-filter'] === $post_type ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
							$selected = ' selected = "selected"';
						}
						?>
						<option value="<?php echo esc_attr( $post_type ); ?>" <?php echo esc_attr( $selected ); ?>><?php echo esc_attr( $label ); ?></option>
						<?php
					}

					echo '</select>';

				}
			}

			$output = ob_get_clean();

			if ( ! empty( $output ) ) {
				echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				submit_button( __( 'Filter' ), '', 'filter_action', false, array( 'id' => 'top-10-query-submit' ) );
			}
		}
		?>
		</div>
		<?php
	}
}

```
