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() ); } } }