Urls_Log_Entity::get_table_name(), // Plural value used for labels and the objects being listed.
'singular' => Urls_Log_Entity::get_table_name(), // Singular label for an object being listed, e.g. 'post'.
'ajax' => false, // If true, the parent class will call the _js_vars() method in the footer.
)
);
}
/**
* Inits class hooks. That is called every time - not in some specific environment set.
*
* @return void
*
* @since 4.9.0
*/
public static function init() {
\add_filter( 'advan_cron_hooks', array( __CLASS__, 'add_cron_job' ) );
\add_action( 'admin_post_' . self::PLUGIN_FILTER_ACTION, array( Urls_View::class, 'plugin_filter_action' ) );
// URL tracking is now handled by the URL_Tracker controller.
// See: \ADVAN\Controllers\URL_Tracker::init()
}
/**
* Adds a cron job for truncating the records in the urls table
*
* @param array $crons - The array with all the crons associated with the plugin.
*
* @return array
*
* @since 4.9.0
*/
public static function add_cron_job( $crons ) {
if ( -1 !== (int) Settings::get_option( 'advana_urls_clear' ) ) {
$crons[ ADVAN_PREFIX . 'urls_table_clear' ] = array(
'time' => Settings::get_option( 'advana_urls_clear' ),
'hook' => array( __CLASS__, 'truncate_urls_table' ),
'args' => array(),
);
}
return $crons;
}
/**
* Returns the columns for the table
*
* @return array
*
* @since 4.9.0
*/
public function get_columns() {
return self::manage_columns( array() );
}
/**
* Checkbox column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
protected function column_cb( $item ) {
$id = isset( $item['id'] ) ? (int) $item['id'] : 0;
$table = self::$table::get_name();
return sprintf(
'',
\esc_attr( $table ),
$id,
sprintf(
/* translators: The column name. */
__( 'Select %s', '0-day-analytics' ),
'id'
)
);
}
/**
* Returns the sortable columns
*
* @return array
*
* @since 4.9.0
*/
public function get_sortable_columns() {
return array(
'id' => array( 'id', false ),
'url' => array( 'url', false ),
'scheme' => array( 'scheme', false ),
'host' => array( 'host', false ),
'path' => array( 'path', false ),
'page_type' => array( 'page_type', false ),
'page_id' => array( 'page_id', false ),
'visit_count' => array( 'visit_count', false ),
'date_added' => array( 'date_added', false ),
);
}
/**
* Prepares the items for display
*
* @return void
*
* @since 4.9.0
*/
public function prepare_items() {
$this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() );
$per_page = $this->get_items_per_page( self::SCREEN_OPTIONS_SLUG . '_per_page', self::$rows_per_page );
$current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page;
$orderby = isset( $_GET['orderby'] ) ? \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) ) : self::$default_order_by; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = isset( $_GET['order'] ) ? \sanitize_text_field( \wp_unslash( $_GET['order'] ) ) : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$search = isset( $_GET[ self::SEARCH_INPUT ] ) ? \sanitize_text_field( \wp_unslash( $_GET[ self::SEARCH_INPUT ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET parameters for list table filtering.
$date_from = isset( $_GET['date_from'] ) ? \sanitize_text_field( \wp_unslash( $_GET['date_from'] ) ) : '';
$date_to = isset( $_GET['date_to'] ) ? \sanitize_text_field( \wp_unslash( $_GET['date_to'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$args = array(
'orderby' => $orderby,
'order' => $order,
'limit' => $per_page,
'offset' => $offset,
'search' => self::encode_url_search( $search ),
);
if ( ! empty( $date_from ) ) {
$args['date_from'] = strtotime( $date_from . ' 00:00:00' );
}
if ( ! empty( $date_to ) ) {
$args['date_to'] = strtotime( $date_to . ' 23:59:59' );
}
$items = Urls_Log_Entity::get_all( $args );
$this->items = $items;
$total_items = Urls_Log_Entity::count_all( $args );
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page ),
)
);
}
/**
* Default column handler
*
* @param array $item The item.
* @param string $column_name The column name.
*
* @return string
*
* @since 4.9.0
*/
public function column_default( $item, $column_name ) {
if ( isset( $item[ $column_name ] ) ) {
return esc_html( $item[ $column_name ] );
}
return '';
}
/**
* Displays the ID column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_id( $item ) {
return (int) $item['id'];
}
/**
* Displays the URL column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_path( $item ) {
return \esc_html( rawurldecode( $item['path'] ) );
}
/**
* Displays the URL column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_url( $item ) {
$id = (int) $item['id'];
$url = '' . \esc_html( rawurldecode( $item['url'] ) ) . '';
// Build delete URL preserving current filters and pagination.
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reading GET parameters for URL construction.
$delete_args = array(
'page' => Performance_View::MENU_SLUG,
'tab' => 'urls',
'action' => 'delete',
'id' => $id,
'_wpnonce' => \wp_create_nonce( 'delete_url_' . $id ),
);
if ( ! empty( $_REQUEST['paged'] ) ) {
$delete_args['paged'] = (int) $_REQUEST['paged'];
}
if ( ! empty( $_REQUEST[ static::SEARCH_INPUT ] ) ) {
$delete_args[ static::SEARCH_INPUT ] = \sanitize_text_field( \wp_unslash( $_REQUEST[ static::SEARCH_INPUT ] ) );
}
if ( ! empty( $_REQUEST['date_from'] ) ) {
$delete_args['date_from'] = \sanitize_text_field( \wp_unslash( $_REQUEST['date_from'] ) );
}
if ( ! empty( $_REQUEST['date_to'] ) ) {
$delete_args['date_to'] = \sanitize_text_field( \wp_unslash( $_REQUEST['date_to'] ) );
}
if ( ! empty( $_REQUEST['orderby'] ) ) {
$delete_args['orderby'] = \sanitize_text_field( \wp_unslash( $_REQUEST['orderby'] ) );
}
if ( ! empty( $_REQUEST['order'] ) ) {
$delete_args['order'] = \sanitize_text_field( \wp_unslash( $_REQUEST['order'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$delete_url = \add_query_arg( $delete_args, \admin_url( 'admin.php' ) );
// Add row actions.
$actions = array(
'details' => sprintf(
'%s',
$id,
__( 'View Details', '0-day-analytics' )
),
'delete' => sprintf(
'%s',
\esc_url( $delete_url ),
\esc_js( __( 'Are you sure you want to delete this URL record?', '0-day-analytics' ) ),
\__( 'Delete', '0-day-analytics' )
),
);
return sprintf(
'%s %s',
$url,
$this->row_actions( $actions )
);
}
/**
* Displays the page_id column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_page_id( $item ) {
$page_id = (int) $item['page_id'];
$page_type = $item['page_type'];
if ( $page_id > 0 ) {
if ( in_array( $page_type, array( 'single_post', 'page', 'front_page' ), true ) ) {
$url = \admin_url( 'post.php?post=' . $page_id . '&action=edit' );
return '' . $page_id . '';
} elseif ( 'category' === $page_type ) {
$url = \admin_url( 'term.php?taxonomy=category&tag_ID=' . $page_id );
return '' . $page_id . '';
} elseif ( 'tag' === $page_type ) {
$url = \admin_url( 'edit-tags.php?taxonomy=post_tag&tag_ID=' . $page_id );
return '' . $page_id . '';
} elseif ( 'taxonomy' === $page_type ) {
$term = \get_term( $page_id );
if ( $term && ! is_wp_error( $term ) ) {
$url = \admin_url( 'edit-tags.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $page_id );
return '' . $page_id . '';
}
} elseif ( 'attachment' === $page_type ) {
$url = \admin_url( 'post.php?post=' . $page_id . '&action=edit' );
return '' . $page_id . '';
}
}
return $page_id;
}
/**
* Displays the visit_count column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_visit_count( $item ) {
return (int) $item['visit_count'];
}
/**
* Displays the assets column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_assets( $item ) {
$id = (int) $item['id'];
$assets_data = ! empty( $item['assets_data'] ) ? json_decode( $item['assets_data'], true ) : null;
$assets_collected_at = ! empty( $item['assets_collected_at'] ) ? $item['assets_collected_at'] : null;
$output = '
';
if ( $assets_data && $assets_collected_at ) {
// Show summary.
$js_count = ! empty( $assets_data['js'] ) ? count( $assets_data['js'] ) : 0;
$css_count = ! empty( $assets_data['css'] ) ? count( $assets_data['css'] ) : 0;
$media_count = ! empty( $assets_data['media'] ) ? count( $assets_data['media'] ) : 0;
$output .= '
';
$output .= sprintf( '%s: %d
', \esc_html__( 'JS', '0-day-analytics' ), $js_count );
$output .= sprintf( '%s: %d
', \esc_html__( 'CSS', '0-day-analytics' ), $css_count );
$output .= sprintf( '%s: %d
', \esc_html__( 'Media', '0-day-analytics' ), $media_count );
$output .= sprintf(
'%s: %s
',
\esc_html__( 'Last collected', '0-day-analytics' ),
\gmdate( 'Y-m-d H:i', (int) $assets_collected_at )
);
$output .= '
';
// View Details button.
$output .= sprintf(
'
%s',
$id,
\esc_html__( 'View Details', '0-day-analytics' )
);
}
// Collect Assets button.
$output .= sprintf(
'
%s',
$id,
$assets_data ? \esc_html__( 'Refresh Assets', '0-day-analytics' ) : \esc_html__( 'Collect Assets', '0-day-analytics' )
);
$output .= '
';
return $output;
}
/**
* Displays the pagespeed column
*
* @param array $item The item.
*
* @return string
*
* @since 4.9.0
*/
public function column_pagespeed( $item ) {
$id = (int) $item['id'];
$pagespeed_data = ! empty( $item['pagespeed_data'] ) ? json_decode( $item['pagespeed_data'], true ) : null;
$pagespeed_collected_at = ! empty( $item['pagespeed_collected_at'] ) ? $item['pagespeed_collected_at'] : null;
$output = '';
if ( $pagespeed_data && $pagespeed_collected_at ) {
// Show summary (mobile and desktop scores).
$mobile_score = null;
$desktop_score = null;
if ( ! empty( $pagespeed_data['mobile_data']['lighthouseResult']['categories']['performance']['score'] ) ) {
$mobile_score = round( $pagespeed_data['mobile_data']['lighthouseResult']['categories']['performance']['score'] * 100 );
}
if ( ! empty( $pagespeed_data['desktop_data']['lighthouseResult']['categories']['performance']['score'] ) ) {
$desktop_score = round( $pagespeed_data['desktop_data']['lighthouseResult']['categories']['performance']['score'] * 100 );
}
$output .= '
';
if ( null !== $mobile_score ) {
$mobile_class = self::get_score_class( $mobile_score );
$output .= sprintf(
'%s: %d
',
\esc_attr( $mobile_class ),
\esc_html__( 'Mobile', '0-day-analytics' ),
$mobile_score
);
}
if ( null !== $desktop_score ) {
$desktop_class = self::get_score_class( $desktop_score );
$output .= sprintf(
'%s: %d
',
\esc_attr( $desktop_class ),
\esc_html__( 'Desktop', '0-day-analytics' ),
$desktop_score
);
}
$output .= sprintf(
'%s: %s
',
\esc_html__( 'Last collected', '0-day-analytics' ),
\gmdate( 'Y-m-d H:i', (int) $pagespeed_collected_at )
);
$output .= '
';
// View Details button.
$output .= sprintf(
'
%s',
$id,
\esc_html__( 'View Details', '0-day-analytics' )
);
}
// Run PageSpeed button.
$output .= sprintf(
'
%s',
$id,
$pagespeed_data ? \esc_html__( 'Refresh PageSpeed', '0-day-analytics' ) : \esc_html__( 'Run PageSpeed', '0-day-analytics' )
);
$output .= '
';
return $output;
}
/**
* Encodes a search term for matching against percent-encoded URL paths in the database.
*
* Non-ASCII characters (e.g. Cyrillic) and spaces are percent-encoded so that a
* human-readable search like "пица" matches the stored value "%D0%BF%D0%B8%D1%86%D0%B0".
* ASCII URL path characters (/, -, _, . etc.) are preserved unchanged.
*
* @param string $search Raw search term entered by the user.
*
* @return string Percent-encoded search term for a LIKE query against stored URL columns.
*
* @since 4.9.0
*/
private static function encode_url_search( string $search ): string {
return preg_replace_callback(
'/[ ]|[^\x00-\x7F]/u',
static function ( array $m ): string {
return rawurlencode( $m[0] );
},
$search
) ?? $search;
}
/**
* Get CSS class for PageSpeed score.
*
* @param int $score Score (0-100).
*
* @return string
*
* @since 4.9.0
*/
private static function get_score_class( $score ): string {
if ( $score >= 90 ) {
return 'score-good';
} elseif ( $score >= 50 ) {
return 'score-average';
} else {
return 'score-poor';
}
}
/**
* Display the date_added column.
*
* @param array $item URL data.
* @return string
*
* @since 4.9.0
*/
public function column_date_added( $item ) {
return \gmdate( 'Y-m-d H:i:s', (int) $item['date_added'] );
}
/**
* Manages the columns for the table.
*
* @param array $columns The columns.
*
* @return array
*
* @since 4.9.0
*/
public static function manage_columns( $columns ): array {
if ( empty( self::$admin_columns ) ) {
$admin_columns = Urls_Log_Entity::get_column_names_admin();
$screen_options = $admin_columns;
$table_columns = array(
'cb' => '', // to display the checkbox.
);
self::$admin_columns = \array_merge( $table_columns, $screen_options, $columns );
}
return self::$admin_columns;
}
/**
* Fetches the table data.
*
* @param array $args Arguments for fetching data.
*
* @return array
*
* @since 4.9.0
*/
public function fetch_table_data( array $args = array() ) {
$parsed_args = \wp_parse_args(
$args,
array(
'offset' => 0,
'search_string' => self::escaped_search_input(),
'per_page' => self::get_screen_option_per_page(),
'orderby' => 'visit_count',
'order' => 'DESC',
'count' => false,
)
);
$search_string = \sanitize_text_field( \wp_unslash( $parsed_args['search_string'] ) );
$offset = (int) $parsed_args['offset'];
$per_page = (int) $parsed_args['per_page'];
$orderby = \sanitize_text_field( \wp_unslash( $parsed_args['orderby'] ) );
$order = \sanitize_text_field( \wp_unslash( $parsed_args['order'] ) );
$query_args = array(
'orderby' => $orderby,
'order' => $order,
'limit' => $per_page,
'offset' => $offset,
'search' => self::encode_url_search( $search_string ),
);
$this->count = Urls_Log_Entity::count_all( $query_args );
if ( $parsed_args['count'] ) {
return array();
}
return Urls_Log_Entity::get_all( $query_args );
}
/**
* Returns an associative array containing the bulk actions.
*
* @return array
*
* @since 4.9.0
*/
public function get_bulk_actions() {
$actions = array(
'delete' => __( 'Delete Records', '0-day-analytics' ),
);
return $actions;
}
/**
* Extra table navigation for filters
*
* @param string $which - Position of the nav.
*
* @return void
*
* @since 4.9.0
*/
public function extra_tablenav( $which ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET parameters for list table filtering.
$date_from = isset( $_REQUEST['date_from'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['date_from'] ) ) : '';
$date_to = isset( $_REQUEST['date_to'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['date_to'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( 'top' === $which ) {
?>
handle_table_actions();
}
/**
* Handles table actions.
*
* @return void
*
* @since 4.9.0
*/
public function handle_table_actions() {
if ( ! \current_user_can( 'manage_options' ) ) {
return;
}
$action = $this->current_action();
if ( 'delete' === $action ) {
// Handle single delete from row actions.
if ( isset( $_REQUEST['id'] ) && ! is_array( $_REQUEST['id'] ) ) {
$id = absint( $_REQUEST['id'] );
$nonce = isset( $_REQUEST['_wpnonce'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
if ( ! \wp_verify_nonce( $nonce, 'delete_url_' . $id ) ) {
return;
}
if ( $id ) {
Urls_Log_Entity::delete_by_id( $id );
\wp_safe_redirect( \esc_url_raw( \add_query_arg( self::build_preserved_args( array( 'deleted' => 1 ) ), \admin_url( 'admin.php' ) ) ) );
exit;
}
} else {
// Handle bulk delete.
$nonce = isset( $_REQUEST['_wpnonce'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
if ( ! \wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
return;
}
$ids = isset( $_REQUEST['id'] ) ? (array) $_REQUEST['id'] : array();
$ids = array_map( 'absint', $ids );
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
Urls_Log_Entity::delete_by_id( $id );
}
\wp_safe_redirect( \esc_url_raw( \add_query_arg( self::build_preserved_args( array( 'deleted' => count( $ids ) ) ), \admin_url( 'admin.php' ) ) ) );
exit;
}
}
}
}
/**
* Returns translated text for per page option
*
* @return string
*
* @since 4.9.0
*/
private static function get_screen_per_page_title(): string {
return __( 'Site URLs performance', '0-day-analytics' );
}
/**
* Build an array of query args that preserves all current filters and pagination.
*
* @param array $extra Extra args to merge (e.g. 'deleted' => 1).
*
* @return array
*
* @since 5.1.0
*/
private static function build_preserved_args( array $extra = array() ): array {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reading GET parameters for redirect construction.
$args = array(
'page' => Performance_View::MENU_SLUG,
'tab' => 'urls',
);
if ( ! empty( $_REQUEST['paged'] ) && (int) $_REQUEST['paged'] > 1 ) {
$args['paged'] = (int) $_REQUEST['paged'];
}
if ( ! empty( $_REQUEST[ static::SEARCH_INPUT ] ) ) {
$args[ static::SEARCH_INPUT ] = \sanitize_text_field( \wp_unslash( $_REQUEST[ static::SEARCH_INPUT ] ) );
}
if ( ! empty( $_REQUEST['date_from'] ) ) {
$args['date_from'] = \sanitize_text_field( \wp_unslash( $_REQUEST['date_from'] ) );
}
if ( ! empty( $_REQUEST['date_to'] ) ) {
$args['date_to'] = \sanitize_text_field( \wp_unslash( $_REQUEST['date_to'] ) );
}
if ( ! empty( $_REQUEST['orderby'] ) ) {
$args['orderby'] = \sanitize_text_field( \wp_unslash( $_REQUEST['orderby'] ) );
}
if ( ! empty( $_REQUEST['order'] ) ) {
$args['order'] = \sanitize_text_field( \wp_unslash( $_REQUEST['order'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return array_merge( $args, $extra );
}
/**
* Truncates the URLs table from CRON job
*
* @return void
*
* @since 5.2.0
*/
public static function truncate_urls_table() {
Common_Table::truncate_table( null, Urls_Log_Entity::get_table_name() );
}
}
}